// ==UserScript== // @name YouTube + // @name:en YouTube + // @name:de YouTube + // @name:ja YouTube + // @name:tr YouTube + // @name:zh-CN YouTube + // @name:zh-TW YouTube + // @name:fr YouTube + // @name:ko YouTube + // @namespace by // @version 2.2 // @author diorhc // @description Вкладки для информации, комментариев, видео, плейлиста и скачивание видео и другие функции ↴ // @description:en Tabview YouTube and Download and others features ↴ // @description:de Tabview YouTube und Download und andere Funktionen ↴ // @description:fr Tabview YouTube et Télécharger et autres fonctionnalités ↴ // @description:zh-CN 标签视图 YouTube、下载及其他功能 ↴ // @description:zh-TW 標籤檢視 YouTube 及下載及其他功能 ↴ // @description:ko Tabview YouTube 및 다운로드 및 기타 기능 ↴ // @description:ja タブビューYouTubeとダウンロードおよびその他の機能 ↴ // @description:tr Sekmeli Görünüm YouTube ve İndir ve diğer özellikler ↴ // @match https://*.youtube.com/* // @match https://music.youtube.com/* // @match *://myactivity.google.com/* // @include *://www.youtube.com/feed/history/* // @include https://www.youtube.com // @include *://*.youtube.com/** // @exclude *://accounts.youtube.com/* // @exclude *://www.youtube.com/live_chat_replay* // @exclude *://www.youtube.com/persist_identity* // @exclude /^https?://\w+\.youtube\.com\/live_chat.*$/ // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @license MIT // @require https://cdn.jsdelivr.net/npm/@preact/signals-core@1.12.1/dist/signals-core.min.js // @require https://cdn.jsdelivr.net/npm/browser-id3-writer@4.4.0/dist/browser-id3-writer.min.js // @require https://cdn.jsdelivr.net/npm/preact@10.27.2/dist/preact.min.js // @require https://cdn.jsdelivr.net/npm/preact@10.27.2/hooks/dist/hooks.umd.js // @require https://cdn.jsdelivr.net/npm/@preact/signals@2.5.0/dist/signals.min.js // @require https://cdn.jsdelivr.net/npm/dayjs@1.11.19/dayjs.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect api.livecounts.io // @connect cnv.cx // @connect mp3yt.is // @connect web.archive.org // @connect * // @connect ytplaylist.robert.wesner.io // @connect youtube.com // @connect googlevideo.com // @connect self // @run-at document-start // @noframes // @homepageURL https://github.com/diorhc/YTP // @supportURL https://github.com/diorhc/YTP/issues // @downloadURL none // ==/UserScript== const MODULE_PREFIX = '[YouTube+]'; const MODULE_NAMES = { ADBLOCKER: `${MODULE_PREFIX}[Ad]`, BASIC: `${MODULE_PREFIX}[B]`, COMMENT: `${MODULE_PREFIX}[C]`, ENHANCED: `${MODULE_PREFIX}[E]`, ERROR_BOUNDARY: `${MODULE_PREFIX}[Err]`, I18N: `${MODULE_PREFIX}[i18n]`, MAIN: `${MODULE_PREFIX}[Main]`, MUSIC: `${MODULE_PREFIX}[Mus]`, PERFORMANCE: `${MODULE_PREFIX}[Perf]`, PIP: `${MODULE_PREFIX}[PIP]`, PLAYLIST_SEARCH: `${MODULE_PREFIX}[PL]`, REPORT: `${MODULE_PREFIX}[Rep]`, SHORTS: `${MODULE_PREFIX}[S]`, STATS: `${MODULE_PREFIX}[St]`, STYLE: `${MODULE_PREFIX}[Sty]`, THUMBNAIL: `${MODULE_PREFIX}[Th]`, TIMECODE: `${MODULE_PREFIX}[TC]`, UPDATE: `${MODULE_PREFIX}[Upd]`, UTILS: `${MODULE_PREFIX}[U]`, }; const DOWNLOAD_SITES = { Y2MATE: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}', }, }; const SVG_NS = 'http://www.w3.org/2000/svg'; const SELECTORS = { VIDEO_PLAYER: '.html5-video-player', VIDEO_ELEMENT: 'video', PLAYER_CONTAINER: '#movie_player', PRIMARY: '#primary', SECONDARY: '#secondary', COMMENTS: '#comments', DESCRIPTION: '#description', TITLE: 'h1.ytd-watch-metadata', CHANNEL_NAME: 'ytd-channel-name', SUBSCRIBE_BUTTON: '#subscribe-button', LIKE_BUTTON: 'like-button-view-model', }; const CLASS_NAMES = { YTP_BUTTON: 'ytp-button', YTP_SETTINGS_BUTTON: 'ytp-settings-button', HIDDEN: 'hidden', ACTIVE: 'active', }; const STORAGE_KEYS = { SETTINGS: 'youtube_plus_settings', TIMECODE_SETTINGS: 'youtube_timecode_settings', COMMENT_SETTINGS: 'youtube_comment_manager_settings', THEME: 'youtube_plus_theme', LANGUAGE: 'youtube_plus_language', }; const API_URLS = { GITHUB_REPO: 'https://github.com/diorhc/YTP', GITHUB_API: 'https://api.github.com/repos/diorhc/YTP/releases/latest', GREASYFORK: 'https://greasyfork.org/scripts/YOUR_SCRIPT_ID', }; const TIMING = { DEBOUNCE_SHORT: 100, DEBOUNCE_MEDIUM: 250, DEBOUNCE_LONG: 500, THROTTLE: 100, ANIMATION_DURATION: 300, TOAST_DURATION: 3000, RETRY_DELAY: 1000, OBSERVER_DELAY: 100, }; const LIMITS = { MAX_PLAYLIST_ITEMS: 5000, MAX_COMMENT_LENGTH: 10000, MAX_TITLE_LENGTH: 100, MAX_DESCRIPTION_LENGTH: 500, MAX_RETRIES: 3, RATE_LIMIT_REQUESTS: 10, RATE_LIMIT_WINDOW: 60000, }; const ERROR_MESSAGES = { INVALID_KEY: 'Key must be a non-empty string', OBSERVER_DISCONNECT_FAILED: 'Observer disconnect failed', FETCH_FAILED: 'Failed to fetch data', INVALID_VIDEO_ID: 'Invalid video ID', STORAGE_FAILED: 'Failed to save to localStorage', PARSE_FAILED: 'Failed to parse JSON', }; const URL_PATTERNS = { VIDEO_ID: /[?&]v=([^&]+)/, PLAYLIST_ID: /[?&]list=([^&]+)/, SHORTS: /\/shorts\/([^/?]+)/, TIMESTAMP: /[?&]t=(\d+)/, CHANNEL_ID: /\/(channel|c|user)\/([^/?]+)/, }; const UI_IDS = { DOWNLOAD_BUTTON: '.ytp-download-button', RIGHT_TABS_TOP_BUTTON: 'right-tabs-top-button', UNIVERSAL_TOP_BUTTON: 'universal-top-button', PLAYLIST_PANEL_TOP_BUTTON: 'playlist-panel-top-button', YTMUSIC_SIDE_PANEL_TOP_BUTTON: 'ytmusic-side-panel-top-button', STATS_MENU_CONTAINER: '.stats-menu-container', TIMECODE_PANEL: 'ytplus-timecode-panel', THUMBNAIL_STYLES: 'ytplus-thumbnail-styles', THUMBNAIL_MODAL_ACTION_BTN: 'thumbnail-modal-action-btn', SETTINGS_NAV_ITEM: 'ytp-plus-settings-nav-item', }; const SVG_ICONS = { ARROW_UP: '', SETTINGS: '', }; const STORAGE_PREFIXES = { TIMECODE: 'youtube_timecode_', COMMENT: 'youtube_comment_', SETTINGS: 'youtubeEnhancer', }; if (typeof window !== 'undefined') { window.YouTubePlusConstants = { MODULE_NAMES, DOWNLOAD_SITES, SVG_NS, SELECTORS, CLASS_NAMES, STORAGE_KEYS, API_URLS, TIMING, LIMITS, ERROR_MESSAGES, URL_PATTERNS, UI_IDS, SVG_ICONS, STORAGE_PREFIXES, }; } window.YouTubePlusConfig = { PRODUCTION_MODE: false, LOG_LEVELS: { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4, }, get currentLogLevel() { return this.PRODUCTION_MODE ? this.LOG_LEVELS.WARN : this.LOG_LEVELS.DEBUG; }, FEATURES: { PERFORMANCE_MONITORING: true, ERROR_BOUNDARY: true, AUTO_UPDATE_CHECK: true, ANALYTICS: false, }, VERSION: '2.2', BUILD_DATE: new Date().toISOString(), shouldLog(level) { return level >= this.currentLogLevel; }, log: { debug(...args) { if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.DEBUG)) { console.log('[YouTube+][DEBUG]', ...args); } }, info(...args) { if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.INFO)) { console.info('[YouTube+][INFO]', ...args); } }, warn(...args) { if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.WARN)) { console.warn('[YouTube+][WARN]', ...args); } }, error(...args) { if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.ERROR)) { console.error('[YouTube+][ERROR]', ...args); } }, }, }; if (typeof module !== 'undefined' && module.exports) { module.exports = window.YouTubePlusConfig; } (function () { 'use strict'; const DebugConfig = { enabled: false, performance: false, errors: true, domOperations: false, navigation: false, modules: false, attachDetach: false, tabOperations: false, api: false, storage: false, userActions: false, }; const debugLog = (category, ...args) => { if (!DebugConfig.enabled) return; if (!DebugConfig[category]) return; console.log(`[YouTube+ Debug:${category}]`, ...args); }; const debugWarn = (category, ...args) => { if (!DebugConfig.enabled) return; if (!DebugConfig[category]) return; console.warn(`[YouTube+ Debug:${category}]`, ...args); }; const debugError = (category, ...args) => { if (!DebugConfig.errors) return; console.error(`[YouTube+ Debug:${category}]`, ...args); }; const debugTime = label => { if (!DebugConfig.enabled || !DebugConfig.performance) { return () => {}; } const startTime = performance.now(); return () => { const endTime = performance.now(); console.log(`[YouTube+ Perf] ${label}: ${(endTime - startTime).toFixed(2)}ms`); }; }; const isDebugEnabled = category => { return DebugConfig.enabled && DebugConfig[category]; }; if (typeof window !== 'undefined') { window.YouTubePlusDebug = { config: DebugConfig, log: debugLog, warn: debugWarn, error: debugError, time: debugTime, isEnabled: isDebugEnabled, get DEBUG_5084() { return isDebugEnabled('attachDetach'); }, get DEBUG_5085() { return isDebugEnabled('tabOperations'); }, }; console.log( '[YouTube+] Debug system initialized. Use window.YouTubePlusDebug.config to configure.' ); } })(); (function () { 'use strict'; const LogLevel = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, CRITICAL: 4, NONE: 5, }; const config = { level: LogLevel.INFO, enabled: true, includeTimestamp: false, includeStack: true, maxStackLines: 5, customHandler: null, }; const getTimestamp = () => { const now = new Date(); return now.toISOString().substring(11, 23); }; const formatStack = error => { if (!error || !error.stack) return ''; const lines = error.stack.split('\n'); const relevantLines = lines.slice(0, config.maxStackLines); return `\n${relevantLines.join('\n')}`; }; const formatMessage = (module, level, args) => { const parts = ['[YouTube+]']; if (config.includeTimestamp) { parts.push(`[${getTimestamp()}]`); } parts.push(`[${level}]`); if (module) { parts.push(`[${module}]`); } return [parts.join(' '), ...args]; }; const shouldLog = level => { return config.enabled && level >= config.level; }; const addStackTraceIfNeeded = (formattedArgs, level, args) => { if (level >= LogLevel.ERROR && config.includeStack) { const lastArg = args[args.length - 1]; if (lastArg instanceof Error) { formattedArgs.push(formatStack(lastArg)); } } }; const outputLog = (module, level, levelName, consoleFn, formattedArgs) => { if (config.customHandler) { config.customHandler(module, level, levelName, formattedArgs); } else if (typeof consoleFn === 'function') { consoleFn(...formattedArgs); } }; const log = (module, level, levelName, consoleFn, args) => { if (!shouldLog(level)) return; try { const formattedArgs = formatMessage(module, levelName, args); addStackTraceIfNeeded(formattedArgs, level, args); outputLog(module, level, levelName, consoleFn, formattedArgs); } catch (err) { if (typeof console !== 'undefined' && console.error) { console.error('[YouTube+] Logger error:', err); } } }; class ModuleLogger { constructor(moduleName) { this.moduleName = moduleName || 'Unknown'; } debug(...args) { log(this.moduleName, LogLevel.DEBUG, 'DEBUG', console.log, args); } info(...args) { log(this.moduleName, LogLevel.INFO, 'INFO', console.log, args); } warn(...args) { log(this.moduleName, LogLevel.WARN, 'WARN', console.warn, args); } error(...args) { log(this.moduleName, LogLevel.ERROR, 'ERROR', console.error, args); } critical(...args) { log(this.moduleName, LogLevel.CRITICAL, 'CRITICAL', console.error, args); } log(level, ...args) { const levelMap = { debug: () => this.debug(...args), info: () => this.info(...args), warn: () => this.warn(...args), error: () => this.error(...args), critical: () => this.critical(...args), }; const logFn = levelMap[level.toLowerCase()]; if (logFn) { logFn(); } else { this.info(...args); } } } const createLogger = moduleName => { return new ModuleLogger(moduleName); }; const configure = options => { if (typeof options !== 'object' || options === null) return; if (typeof options.level === 'number') { config.level = options.level; } if (typeof options.enabled === 'boolean') { config.enabled = options.enabled; } if (typeof options.includeTimestamp === 'boolean') { config.includeTimestamp = options.includeTimestamp; } if (typeof options.includeStack === 'boolean') { config.includeStack = options.includeStack; } if (typeof options.maxStackLines === 'number') { config.maxStackLines = options.maxStackLines; } if (typeof options.customHandler === 'function') { config.customHandler = options.customHandler; } }; const setLevel = level => { if (typeof level === 'number') { config.level = level; } else if (typeof level === 'string') { const levelMap = { debug: LogLevel.DEBUG, info: LogLevel.INFO, warn: LogLevel.WARN, error: LogLevel.ERROR, critical: LogLevel.CRITICAL, none: LogLevel.NONE, }; const levelValue = levelMap[level.toLowerCase()]; if (levelValue !== undefined) { config.level = levelValue; } } }; const setEnabled = enabled => { config.enabled = !!enabled; }; if (typeof window !== 'undefined') { (window).YouTubePlusLogger = { createLogger, configure, setLevel, setEnabled, LogLevel, logger: createLogger('YouTube+'), }; } console.log('[YouTube+] Logger system initialized'); })(); (function () { 'use strict'; const PATTERNS = { URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/, VIDEO_ID: /^[a-zA-Z0-9_-]{11}$/, EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, STORAGE_KEY: /^[a-zA-Z0-9_.-]{1,100}$/, SAFE_STRING: /^[a-zA-Z0-9\s._-]{0,1000}$/, }; const MAX_LENGTHS = { URL: 2048, VIDEO_ID: 11, EMAIL: 254, STORAGE_KEY: 100, STORAGE_VALUE: 5242880, HTML_CONTENT: 1000000, USER_INPUT: 10000, TITLE: 500, DESCRIPTION: 5000, }; const DANGEROUS_PATTERNS = [ //gi, //gi, /javascript:/gi, /data:text\/html/gi, /on\w+\s*=/gi, // Event handlers like onclick= //gi, //gi, ]; const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']); const sanitizeHTML = (html, options = {}) => { if (typeof html !== 'string') return ''; let sanitizedHtml = html; if (sanitizedHtml.length > MAX_LENGTHS.HTML_CONTENT) { console.warn('[YouTube+][Security] HTML content exceeds maximum length, truncating'); sanitizedHtml = sanitizedHtml.substring(0, MAX_LENGTHS.HTML_CONTENT); } for (const pattern of DANGEROUS_PATTERNS) { sanitizedHtml = sanitizedHtml.replace(pattern, ''); } const escapeMap = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', }; let sanitized = sanitizedHtml.replace(/[<>&"'`=/]/g, char => escapeMap[char] || char); if (options.allowLinks) { sanitized = sanitized.replace( /<a\s+href="(https?:\/\/[^&]+)">([^&]+)<\/a>/gi, '$2' ); } if (options.allowBasicFormatting) { const allowedTags = ['b', 'i', 'u', 'strong', 'em', 'br']; for (const tag of allowedTags) { sanitized = sanitized.replace(new RegExp(`<${tag}>`, 'gi'), `<${tag}>`); sanitized = sanitized.replace(new RegExp(`</${tag}>`, 'gi'), ``); } } return sanitized; }; const validateURL = (url, options = {}) => { const result = { valid: false, sanitized: null, error: null, }; try { if (typeof url !== 'string') { result.error = 'URL must be a string'; return result; } if (url.length > MAX_LENGTHS.URL) { result.error = `URL exceeds maximum length of ${MAX_LENGTHS.URL} characters`; return result; } if (url.trim() !== url) { result.error = 'URL contains leading or trailing whitespace'; return result; } const parsed = new URL(url); if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) { result.error = `Protocol ${parsed.protocol} not allowed`; return result; } if (options.requireHttps && parsed.protocol !== 'https:') { result.error = 'HTTPS required'; return result; } if (options.allowedDomains && options.allowedDomains.length > 0) { const isAllowed = options.allowedDomains.some( domain => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`) ); if (!isAllowed) { result.error = `Domain ${parsed.hostname} not in whitelist`; return result; } } result.valid = true; result.sanitized = parsed.toString(); } catch (error) { result.error = `Invalid URL: ${error.message}`; } return result; }; const validateVideoId = videoId => { if (typeof videoId !== 'string') return false; return PATTERNS.VIDEO_ID.test(videoId); }; const validateEmail = email => { if (typeof email !== 'string') return false; if (email.length > MAX_LENGTHS.EMAIL) return false; return PATTERNS.EMAIL.test(email); }; const validateStorageKey = key => { if (typeof key !== 'string') return false; if (key.length === 0 || key.length > MAX_LENGTHS.STORAGE_KEY) return false; return PATTERNS.STORAGE_KEY.test(key); }; const sanitizeInput = (input, options = {}) => { if (typeof input !== 'string') return ''; const maxLength = options.maxLength || MAX_LENGTHS.USER_INPUT; const allowNewlines = options.allowNewlines !== false; const shouldTrim = options.trim !== false; let sanitized = input; if (shouldTrim) { sanitized = sanitized.trim(); } if (!allowNewlines) { sanitized = sanitized.replace(/[\r\n]+/g, ' '); } if (sanitized.length > maxLength) { sanitized = sanitized.substring(0, maxLength); } sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); return sanitized; }; const generateNonce = () => { const array = new Uint8Array(16); if (typeof crypto !== 'undefined' && crypto.getRandomValues) { crypto.getRandomValues(array); } else { for (let i = 0; i < array.length; i++) { array[i] = Math.floor(Math.random() * 256); } } return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); }; const isSecureContext = () => { return ( typeof window !== 'undefined' && (window.isSecureContext || window.location.protocol === 'https:') ); }; const validateJSON = (jsonString, maxSize = MAX_LENGTHS.STORAGE_VALUE) => { const result = { valid: false, parsed: null, error: null, }; try { if (typeof jsonString !== 'string') { result.error = 'Input must be a string'; return result; } if (jsonString.length > maxSize) { result.error = `JSON exceeds maximum size of ${maxSize} bytes`; return result; } result.parsed = JSON.parse(jsonString); result.valid = true; } catch (error) { result.error = `Invalid JSON: ${error.message}`; } return result; }; class RateLimiter { constructor(maxCalls, windowMs) { this.maxCalls = maxCalls; this.windowMs = windowMs; this.calls = []; } isAllowed(key = 'default') { const now = Date.now(); const windowStart = now - this.windowMs; this.calls = this.calls.filter(call => call.timestamp > windowStart); const keyCallCount = this.calls.filter(call => call.key === key).length; if (keyCallCount < this.maxCalls) { this.calls.push({ key, timestamp: now }); return true; } return false; } reset() { this.calls = []; } } if (typeof window !== 'undefined') { (window).YouTubePlusSecurity = { sanitizeHTML, validateURL, validateVideoId, validateEmail, validateStorageKey, sanitizeInput, generateNonce, isSecureContext, validateJSON, RateLimiter, PATTERNS, MAX_LENGTHS, }; } console.log('[YouTube+] Security module initialized'); })(); const sanitizeHTML = html => { if (typeof html !== 'string') return ''; let sanitizedHtml = html; if (sanitizedHtml.length > 1000000) { console.warn('[YouTube+] HTML content too large, truncating'); sanitizedHtml = sanitizedHtml.substring(0, 1000000); } const map = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', }; return sanitizedHtml.replace(/[<>&"'\/`=]/g, char => map[char] || char); }; const isValidURL = url => { if (typeof url !== 'string') return false; if (url.length > 2048) return false; if (url.trim() !== url) return false; try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol); } catch { return false; } }; const safeExecute = (fn, context = 'Unknown') => { return function (...args) { try { return fn.call(this, ...args); } catch (error) { console.error(`[YouTube+][${context}] Execution failed:`, error); return null; } }; }; const safeExecuteAsync = (fn, context = 'Unknown') => { return async function (...args) { try { return await fn.call(this, ...args); } catch (error) { console.error(`[YouTube+][${context}] Async execution failed:`, error); return null; } }; }; const sanitizeText = text => { if (typeof text !== 'string') return ''; return text.replace(/<[^>]*>/g, '').trim(); }; const sanitizeInput = (input, maxLength = 1000) => { if (typeof input !== 'string') return ''; const sanitized = sanitizeText(input); return sanitized.length > maxLength ? sanitized.substring(0, maxLength) : sanitized; }; const containsScriptTags = str => { if (typeof str !== 'string') return false; return /]*>.*?<\/script>/gi.test(str); }; const isValidEmail = email => { if (typeof email !== 'string') return false; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email) && email.length <= 254; }; if (typeof window !== 'undefined') { window.YouTubePlusSecurity = { sanitizeHTML, sanitizeText, sanitizeInput, isValidURL, safeExecute, safeExecuteAsync, containsScriptTags, isValidEmail, }; } const isValidKey = key => { if (typeof key !== 'string' || !key) return false; return /^[a-zA-Z0-9_.-]+$/.test(key); }; const storage = { get: (key, defaultValue = null) => { try { if (!isValidKey(key)) { console.warn(`[YouTube+][Storage] Invalid storage key: ${key}`); return defaultValue; } const value = localStorage.getItem(key); if (value === null) return defaultValue; if (value.length > 5242880) { console.warn(`[YouTube+][Storage] Value exceeds 5MB limit for key: ${key}`); return defaultValue; } return JSON.parse(value); } catch (e) { console.error(`[YouTube+][Storage] Failed to get item: ${key}`, e); return defaultValue; } }, set: (key, value) => { try { if (!isValidKey(key)) { console.warn(`[YouTube+][Storage] Invalid storage key: ${key}`); return false; } const serialized = JSON.stringify(value); if (serialized.length > 5242880) { console.warn(`[YouTube+][Storage] Serialized value exceeds 5MB limit for key: ${key}`); return false; } localStorage.setItem(key, serialized); return true; } catch (e) { console.error(`[YouTube+][Storage] Failed to set item: ${key}`, e); return false; } }, remove: key => { try { if (!isValidKey(key)) { console.warn(`[YouTube+][Storage] Invalid storage key: ${key}`); return false; } localStorage.removeItem(key); return true; } catch (e) { console.error(`[YouTube+][Storage] Failed to remove item: ${key}`, e); return false; } }, clear: () => { try { localStorage.clear(); return true; } catch (e) { console.error('[YouTube+][Storage] Failed to clear storage', e); return false; } }, has: key => { try { if (!isValidKey(key)) return false; return localStorage.getItem(key) !== null; } catch (e) { console.error(`[YouTube+][Storage] Failed to check key: ${key}`, e); return false; } }, }; if (typeof window !== 'undefined') { window.YouTubePlusStorage = storage; } const selectorCache = new Map(); const CACHE_MAX_SIZE = 50; const CACHE_MAX_AGE = 5000; const createElement = (tag, props = {}, children = []) => { const validTags = /^[a-z][a-z0-9-]*$/i; if (!validTags.test(tag)) { console.error( `[YouTube+][createElement] Invalid tag name:`, new Error(`Tag "${tag}" is not allowed`) ); return document.createElement('div'); } const element = document.createElement(tag); Object.entries(props).forEach(([key, value]) => { if (key === 'className') { element.className = value; } else if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else if (key.startsWith('on') && typeof value === 'function') { element.addEventListener(key.substring(2).toLowerCase(), value); } else if (key === 'dataset' && typeof value === 'object') { Object.assign(element.dataset, value); } else if (key === 'innerHTML' || key === 'outerHTML') { console.error( '[YouTube+][createElement] Direct HTML injection prevented:', new Error('Use children array instead') ); } else { try { element.setAttribute(key, value); } catch (e) { console.error(`[YouTube+][createElement] Failed to set attribute ${key}:`, e); } } }); children.forEach(child => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { element.appendChild(child); } }); return element; }; const querySelector = (selector, nocache = false) => { if (nocache) return document.querySelector(selector); const now = Date.now(); const cached = selectorCache.get(selector); if (cached?.element?.isConnected && now - cached.timestamp < CACHE_MAX_AGE) { return cached.element; } if (cached) { selectorCache.delete(selector); } const element = document.querySelector(selector); if (element) { if (selectorCache.size >= CACHE_MAX_SIZE) { const firstKey = selectorCache.keys().next().value; selectorCache.delete(firstKey); } selectorCache.set(selector, { element, timestamp: now }); } return element; }; const clearCache = () => { selectorCache.clear(); }; const isValidSelector = selector => { if (!selector || typeof selector !== 'string') return false; try { document.createDocumentFragment().querySelector(selector); return true; } catch { return false; } }; const validateWaitParameters = (selector, parent) => { if (!isValidSelector(selector)) { return new Error(`Invalid selector: ${selector}`); } if (!parent || !(parent instanceof Element)) { return new Error('Parent must be a valid DOM element'); } return null; }; const checkForExistingElement = (parent, selector) => { try { const element = parent.querySelector(selector); return { element, error: null }; } catch { return { element: null, error: new Error(`Invalid selector: ${selector}`) }; } }; const disconnectObserver = observer => { if (!observer) return; try { observer.disconnect(); } catch (e) { console.error(`[YouTube+][waitForElement] Observer disconnect failed:`, e); } }; const createWaitObserver = (parent, selector, resolve, timeoutId) => { const observer = new MutationObserver(() => { try { const element = parent.querySelector(selector); if (element) { clearTimeout(timeoutId); disconnectObserver(observer); resolve( ( (element))); } } catch (e) { console.error(`[YouTube+][waitForElement] Observer callback error:`, e); } }); try { observer.observe(parent, { childList: true, subtree: true }); return observer; } catch { return null; } }; const waitForElement = (selector, timeout = 5000, parent = document.body) => { return new Promise((resolve, reject) => { const validationError = validateWaitParameters(selector, parent); if (validationError) { reject(validationError); return; } const { element, error } = checkForExistingElement(parent, selector); if (error) { reject(error); return; } if (element) { resolve( ( (element))); return; } let observer = null; const timeoutId = setTimeout(() => { disconnectObserver(observer); reject(new Error(`Element ${selector} not found within ${timeout}ms`)); }, timeout); observer = createWaitObserver(parent, selector, resolve, timeoutId); if (!observer) { clearTimeout(timeoutId); reject(new Error('Failed to observe DOM')); } }); }; const isVisible = element => { if (!element || !(element instanceof HTMLElement)) return false; if (!element.isConnected) return false; const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const getElementDimensions = element => { if (!element || !(element instanceof HTMLElement)) { return { width: 0, height: 0, top: 0, left: 0 }; } const rect = element.getBoundingClientRect(); return { width: rect.width, height: rect.height, top: rect.top, left: rect.left, }; }; if (typeof window !== 'undefined') { window.YouTubePlusDOMHelper = { createElement, querySelector, waitForElement, clearCache, isValidSelector, isVisible, getElementDimensions, }; } const DOMManager = (() => { 'use strict'; const logError = (context, message, error) => { console.error(`[YouTube+][DOMManager][${context}] ${message}:`, error); }; const setClassName = (element, value) => { element.className = value; }; const setDataset = (element, dataObj) => { if (!dataObj || typeof dataObj !== 'object') return; for (const k in dataObj) { if (Object.prototype.hasOwnProperty.call(dataObj, k)) { element.dataset[k] = dataObj[k]; } } }; const setStyles = (element, styleObj) => { if (!styleObj || typeof styleObj !== 'object') return; for (const key in styleObj) { if (Object.prototype.hasOwnProperty.call(styleObj, key)) { try { element.style[key] = styleObj[key]; } catch { } } } }; const attachEventListener = (element, key, handler) => { const eventName = key.slice(2).toLowerCase(); element.addEventListener(eventName, handler); }; const setAttribute = (element, key, value) => { try { element.setAttribute(key, value); } catch (e) { logError('setAttribute', `Failed to set attribute ${key}`, e); } }; const attributeHandlers = { className: (element, value) => setClassName(element, value), dataset: (element, value) => setDataset(element, value), }; const isEventHandler = (key, value) => key.startsWith('on') && typeof value === 'function'; const isStyleObject = (key, value) => key === 'style' && typeof value === 'object'; const applyAttribute = (element, key, value) => { if (attributeHandlers[key]) { attributeHandlers[key](element, value); return; } if (isStyleObject(key, value)) { setStyles(element, value); return; } if (isEventHandler(key, value)) { attachEventListener(element, key, value); return; } setAttribute(element, key, value); }; const applyAttributes = (element, attrs) => { if (!attrs || typeof attrs !== 'object') return; for (const key in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, key)) { applyAttribute(element, key, attrs[key]); } } }; const appendChild = (element, child) => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { element.appendChild(child); } }; const appendChildren = (element, children) => { children.forEach(child => appendChild(element, child)); }; const createElement = (tag, attrs = {}, children = []) => { if (!tag || typeof tag !== 'string') { logError('createElement', 'Invalid tag', new Error('Tag must be a non-empty string')); return document.createElement('div'); } const element = document.createElement(tag); applyAttributes(element, attrs); appendChildren(element, children); return element; }; const selectorCache = new Map(); const CACHE_MAX_SIZE = 50; const CACHE_MAX_AGE = 5000; const querySelector = (selector, nocache = false) => { if (nocache) return document.querySelector(selector); const now = Date.now(); const cached = selectorCache.get(selector); if (cached?.element?.isConnected && now - cached.timestamp < CACHE_MAX_AGE) { return cached.element; } if (cached) { selectorCache.delete(selector); } const element = document.querySelector(selector); if (element) { if (selectorCache.size >= CACHE_MAX_SIZE) { const firstKey = selectorCache.keys().next().value; selectorCache.delete(firstKey); } selectorCache.set(selector, { element, timestamp: now }); } return element; }; const clearCache = () => selectorCache.clear(); const validateWaitParams = (selector, parent) => { if (!selector || typeof selector !== 'string') { return new Error('Selector must be a non-empty string'); } if (!parent || !(parent instanceof Element)) { return new Error('Parent must be a valid DOM element'); } return null; }; const checkExistingElement = (parent, selector) => { try { const element = parent.querySelector(selector); return { element, error: null }; } catch { return { element: null, error: new Error(`Invalid selector: ${selector}`) }; } }; const cleanupWaitResources = (observer, timeoutId) => { if (observer) { try { observer.disconnect(); } catch (e) { logError('waitForElement', 'Observer disconnect failed', e); } } clearTimeout(timeoutId); }; const createElementObserver = (parent, selector, resolve, timeoutId) => { const observer = new MutationObserver(() => { try { const element = parent.querySelector(selector); if (element) { cleanupWaitResources(observer, timeoutId); resolve( (element)); } } catch (e) { logError('waitForElement', 'Observer callback error', e); } }); try { observer.observe(parent, { childList: true, subtree: true }); return observer; } catch { return null; } }; const waitForElement = (selector, timeout = 5000, parent = document.body) => { return new Promise((resolve, reject) => { const validationError = validateWaitParams(selector, parent); if (validationError) { reject(validationError); return; } const { element, error } = checkExistingElement(parent, selector); if (error) { reject(error); return; } if (element) { resolve( (element)); return; } let observer = null; const timeoutId = setTimeout(() => { cleanupWaitResources(observer, timeoutId); reject(new Error(`Element ${selector} not found within ${timeout}ms`)); }, timeout); observer = createElementObserver(parent, selector, resolve, timeoutId); if (!observer) { clearTimeout(timeoutId); reject(new Error('Failed to observe DOM')); } }); }; const cleanupFunctions = functions => { functions.forEach(fn => { try { fn(); } catch (e) { logError('Cleanup', 'Cleanup function failed', e); } }); functions.clear(); }; const cleanupObservers = observers => { observers.forEach(obs => { try { obs.disconnect(); } catch (e) { logError('Cleanup', 'Observer disconnect failed', e); } }); observers.clear(); }; const cleanupListeners = listeners => { listeners.forEach(({ element, event, handler, options }) => { try { element.removeEventListener(event, handler, options); } catch (e) { logError('Cleanup', 'Listener removal failed', e); } }); listeners.clear(); }; const cleanupIntervals = intervals => { intervals.forEach(id => clearInterval(id)); intervals.clear(); }; const cleanupTimeouts = timeouts => { timeouts.forEach(id => clearTimeout(id)); timeouts.clear(); }; const cleanupAnimationFrames = frames => { frames.forEach(id => cancelAnimationFrame(id)); frames.clear(); }; const cleanupManager = { observers: new Set(), listeners: new Map(), intervals: new Set(), timeouts: new Set(), animationFrames: new Set(), cleanupFunctions: new Set(), register: fn => { if (typeof fn === 'function') { cleanupManager.cleanupFunctions.add(fn); } return fn; }, unregister: fn => { cleanupManager.cleanupFunctions.delete(fn); }, registerObserver: observer => { cleanupManager.observers.add(observer); return observer; }, unregisterObserver: observer => { if (observer) { try { observer.disconnect(); } catch (e) { logError('Cleanup', 'Observer disconnect failed', e); } cleanupManager.observers.delete(observer); } }, registerListener: (element, event, handler, options) => { const key = Symbol('listener'); cleanupManager.listeners.set(key, { element, event, handler, options }); try { element.addEventListener(event, handler, options); } catch (e) { logError('registerListener', 'Failed to add listener', e); } return key; }, unregisterListener: key => { const listener = cleanupManager.listeners.get(key); if (listener) { const { element, event, handler, options } = listener; try { element.removeEventListener(event, handler, options); } catch (e) { logError('Cleanup', 'Listener removal failed', e); } cleanupManager.listeners.delete(key); } }, registerInterval: id => { cleanupManager.intervals.add(id); return id; }, unregisterInterval: id => { clearInterval(id); cleanupManager.intervals.delete(id); }, registerTimeout: id => { cleanupManager.timeouts.add(id); return id; }, unregisterTimeout: id => { clearTimeout(id); cleanupManager.timeouts.delete(id); }, registerAnimationFrame: id => { cleanupManager.animationFrames.add(id); return id; }, unregisterAnimationFrame: id => { cancelAnimationFrame(id); cleanupManager.animationFrames.delete(id); }, cleanup: () => { cleanupFunctions(cleanupManager.cleanupFunctions); cleanupObservers(cleanupManager.observers); cleanupListeners(cleanupManager.listeners); cleanupIntervals(cleanupManager.intervals); cleanupTimeouts(cleanupManager.timeouts); cleanupAnimationFrames(cleanupManager.animationFrames); }, }; return { createElement, querySelector, clearCache, waitForElement, cleanupManager, }; })(); if (typeof window !== 'undefined') { window.YouTubePlusDOMManager = DOMManager; } const SettingsManager = (() => { 'use strict'; const Storage = window.YouTubePlusStorage || {}; const storageGet = key => { if (Storage.get) return Storage.get(key); try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : null; } catch { return null; } }; const storageSet = (key, value) => { if (Storage.set) { Storage.set(key, value); return; } try { localStorage.setItem(key, JSON.stringify(value)); } catch { } }; const STORAGE_KEY = 'youtube_plus_all_settings_v2'; const defaults = { speedControl: { enabled: true, currentSpeed: 1 }, screenshot: { enabled: true }, download: { enabled: true }, updateChecker: { enabled: true }, adBlocker: { enabled: true }, pip: { enabled: true }, timecodes: { enabled: true }, }; const load = () => { const saved = storageGet(STORAGE_KEY); return saved ? { ...defaults, ...saved } : { ...defaults }; }; const save = settings => { storageSet(STORAGE_KEY, settings); window.dispatchEvent( new CustomEvent('youtube-plus-settings-changed', { detail: settings, }) ); }; const get = path => { const settings = load(); return path.split('.').reduce((obj, key) => obj?.[key], settings); }; const set = (path, value) => { const settings = load(); const keys = path.split('.'); const last = keys.pop(); const target = keys.reduce((obj, key) => { obj[key] = obj[key] || {}; return obj[key]; }, settings); target[last] = value; save(settings); }; const reset = () => { save({ ...defaults }); }; const update = updates => { const settings = load(); const merged = { ...settings, ...updates }; save(merged); }; return { load, save, get, set, reset, update, defaults, }; })(); if (typeof window !== 'undefined') { window.YouTubePlusSettingsManager = SettingsManager; } const StyleManager = (() => { 'use strict'; const styles = new Map(); let styleElement = null; const logError = (message, error) => { console.error(`[YouTube+][StyleManager] ${message}:`, error); }; const update = () => { try { if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = 'youtube-plus-styles'; styleElement.type = 'text/css'; (document.head || document.documentElement).appendChild(styleElement); } styleElement.textContent = Array.from(styles.values()).join('\n'); } catch (error) { logError('Failed to update styles', error); } }; const add = (id, css) => { if (typeof id !== 'string' || !id) { logError('Invalid style ID', new Error('ID must be a non-empty string')); return; } if (typeof css !== 'string') { logError('Invalid CSS', new Error('CSS must be a string')); return; } styles.set(id, css); update(); }; const remove = id => { styles.delete(id); update(); }; const clear = () => { styles.clear(); if (styleElement) { try { styleElement.remove(); } catch (e) { logError('Failed to remove style element', e); } styleElement = null; } }; const has = id => styles.has(id); const get = id => styles.get(id); const getIds = () => Array.from(styles.keys()); return { add, remove, clear, has, get, getIds, styles, }; })(); if (typeof window !== 'undefined') { window.YouTubePlusStyleManager = StyleManager; } const NotificationManager = (() => { 'use strict'; const DOMManager = window.YouTubePlusDOMManager || {}; const createElement = DOMManager.createElement || ((tag, attrs, children) => { const el = document.createElement(tag); if (attrs?.className) { el.className = attrs.className; } if (children) { children.forEach(c => el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c) ); } return el; }); const _queue = []; const activeNotifications = new Set(); const _MAX_VISIBLE = 3; const DEFAULT_DURATION = 3000; const CONTAINER_ID = 'youtube-enhancer-notification-container'; const POSITION_PRESETS = { 'top-right': { top: '20px', right: '20px' }, 'top-left': { top: '20px', left: '20px' }, 'bottom-right': { bottom: '20px', right: '20px' }, 'bottom-left': { bottom: '20px', left: '20px' }, }; const logError = (message, error) => { console.error(`[YouTube+][NotificationManager] ${message}:`, error); }; const getContainer = () => { let container = document.getElementById(CONTAINER_ID); if (!container) { container = createElement('div', { id: CONTAINER_ID, className: 'youtube-enhancer-notification-container', }); try { document.body.appendChild(container); } catch (e) { logError('Failed to create container', e); return null; } } return container; }; const animateRemoval = notification => { notification.style.opacity = '0'; notification.style.transform = 'translateY(20px)'; }; const cleanupNotification = notification => { try { notification.remove(); activeNotifications.delete(notification); } catch (e) { logError('Failed to remove notification', e); } }; const remove = notification => { if (!notification) return; try { animateRemoval(notification); setTimeout(() => cleanupNotification(notification), 300); } catch (e) { logError('Failed to animate notification removal', e); } }; const getPositionStyles = position => { return position && POSITION_PRESETS[position] ? POSITION_PRESETS[position] : {}; }; const createMessageElement = message => { return createElement('span', { style: { flex: '1' } }, [message]); }; const createActionButton = (action, notification) => { const actionBtn = createElement( 'button', { style: { background: 'rgba(255,255,255,0.2)', border: '1px solid rgba(255,255,255,0.3)', color: 'white', padding: '4px 12px', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', fontWeight: '600', transition: 'background 0.2s', }, }, [action.text] ); actionBtn.addEventListener('click', () => { action.callback(); remove(notification); }); return actionBtn; }; const setAccessibilityAttributes = notification => { notification.setAttribute('role', 'status'); notification.setAttribute('aria-live', 'polite'); notification.setAttribute('aria-atomic', 'true'); }; const createNotification = (message, options = {}) => { const { position = null, action = null } = options; const notification = createElement('div', { className: 'youtube-enhancer-notification', dataset: { message }, style: { zIndex: '10001', width: 'auto', display: 'flex', alignItems: 'center', gap: '10px', ...getPositionStyles(position), }, }); setAccessibilityAttributes(notification); const messageSpan = createMessageElement(message); notification.appendChild(messageSpan); if (action && action.text && typeof action.callback === 'function') { const actionBtn = createActionButton(action, notification); notification.appendChild(actionBtn); } return notification; }; const isValidMessage = message => { if (!message || typeof message !== 'string') { logError('Invalid message', new Error('Message must be a non-empty string')); return false; } return true; }; const removeDuplicates = message => { activeNotifications.forEach(notif => { if (notif.dataset.message === message) { remove(notif); } }); }; const addToContainer = (notification, container) => { container.appendChild(notification); activeNotifications.add(notification); }; const scheduleRemoval = (notification, duration) => { if (duration > 0) { setTimeout(() => remove(notification), duration); } }; const show = (message, options = {}) => { if (!isValidMessage(message)) { return null; } const { duration = DEFAULT_DURATION } = options; removeDuplicates(message); const container = getContainer(); if (!container) return null; try { const notification = createNotification(message, options); addToContainer(notification, container); scheduleRemoval(notification, duration); return notification; } catch (error) { logError('Failed to show notification', error); return null; } }; const clearAll = () => { activeNotifications.forEach(notif => remove(notif)); activeNotifications.clear(); }; const success = (message, options = {}) => { return show(`✓ ${message}`, { ...options, type: 'success' }); }; const error = (message, options = {}) => { return show(`✗ ${message}`, { ...options, type: 'error', duration: 5000 }); }; const info = (message, options = {}) => { return show(`ℹ ${message}`, { ...options, type: 'info' }); }; return { show, remove, clearAll, success, error, info, activeNotifications, }; })(); if (typeof window !== 'undefined') { window.YouTubePlusNotificationManager = NotificationManager; } const measurePerformance = (label, fn) => { return function (...args) { const start = performance.now(); try { const result = fn.apply(this, args); const duration = performance.now() - start; if (duration > 100) { console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`); } return result; } catch (error) { console.error(`[YouTube+][Performance] ${label} failed:`, error); throw error; } }; }; const measurePerformanceAsync = (label, fn) => { return async function (...args) { const start = performance.now(); try { const result = await fn.apply(this, args); const duration = performance.now() - start; if (duration > 100) { console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`); } return result; } catch (error) { console.error(`[YouTube+][Performance] ${label} failed:`, error); throw error; } }; }; const measureBlock = async (label, fn) => { const start = performance.now(); try { await fn(); const duration = performance.now() - start; console.log(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`); return duration; } catch (error) { console.error(`[YouTube+][Performance] ${label} failed:`, error); throw error; } }; const debounce = (func, wait, options = {}) => { let timeout = null; let lastArgs = null; let lastThis = null; const debounced = function (...args) { lastArgs = args; lastThis = this; if (timeout !== null) clearTimeout(timeout); if (options.leading && timeout === null) { func.call(this, ...args); } timeout = setTimeout(() => { if (!options.leading && lastArgs) { func.call(lastThis, ...lastArgs); } timeout = null; lastArgs = null; lastThis = null; }, wait); }; debounced.cancel = () => { if (timeout !== null) clearTimeout(timeout); timeout = null; lastArgs = null; lastThis = null; }; return debounced; }; const throttle = (func, limit) => { let inThrottle = false; let lastResult = undefined; return function (...args) { if (!inThrottle) { lastResult = func.call(this, ...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } return lastResult; }; }; const retryAsync = async (fn, retries = 3, delay = 1000) => { for (let i = 0; i < retries; i++) { try { return await fn(); } catch (error) { if (i === retries - 1) throw error; await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); } } }; if (typeof window !== 'undefined') { window.YouTubePlusPerformance = { measurePerformance, measurePerformanceAsync, measureBlock, debounce, throttle, retryAsync, }; } (function () { 'use strict'; const logError = (module, message, error) => { try { const errorDetails = { module, message, error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack, } : error, timestamp: new Date().toISOString(), userAgent: typeof navigator === 'undefined' ? 'unknown' : navigator.userAgent, url: typeof window === 'undefined' ? 'unknown' : window.location.href, }; console.error(`[YouTube+][${module}] ${message}:`, error); console.debug('[YouTube+] Error details:', errorDetails); } catch (loggingError) { console.error('[YouTube+] Error logging failed:', loggingError); } }; const debounce = (fn, ms, options = {}) => { let timeout = null; let lastArgs = null; let lastThis = null; const debounced = function (...args) { lastArgs = args; lastThis = this; clearTimeout(timeout); if (options.leading && !timeout) { (fn).call(this, ...args); } timeout = setTimeout(() => { if (!options.leading) (fn).call(lastThis, ...lastArgs); timeout = null; lastArgs = null; lastThis = null; }, ms); }; debounced.cancel = () => { clearTimeout(timeout); timeout = null; lastArgs = null; lastThis = null; }; return (debounced); }; const throttle = (fn, limit) => { let inThrottle = false; let lastResult; const throttled = function (...args) { if (!inThrottle) { lastResult = (fn).call(this, ...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } return lastResult; }; return (throttled); }; const StyleManager = (function () { const styles = new Map(); return { add(id, css) { try { let el = document.getElementById(id); if (!el) { el = document.createElement('style'); el.id = id; document.head.appendChild(el); } styles.set(id, css); el.textContent = Array.from(styles.values()).join('\n\n'); } catch (e) { logError('StyleManager', 'add failed', e); } }, remove(id) { try { styles.delete(id); const el = document.getElementById(id); if (el) el.remove(); } catch (e) { logError('StyleManager', 'remove failed', e); } }, clear() { for (const id of Array.from(styles.keys())) this.remove(id); }, }; })(); const cleanupManager = (function () { const observers = new Set(); const listeners = new Map(); const intervals = new Set(); const timeouts = new Set(); const animationFrames = new Set(); return { registerObserver(o) { try { observers.add(o); } catch {} return o; }, registerListener(target, ev, fn, opts) { try { target.addEventListener(ev, fn, opts); const key = Symbol(); listeners.set(key, { target, ev, fn, opts }); return key; } catch (e) { logError('cleanupManager', 'registerListener failed', e); return null; } }, registerInterval(id) { intervals.add(id); return id; }, registerTimeout(id) { timeouts.add(id); return id; }, registerAnimationFrame(id) { animationFrames.add(id); return id; }, cleanup() { try { for (const o of observers) { try { o.disconnect(); } catch {} } observers.clear(); for (const keyEntry of listeners.values()) { try { keyEntry.target.removeEventListener(keyEntry.ev, keyEntry.fn, keyEntry.opts); } catch {} } listeners.clear(); for (const id of intervals) clearInterval(id); intervals.clear(); for (const id of timeouts) clearTimeout(id); timeouts.clear(); for (const id of animationFrames) cancelAnimationFrame(id); animationFrames.clear(); } catch (e) { logError('cleanupManager', 'cleanup failed', e); } }, observers, listeners, intervals, timeouts, animationFrames, }; })(); const delegateEvent = (parent, selector, eventType, handler, options = {}) => { const delegatedHandler = event => { const target = event.target.closest(selector); if (target && parent.contains(target)) { handler.call(target, event); } }; parent.addEventListener(eventType, delegatedHandler, options); return () => parent.removeEventListener(eventType, delegatedHandler, options); }; const batchDelegateEvents = (parent, config) => { const cleanupFns = []; for (const [selector, events] of Object.entries(config)) { for (const [eventType, handler] of Object.entries(events)) { cleanupFns.push(delegateEvent(parent, selector, eventType, handler)); } } return () => cleanupFns.forEach(fn => fn()); }; const createElement = (tag, props = {}, children = []) => { try { const element = document.createElement(tag); Object.entries(props).forEach(([k, v]) => { if (k === 'className') element.className = v; else if (k === 'style' && typeof v === 'object') Object.assign(element.style, v); else if (k === 'dataset' && typeof v === 'object') Object.assign(element.dataset, v); else if (k.startsWith('on') && typeof v === 'function') { element.addEventListener(k.slice(2), v); } else element.setAttribute(k, v); }); children.forEach(c => { if (typeof c === 'string') element.appendChild(document.createTextNode(c)); else if (c instanceof Node) element.appendChild(c); }); return element; } catch (e) { logError('createElement', 'failed', e); return document.createElement('div'); } }; const validateWaitForElementParams = selector => { if (!selector || typeof selector !== 'string') { return new Error('Invalid selector'); } return null; }; const tryFindElement = (parent, selector) => { try { const el = parent.querySelector(selector); return { element: el, error: null }; } catch (e) { return { element: null, error: e }; } }; const setupElementObserver = (parent, selector, resolve) => { const obs = new MutationObserver(() => { const el = parent.querySelector(selector); if (el) { try { obs.disconnect(); } catch {} resolve(el); } }); return obs; }; const startObserving = (obs, parent) => { try { if ( parent && (parent instanceof Node || parent instanceof Document || parent instanceof DocumentFragment) ) { obs.observe(parent, { childList: true, subtree: true }); } else if (document.body) { obs.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener( 'DOMContentLoaded', () => { try { obs.observe(document.body, { childList: true, subtree: true }); } catch (observeError) { logError( 'waitForElement', 'Failed to observe document.body after DOMContentLoaded', observeError ); } }, { once: true } ); } } catch (observeError) { logError('waitForElement', 'observer.observe failed', observeError); } }; const setupElementTimeout = (obs, reject, timeout) => { const id = setTimeout(() => { try { obs.disconnect(); } catch {} reject(new Error('timeout')); }, timeout); cleanupManager.registerTimeout(id); return id; }; const waitForElement = (selector, timeout = 5000, parent = document.body) => new Promise((resolve, reject) => { const validationError = validateWaitForElementParams(selector); if (validationError) return reject(validationError); const { element, error } = tryFindElement(parent, selector); if (error) return reject(error); if (element) return resolve(element); const obs = setupElementObserver(parent, selector, resolve); startObserving(obs, parent); setupElementTimeout(obs, reject, timeout); }); const sanitizeHTML = html => { if (typeof html !== 'string') return ''; let sanitizedHtml = html; if (sanitizedHtml.length > 1000000) { console.warn('[YouTube+] HTML content too large, truncating'); sanitizedHtml = sanitizedHtml.substring(0, 1000000); } const map = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', }; return sanitizedHtml.replace(/[<>&"'\/`=]/g, char => map[char] || char); }; const isValidURL = url => { if (typeof url !== 'string') return false; if (url.length > 2048) return false; if (/^\s|\s$/.test(url)) return false; try { const parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol)) return false; return true; } catch { return false; } }; const retryWithBackoff = async (fn, maxRetries = 3, baseDelay = 1000) => { let lastError; for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error; if (i < maxRetries - 1) { const delay = baseDelay * Math.pow(2, i); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; }; const storage = { get(key, def = null) { if (typeof key !== 'string' || !/^[a-zA-Z0-9_\-\.]+$/.test(key)) { logError('storage', 'Invalid key format', new Error(`Invalid key: ${key}`)); return def; } try { const v = localStorage.getItem(key); if (v === null) return def; if (v.length > 5 * 1024 * 1024) { logError('storage', 'Stored value too large', new Error(`Key: ${key}`)); return def; } return JSON.parse(v); } catch (e) { logError('storage', 'Failed to parse stored value', e); return def; } }, set(key, val) { if (typeof key !== 'string' || !/^[a-zA-Z0-9_\-\.]+$/.test(key)) { logError('storage', 'Invalid key format', new Error(`Invalid key: ${key}`)); return false; } try { const serialized = JSON.stringify(val); if (serialized.length > 5 * 1024 * 1024) { logError('storage', 'Value too large to store', new Error(`Key: ${key}`)); return false; } localStorage.setItem(key, serialized); return true; } catch (e) { logError('storage', 'Failed to store value', e); return false; } }, remove(key) { try { localStorage.removeItem(key); } catch (e) { logError('storage', 'Failed to remove value', e); } }, clear() { try { localStorage.clear(); } catch (e) { logError('storage', 'Failed to clear storage', e); } }, has(key) { try { return localStorage.getItem(key) !== null; } catch { return false; } }, }; if (typeof window !== 'undefined') { (window).YouTubeUtils = (window).YouTubeUtils || {}; const U = (window).YouTubeUtils; U.logError = U.logError || logError; U.debounce = U.debounce || debounce; U.throttle = U.throttle || throttle; U.delegateEvent = U.delegateEvent || delegateEvent; U.batchDelegateEvents = U.batchDelegateEvents || batchDelegateEvents; U.StyleManager = U.StyleManager || StyleManager; U.cleanupManager = U.cleanupManager || cleanupManager; U.createElement = U.createElement || createElement; U.waitForElement = U.waitForElement || waitForElement; U.storage = U.storage || storage; U.sanitizeHTML = U.sanitizeHTML || sanitizeHTML; U.isValidURL = U.isValidURL || isValidURL; U.retryWithBackoff = U.retryWithBackoff || retryWithBackoff; } })(); (function () { 'use strict'; const DOMHelpers = { safeQuery(parent, selector) { try { if (!parent || typeof parent.querySelector !== 'function') return null; return parent.querySelector(selector); } catch (error) { console.warn('[YouTube+][DOMHelpers] Query failed:', selector, error); return null; } }, _scrollIntoViewIfNeeded(list, node) { try { if (!node || !list) return; const nrect = node.getBoundingClientRect(); const lrect = list.getBoundingClientRect(); if (nrect.top < lrect.top) node.scrollIntoView(true); else if (nrect.bottom > lrect.bottom) node.scrollIntoView(false); } catch {} }, _buildCustomSelectItem(root, list, opt, id) { const item = document.createElement('div'); let useId; if (typeof id === 'undefined') { root._idCounter = (root._idCounter || 0) + 1; useId = root._idCounter; } else { useId = id; } item.id = `ytp-plus-custom-opt-${Date.now()}-${useId}`; item.setAttribute('role', 'option'); item.setAttribute('aria-selected', 'false'); item.textContent = opt.text; item.dataset.value = String(opt.value); Object.assign(item.style, { padding: '8px 10px', cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.02)', color: 'inherit', }); item.addEventListener('click', e => { e.stopPropagation(); const idx = Array.prototype.indexOf.call(list.children, item); const selFn = root._selectIndexFn; if (typeof selFn === 'function') selFn(idx); }); item.addEventListener('mouseenter', () => { const idx = Array.prototype.indexOf.call(list.children, item); const hlFn = root._highlightFn; if (typeof hlFn === 'function') hlFn(idx); }); return item; }, _setCustomSelectOptions(root, list, options) { root._options = Array.isArray(options) ? options : []; list.innerHTML = ''; root._activeIndex = -1; for (let i = 0; i < root._options.length; i++) { const opt = root._options[i]; const item = DOMHelpers._buildCustomSelectItem(root, list, opt); list.appendChild(item); } if (root._options.length > 0) { root.value = root._options[0].value; } else { root._value = ''; const lbl = root.querySelector('.ytp-plus-custom-select-label'); if (lbl) lbl.textContent = ''; } }, _makeCustomSelectKeyHandler(root, list, openFn, closeFn, highlightFn, selectFn) { return function (e) { if (root._disabled) return; const { key } = e; const len = root._options.length; if (key === 'ArrowDown') { e.preventDefault(); if (list.style.display === 'none') { openFn(); highlightFn(0); } else { const next = Math.min(len - 1, Math.max(0, root._activeIndex + 1)); highlightFn(next); DOMHelpers._scrollIntoViewIfNeeded(list, list.children[next]); } } else if (key === 'ArrowUp') { e.preventDefault(); if (list.style.display === 'none') { openFn(); highlightFn(len - 1); } else { const prev = Math.max(0, root._activeIndex - 1); highlightFn(prev); DOMHelpers._scrollIntoViewIfNeeded(list, list.children[prev]); } } else if (key === 'Enter' || key === ' ') { e.preventDefault(); if (list.style.display === 'none') { openFn(); highlightFn(root._activeIndex >= 0 ? root._activeIndex : 0); } else if (root._activeIndex >= 0) { selectFn(root._activeIndex); } } else if (key === 'Home') { e.preventDefault(); highlightFn(0); DOMHelpers._scrollIntoViewIfNeeded(list, list.children[0]); } else if (key === 'End') { e.preventDefault(); highlightFn(len - 1); DOMHelpers._scrollIntoViewIfNeeded(list, list.children[len - 1]); } else if (key === 'Escape') { if (list.style.display !== 'none') { e.preventDefault(); closeFn(); } } }; }, createCustomSelect() { const root = document.createElement('div'); root.className = 'ytp-plus-custom-select'; root.tabIndex = 0; root.setAttribute('role', 'combobox'); root.setAttribute('aria-haspopup', 'listbox'); root.setAttribute('aria-expanded', 'false'); Object.assign(root.style, { position: 'relative', display: 'inline-block', width: 'auto', }); const display = document.createElement('div'); display.className = 'ytp-plus-custom-select-display'; display.tabIndex = -1; Object.assign(display.style, { padding: '6px 8px', borderRadius: '6px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', color: 'inherit', minWidth: '70px', display: 'inline-flex', alignItems: 'center', gap: '8px', cursor: 'pointer', }); const label = document.createElement('div'); label.className = 'ytp-plus-custom-select-label'; label.style.flex = '1'; label.style.overflow = 'hidden'; label.style.textOverflow = 'ellipsis'; label.style.whiteSpace = 'nowrap'; label.textContent = ''; const chevron = document.createElement('div'); chevron.textContent = '▾'; chevron.style.opacity = '0.8'; display.appendChild(label); display.appendChild(chevron); const list = document.createElement('div'); list.setAttribute('role', 'listbox'); list.className = 'ytp-plus-custom-select-list'; Object.assign(list.style, { position: 'absolute', left: '0', right: '0', top: 'calc(100% + 6px)', maxHeight: '220px', overflowY: 'auto', display: 'none', borderRadius: '8px', background: 'linear-gradient(rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.02))', border: '1px solid rgba(255,255,255,0.06)', backdropFilter: 'blur(8px)', boxShadow: 'rgba(0, 0, 0, 0.6) 0px 8px 30px', zIndex: 9999, }); root.appendChild(display); root.appendChild(list); root._options = []; root._value = ''; root._disabled = false; root._activeIndex = -1; root._idCounter = 0; const closeList = () => { list.style.display = 'none'; root.setAttribute('aria-expanded', 'false'); }; const openList = () => { if (root._disabled) return; list.style.display = ''; root.setAttribute('aria-expanded', 'true'); }; const highlightIndex = idx => { const items = Array.from(list.children); items.forEach((it, i) => { if (i === idx) { it.classList.add('active'); it.style.background = 'rgba(255,255,255,0.03)'; it.setAttribute('aria-selected', 'true'); root._activeIndex = i; root.setAttribute('aria-activedescendant', it.id || ''); } else { it.classList.remove('active'); it.style.background = 'transparent'; it.setAttribute('aria-selected', 'false'); } }); }; const selectIndex = idx => { const opt = root._options[idx]; if (!opt) return; root.value = opt.value; closeList(); root.dispatchEvent(new Event('change', { bubbles: true })); }; display.addEventListener('click', e => { if (root._disabled) return; e.stopPropagation(); if (list.style.display === 'none') openList(); else closeList(); }); document.addEventListener('click', e => { if (!root.contains(e.target)) closeList(); }); root._selectIndexFn = selectIndex; root._highlightFn = highlightIndex; root.addEventListener( 'keydown', DOMHelpers._makeCustomSelectKeyHandler( root, list, openList, closeList, highlightIndex, selectIndex ) ); root.setPlaceholder = text => { label.textContent = text || ''; root._options = []; list.innerHTML = ''; root._value = ''; root._activeIndex = -1; }; root.setOptions = options => DOMHelpers._setCustomSelectOptions(root, list, options); Object.defineProperty(root, 'value', { get() { return root._value; }, set(v) { root._value = String(v); const found = root._options.find(o => String(o.value) === String(root._value)); label.textContent = found ? found.text : ''; }, }); Object.defineProperty(root, 'disabled', { get() { return root._disabled; }, set(v) { root._disabled = !!v; root.style.pointerEvents = root._disabled ? 'none' : ''; root.style.opacity = root._disabled ? '0.5' : '1'; }, }); return root; }, safeQueryAll(parent, selector) { try { if (!parent || typeof parent.querySelectorAll !== 'function') return []; return Array.from(parent.querySelectorAll(selector)); } catch (error) { console.warn('[YouTube+][DOMHelpers] QueryAll failed:', selector, error); return []; } }, safeGetAttribute(element, attr, defaultValue = null) { try { if (!element || typeof element.getAttribute !== 'function') return defaultValue; const value = element.getAttribute(attr); return value === null ? defaultValue : value; } catch { return defaultValue; } }, safeSetAttribute(element, attr, value) { try { if (!element || typeof element.setAttribute !== 'function') return false; if (!attr || typeof attr !== 'string') return false; element.setAttribute(attr, String(value)); return true; } catch (error) { console.warn('[YouTube+][DOMHelpers] setAttribute failed:', attr, error); return false; } }, matches(element, selector) { try { if (!element || typeof element.matches !== 'function') return false; return element.matches(selector); } catch { return false; } }, closest(element, selector) { try { if (!element || typeof element.closest !== 'function') return null; return element.closest(selector); } catch { return null; } }, }; const NestingHelpers = { earlyReturn(condition, callback) { if (!condition) return false; if (typeof callback === 'function') callback(); return true; }, guardClauses(guards) { for (const guard of guards) { if (!guard.condition) { if (guard.message) { console.warn('[YouTube+] Guard clause failed:', guard.message); } return false; } } return true; }, extractLogic(fn, context = null, ...args) { if (typeof fn !== 'function') return null; try { return fn.apply(context, args); } catch (error) { console.error('[YouTube+][NestingHelpers] Logic extraction failed:', error); return null; } }, processItems(items, processFn, options = {}) { const { continueOnError = true, filterFn = null, maxItems = Infinity } = options; if (!Array.isArray(items)) return []; if (typeof processFn !== 'function') return items; const results = []; let processedCount = 0; for (const item of items) { if (processedCount >= maxItems) break; if (filterFn && !filterFn(item)) continue; try { const result = processFn(item); if (result !== undefined) { results.push(result); processedCount++; } } catch (error) { if (!continueOnError) throw error; console.warn('[YouTube+][NestingHelpers] Item processing failed:', error); } } return results; }, }; const AsyncHelpers = { async retry(asyncFn, options = {}) { const { maxRetries = 3, initialDelay = 1000, maxDelay = 10000, backoffMultiplier = 2, onRetry = null, } = options; let lastError; let delay = initialDelay; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await asyncFn(); } catch (error) { lastError = error; if (attempt === maxRetries) break; if (typeof onRetry === 'function') { onRetry(attempt + 1, error, delay); } await new Promise(resolve => setTimeout(resolve, delay)); delay = Math.min(delay * backoffMultiplier, maxDelay); } } throw lastError; }, async withTimeout(promise, timeoutMs, errorMessage = 'Operation timed out') { let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error(errorMessage)), timeoutMs); }); try { const result = await Promise.race([promise, timeoutPromise]); clearTimeout(timeoutId); return result; } catch (error) { clearTimeout(timeoutId); throw error; } }, debounceAsync(asyncFn, delay) { let timeoutId = null; let latestPromise = null; return async function (...args) { if (timeoutId) clearTimeout(timeoutId); latestPromise = new Promise((resolve, reject) => { timeoutId = setTimeout(async () => { try { const result = await asyncFn.apply(this, args); resolve(result); } catch (error) { reject(error); } }, delay); }); return latestPromise; }; }, }; const ConditionHelpers = { all(...conditions) { return conditions.every(c => c); }, any(...conditions) { return conditions.some(c => c); }, none(...conditions) { return conditions.every(c => !c); }, ifThenElse(condition, thenFn, elseFn = null) { if (condition) { return typeof thenFn === 'function' ? thenFn() : undefined; } return typeof elseFn === 'function' ? elseFn() : undefined; }, }; const StateHelpers = { createState(initialState = {}) { let state = { ...initialState }; const listeners = new Set(); return { get(key) { return key ? state[key] : { ...state }; }, set(key, value) { const oldValue = state[key]; state[key] = value; this.notify(key, value, oldValue); }, update(partial) { const oldState = { ...state }; state = { ...state, ...partial }; this.notify('*', state, oldState); }, subscribe(listener) { if (typeof listener === 'function') { listeners.add(listener); } return () => listeners.delete(listener); }, notify(key, newValue, oldValue) { for (const listener of listeners) { try { listener(key, newValue, oldValue); } catch (error) { console.error('[YouTube+][StateHelpers] Listener error:', error); } } }, reset() { state = { ...initialState }; this.notify('*', state, {}); }, }; }, }; if (typeof window !== 'undefined') { window.YouTubePlusHelpers = { DOM: DOMHelpers, Nesting: NestingHelpers, Async: AsyncHelpers, Condition: ConditionHelpers, State: StateHelpers, }; console.log('[YouTube+] Helper utilities loaded'); } })(); (function () { 'use strict'; const CircuitState = { CLOSED: 'closed', OPEN: 'open', HALF_OPEN: 'half_open', }; const ErrorBoundaryConfig = { maxErrors: 10, errorWindow: 60000, enableLogging: true, enableRecovery: true, storageKey: 'youtube_plus_errors', circuitBreaker: { enabled: true, failureThreshold: 5, resetTimeout: 30000, halfOpenAttempts: 3, }, }; const errorState = { errors: [], errorCount: 0, lastErrorTime: 0, isRecovering: false, circuitState: CircuitState.CLOSED, circuitFailureCount: 0, circuitLastFailureTime: 0, circuitSuccessCount: 0, }; const ErrorSeverity = { LOW: 'low', MEDIUM: 'medium', HIGH: 'high', CRITICAL: 'critical', }; const categorizeSeverity = error => { const message = error.message?.toLowerCase() || ''; if ( message.includes('cannot read') || message.includes('undefined') || message.includes('null') ) { return ErrorSeverity.MEDIUM; } if (message.includes('network') || message.includes('fetch') || message.includes('timeout')) { return ErrorSeverity.LOW; } if (message.includes('syntax') || message.includes('reference') || message.includes('type')) { return ErrorSeverity.HIGH; } if (message.includes('security') || message.includes('csp')) { return ErrorSeverity.CRITICAL; } return ErrorSeverity.MEDIUM; }; const checkCircuitBreaker = success => { if (!ErrorBoundaryConfig.circuitBreaker.enabled) return true; const now = Date.now(); const { circuitBreaker } = ErrorBoundaryConfig; if ( errorState.circuitState === CircuitState.OPEN && now - errorState.circuitLastFailureTime >= circuitBreaker.resetTimeout ) { console.log('[YouTube+] Circuit breaker transitioning to HALF_OPEN'); errorState.circuitState = CircuitState.HALF_OPEN; errorState.circuitSuccessCount = 0; } if (success) { if (errorState.circuitState === CircuitState.HALF_OPEN) { errorState.circuitSuccessCount++; if (errorState.circuitSuccessCount >= circuitBreaker.halfOpenAttempts) { console.log('[YouTube+] Circuit breaker CLOSED - system recovered'); errorState.circuitState = CircuitState.CLOSED; errorState.circuitFailureCount = 0; errorState.circuitSuccessCount = 0; } } else if (errorState.circuitState === CircuitState.CLOSED) { errorState.circuitFailureCount = Math.max(0, errorState.circuitFailureCount - 1); } return true; } errorState.circuitFailureCount++; errorState.circuitLastFailureTime = now; if (errorState.circuitState === CircuitState.CLOSED) { if (errorState.circuitFailureCount >= circuitBreaker.failureThreshold) { console.error('[YouTube+] Circuit breaker OPEN - too many failures'); errorState.circuitState = CircuitState.OPEN; return false; } } else if (errorState.circuitState === CircuitState.HALF_OPEN) { console.error('[YouTube+] Circuit breaker reopened - recovery failed'); errorState.circuitState = CircuitState.OPEN; errorState.circuitSuccessCount = 0; return false; } return errorState.circuitState !== CircuitState.OPEN; }; const logError = (error, context = {}) => { if (!ErrorBoundaryConfig.enableLogging) return; checkCircuitBreaker(false); const fallbackMessage = error.message?.trim() || ''; if (!fallbackMessage || fallbackMessage === '(no message)') { if (!error.stack && !context.filename) { return; } } const displayMessage = fallbackMessage || (context.filename ? `Error in ${context.filename}:${context.lineno}` : 'Unknown error'); const errorInfo = { timestamp: new Date().toISOString(), message: displayMessage, stack: error.stack, severity: categorizeSeverity(error), context: { url: window.location.href, userAgent: navigator.userAgent, ...context, }, }; console.error('[YouTube+][Error Boundary]', `${errorInfo.message}`, errorInfo); errorState.errors.push(errorInfo); if (errorState.errors.length > 50) { errorState.errors.shift(); } try { const stored = JSON.parse(localStorage.getItem(ErrorBoundaryConfig.storageKey) || '[]'); stored.push(errorInfo); if (stored.length > 20) stored.shift(); localStorage.setItem(ErrorBoundaryConfig.storageKey, JSON.stringify(stored)); } catch {} }; const isErrorRateExceeded = () => { const now = Date.now(); const windowStart = now - ErrorBoundaryConfig.errorWindow; const recentErrors = errorState.errors.filter( e => new Date(e.timestamp).getTime() > windowStart ); return recentErrors.length >= ErrorBoundaryConfig.maxErrors; }; const getErrorRate = () => { const now = Date.now(); const oneMinuteAgo = now - 60000; const recentErrors = errorState.errors.filter( e => new Date(e.timestamp).getTime() > oneMinuteAgo ); return recentErrors.length; }; const shouldSuppressNotification = error => { const rate = getErrorRate(); if (rate > 5) { return true; } const tenSecondsAgo = Date.now() - 10000; const recentSimilar = errorState.errors.filter( e => new Date(e.timestamp).getTime() > tenSecondsAgo && e.message === error.message && e.severity === categorizeSeverity(error) ); return recentSimilar.length > 0; }; const showErrorNotification = (error, _context) => { try { const Y = window.YouTubeUtils; if (!Y || !Y.NotificationManager || typeof Y.NotificationManager.show !== 'function') { return; } const severity = categorizeSeverity(error); let message = 'An error occurred'; let duration = 3000; switch (severity) { case ErrorSeverity.LOW: message = 'A minor issue occurred. Functionality should continue normally.'; duration = 2000; break; case ErrorSeverity.MEDIUM: message = 'An error occurred. Some features may not work correctly.'; duration = 3000; break; case ErrorSeverity.HIGH: message = 'A serious error occurred. Please refresh the page if issues persist.'; duration = 5000; break; case ErrorSeverity.CRITICAL: message = 'A critical error occurred. YouTube+ may not function properly. Please report this issue.'; duration = 7000; break; } Y.NotificationManager.show(message, { duration, type: 'error' }); } catch (notificationError) { console.error('[YouTube+] Failed to show error notification:', notificationError); } }; const attemptRecovery = (error, context) => { if (!ErrorBoundaryConfig.enableRecovery || errorState.isRecovering) return; const severity = categorizeSeverity(error); if (severity === ErrorSeverity.CRITICAL) { console.error('[YouTube+] Critical error detected. Script may not function properly.'); showErrorNotification(error, context); return; } errorState.isRecovering = true; try { if (severity !== ErrorSeverity.LOW && !shouldSuppressNotification(error)) { showErrorNotification(error, context); } const RecoveryUtils = window.YouTubePlusErrorRecovery; if (RecoveryUtils && RecoveryUtils.attemptRecovery) { RecoveryUtils.attemptRecovery(error, context); } else { performLegacyRecovery(error, context); } setTimeout(() => { errorState.isRecovering = false; }, 5000); } catch (recoveryError) { console.error('[YouTube+] Recovery attempt failed:', recoveryError); errorState.isRecovering = false; } }; const performLegacyRecovery = (error, context) => { if (context.module) { console.log(`[YouTube+] Attempting recovery for module: ${context.module}`); const Y = window.YouTubeUtils; if (Y && Y.cleanupManager) { switch (context.module) { case 'StyleManager': break; case 'NotificationManager': break; default: break; } } if ( error.message && (error.message.includes('null') || error.message.includes('undefined')) && context.element ) { console.log('[YouTube+] Attempting to re-query DOM element'); } } }; const handleError = event => { const error = event.error || new Error(event.message); const message = (error.message || event.message || '').trim(); const source = event.filename || ''; const isCrossOriginSource = source && !source.startsWith(window.location.origin) && !/YouTube\+/.test(source); if (!message && isCrossOriginSource) { return false; } if (!message || (message === '(no message)' && isCrossOriginSource)) { return false; } errorState.errorCount++; errorState.lastErrorTime = Date.now(); logError(error, { type: 'uncaught', filename: event.filename, lineno: event.lineno, colno: event.colno, }); if (isErrorRateExceeded()) { console.error( '[YouTube+] Error rate exceeded! Too many errors in short period. Some features may be disabled.' ); return false; } attemptRecovery(error, { type: 'uncaught' }); return false; }; const handleUnhandledRejection = event => { const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason)); logError(error, { type: 'unhandledRejection', promise: event.promise, }); if (isErrorRateExceeded()) { console.error('[YouTube+] Promise rejection rate exceeded!'); return; } attemptRecovery(error, { type: 'unhandledRejection' }); }; const withErrorBoundary = (fn, context = 'unknown') => { return function (...args) { try { const fnAny = (fn); return fnAny.call(this, ...args); } catch (error) { logError(error, { module: context, args }); attemptRecovery(error, { module: context }); return null; } }; }; const withAsyncErrorBoundary = (fn, context = 'unknown') => { return async function (...args) { try { const fnAny = (fn); return await fnAny.call(this, ...args); } catch (error) { logError(error, { module: context, args }); attemptRecovery(error, { module: context }); return null; } }; }; const getErrorStats = () => { return { totalErrors: errorState.errorCount, recentErrors: errorState.errors.length, lastErrorTime: errorState.lastErrorTime, isRecovering: errorState.isRecovering, errorsByType: errorState.errors.reduce((acc, e) => { acc[e.severity] = (acc[e.severity] || 0) + 1; return acc; }, {}), }; }; const clearErrors = () => { errorState.errors = []; try { localStorage.removeItem(ErrorBoundaryConfig.storageKey); } catch {} }; if (typeof window !== 'undefined') { window.addEventListener('error', handleError, true); window.addEventListener('unhandledrejection', handleUnhandledRejection, true); window.YouTubeErrorBoundary = { withErrorBoundary, withAsyncErrorBoundary, getErrorStats, clearErrors, logError, getErrorRate, config: ErrorBoundaryConfig, }; console.log('[YouTube+][Error Boundary]', 'Error boundary initialized'); } })(); (function () { 'use strict'; const PerformanceConfig = { enabled: true, sampleRate: 0.01, storageKey: 'youtube_plus_performance', metricsRetention: 30, enableConsoleOutput: false, budgets: { initialization: 100, domManipulation: 50, apiCall: 500, rendering: 16, }, lazyLoadThreshold: 5000, maxOverhead: 30, }; const metrics = { timings: new Map(), marks: new Map(), measures: [], resources: [], }; let initialized = false; const initTime = Date.now(); const shouldSample = () => { if (!PerformanceConfig.enabled) return false; if (PerformanceConfig.sampleRate >= 1.0) return true; return Math.random() < PerformanceConfig.sampleRate; }; const isLazyLoadComplete = () => { if (!initialized && Date.now() - initTime >= PerformanceConfig.lazyLoadThreshold) { initialized = true; } return initialized; }; const mark = name => { if (!PerformanceConfig.enabled || !shouldSample()) return; if (!isLazyLoadComplete()) return; try { if (typeof performance !== 'undefined' && performance.mark) { performance.mark(name); } metrics.marks.set(name, Date.now()); } catch (e) { console.warn('[YouTube+][Performance]', 'Failed to create mark:', e); } }; const canMeasure = () => { if (!PerformanceConfig.enabled || !shouldSample()) return false; if (!isLazyLoadComplete()) return false; return true; }; const calculateDuration = (startMark, endMark) => { const startTime = metrics.marks.get(startMark); if (!startTime) { console.warn('[YouTube+][Performance]', `Start mark "${startMark}" not found`); return null; } const endTime = endMark ? metrics.marks.get(endMark) : Date.now(); const duration = endTime - startTime; return { duration, endTime }; }; const storeMeasurement = (name, startMark, endMark, duration) => { const measureData = { name, startMark, endMark: endMark || 'now', duration, timestamp: Date.now(), }; metrics.measures.push(measureData); if (metrics.measures.length > PerformanceConfig.metricsRetention) { metrics.measures.shift(); } }; const checkBudgetExceeded = (name, duration) => { for (const [category, budget] of Object.entries(PerformanceConfig.budgets)) { if (name.toLowerCase().includes(category.toLowerCase()) && duration > budget) { console.warn( '[YouTube+][Performance]', `⚠️ Budget exceeded: ${name} took ${duration.toFixed(2)}ms (budget: ${budget}ms)` ); return true; } } return false; }; const logMeasurement = (name, duration, budgetExceeded) => { if (PerformanceConfig.enableConsoleOutput || budgetExceeded) { const status = budgetExceeded ? '⚠️' : '✓'; console.log('[YouTube+][Performance]', `${status} ${name}: ${duration.toFixed(2)}ms`); } }; const tryNativePerformanceAPI = (name, startMark, endMark) => { if (typeof performance !== 'undefined' && performance.measure) { try { performance.measure(name, startMark, endMark); } catch { } } }; const measure = (name, startMark, endMark) => { if (!canMeasure()) return 0; try { const result = calculateDuration(startMark, endMark); if (!result) return 0; const { duration } = result; storeMeasurement(name, startMark, endMark, duration); const budgetExceeded = checkBudgetExceeded(name, duration); logMeasurement(name, duration, budgetExceeded); tryNativePerformanceAPI(name, startMark, endMark); return duration; } catch (e) { console.warn('[YouTube+][Performance]', 'Failed to measure:', e); return 0; } }; const timeFunction = (name, fn) => { if (!PerformanceConfig.enabled) return fn; return function (...args) { if (!shouldSample() || !isLazyLoadComplete()) { const fnAny = (fn); return fnAny.call(this, ...args); } const startMark = `${name}-start-${Date.now()}`; mark(startMark); try { const fnAny = (fn); const result = fnAny.call(this, ...args); if (result && typeof result.then === 'function') { return result.finally(() => { measure(name, startMark, undefined); }); } measure(name, startMark, undefined); return result; } catch (error) { measure(name, startMark, undefined); throw error; } }; }; const timeAsyncFunction = (name, fn) => { if (!PerformanceConfig.enabled) return fn; return async function (...args) { if (!shouldSample() || !isLazyLoadComplete()) { const fnAny = (fn); return await fnAny.call(this, ...args); } const startMark = `${name}-start-${Date.now()}`; mark(startMark); try { const fnAny = (fn); const result = await fnAny.call(this, ...args); measure(name, startMark, undefined); return result; } catch (error) { measure(name, startMark, undefined); throw error; } }; }; const recordMetric = (name, value, metadata = {}) => { if (!PerformanceConfig.enabled) return; const metric = { name, value, timestamp: Date.now(), ...metadata, }; metrics.timings.set(name, metric); if (PerformanceConfig.enableConsoleOutput) { console.log('[YouTube+][Performance]', `${name}: ${value}`, metadata); } }; const getStats = metricName => { if (metricName) { const filtered = metrics.measures.filter(m => m.name === metricName); if (filtered.length === 0) return null; const durations = filtered.map(m => m.duration); return { name: metricName, count: durations.length, min: Math.min(...durations), max: Math.max(...durations), avg: durations.reduce((a, b) => a + b, 0) / durations.length, latest: durations[durations.length - 1], }; } const allMetrics = {}; const metricNames = [...new Set(metrics.measures.map(m => m.name))]; metricNames.forEach(name => { allMetrics[name] = getStats(name); }); return { metrics: allMetrics, totalMeasures: metrics.measures.length, totalMarks: metrics.marks.size, customMetrics: Object.fromEntries(metrics.timings), }; }; const getMemoryUsage = () => { if (typeof performance === 'undefined' || !performance.memory) { return null; } try { const { memory } = performance; return { usedJSHeapSize: memory.usedJSHeapSize, totalJSHeapSize: memory.totalJSHeapSize, jsHeapSizeLimit: memory.jsHeapSizeLimit, usedPercent: ((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100).toFixed(2), }; } catch { return null; } }; const trackMemory = () => { const memory = getMemoryUsage(); if (memory) { recordMetric('memory-usage', memory.usedJSHeapSize, { totalJSHeapSize: memory.totalJSHeapSize, usedPercent: memory.usedPercent, }); } }; const checkThresholds = thresholds => { const violations = []; const allStats = getStats(undefined); if (!allStats || !allStats.metrics) return violations; Object.entries(thresholds).forEach(([metricName, threshold]) => { const stat = allStats.metrics[metricName]; if (stat && stat.avg > threshold) { violations.push({ metric: metricName, threshold, actual: stat.avg, exceeded: stat.avg - threshold, }); } }); return violations; }; const exportMetrics = () => { const data = { timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href, memory: getMemoryUsage(), stats: getStats(undefined), measures: metrics.measures, customMetrics: Object.fromEntries(metrics.timings), }; return JSON.stringify(data, null, 2); }; const exportToFile = (filename = 'youtube-plus-performance.json') => { try { const data = exportMetrics(); if (typeof Blob === 'undefined') { console.warn('[YouTube+][Performance]', 'Blob API not available'); return false; } const blob = new Blob([data], { 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); return true; } catch (e) { console.error('[YouTube+][Performance]', 'Failed to export to file:', e); return false; } }; const aggregateByPeriod = (periodMs = 60000) => { const periods = new Map(); metrics.measures.forEach(measureItem => { const periodStart = Math.floor(measureItem.timestamp / periodMs) * periodMs; if (!periods.has(periodStart)) { periods.set(periodStart, []); } periods.get(periodStart).push(measureItem); }); const aggregated = []; periods.forEach((measures, periodStart) => { const durations = measures.map(m => m.duration); aggregated.push({ period: new Date(periodStart).toISOString(), count: durations.length, min: Math.min(...durations), max: Math.max(...durations), avg: durations.reduce((a, b) => a + b, 0) / durations.length, }); }); return aggregated; }; const clearMetrics = () => { metrics.timings.clear(); metrics.marks.clear(); metrics.measures = []; metrics.resources = []; try { localStorage.removeItem(PerformanceConfig.storageKey); } catch {} if (typeof performance !== 'undefined' && performance.clearMarks) { try { performance.clearMarks(); performance.clearMeasures(); } catch {} } }; const monitorMutations = (element, name) => { if (!PerformanceConfig.enabled) return null; let mutationCount = 0; const startTime = Date.now(); const observer = new MutationObserver(mutations => { mutationCount += mutations.length; recordMetric(`${name}-mutations`, mutationCount, { elapsed: Date.now() - startTime, }); }); observer.observe(element, { childList: true, subtree: true, attributes: true, }); return observer; }; const getPerformanceEntries = type => { if (typeof performance === 'undefined' || !performance.getEntriesByType) { return []; } try { return performance.getEntriesByType(type); } catch { return []; } }; const logPageLoadMetrics = () => { if (!PerformanceConfig.enabled) return; try { const navigation = getPerformanceEntries('navigation')[0]; if (navigation) { recordMetric('page-load-time', navigation.loadEventEnd - navigation.fetchStart); recordMetric('dom-content-loaded', navigation.domContentLoadedEventEnd); recordMetric('dom-interactive', navigation.domInteractive); } } catch (e) { console.warn('[YouTube+][Performance]', 'Failed to log page metrics:', e); } }; if (typeof window !== 'undefined') { if (document.readyState === 'complete') { logPageLoadMetrics(); } else { window.addEventListener('load', logPageLoadMetrics, { once: true }); } window.YouTubePerformance = { mark, measure, timeFunction, timeAsyncFunction, recordMetric, getStats, exportMetrics, exportToFile, clearMetrics, monitorMutations, getPerformanceEntries, getMemoryUsage, trackMemory, checkThresholds, aggregateByPeriod, config: PerformanceConfig, }; console.log('[YouTube+][Performance]', 'Performance monitoring initialized'); } })(); (function () { 'use strict'; const blockAdRequests = () => { const adDomains = [ 'doubleclick.net', 'googleads.g.doubleclick.net', 'googlesyndication.com', 'googleadservices.com', 'google-analytics.com', 'google.com/pagead', 'ad.doubleclick.net', 'www.google.com/pagead', 'www.google.com.tr/pagead', 'static.doubleclick.net', ]; const isAdRequest = url => { const urlString = String(url); return adDomains.some(domain => urlString.includes(domain)); }; if (typeof XMLHttpRequest !== 'undefined') { const originalXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...args) { if (isAdRequest(url)) { console.debug('[YT+][Perf] Blocked XHR:', String(url).substring(0, 60)); this.status = 200; this.readyState = 4; return undefined; } return originalXHROpen.call(this, method, url, ...args); }; } if (typeof fetch !== 'undefined' && typeof window !== 'undefined') { const originalFetch = window.fetch; window.fetch = function (url, ...args) { if (isAdRequest(url)) { console.debug('[YT+][Perf] Blocked fetch:', String(url).substring(0, 60)); return Promise.resolve( new Response('', { status: 200, statusText: 'OK', headers: new Headers(), }) ); } return originalFetch.call(this, url, ...args); }; } if (typeof Image !== 'undefined' && typeof HTMLImageElement !== 'undefined') { const OriginalImage = Image; const ImageProxy = new Proxy(OriginalImage, { construct(target, args) { const img = new target(...args); const descriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src'); if (descriptor && descriptor.set) { const originalSrcSetter = descriptor.set; Object.defineProperty(img, 'src', { set(value) { if (isAdRequest(value)) { console.debug('[YT+][Perf] Blocked image:', String(value).substring(0, 60)); originalSrcSetter.call( this, 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' ); return; } originalSrcSetter.call(this, value); }, get() { return this.getAttribute('src'); }, }); } return img; }, }); window.Image = ImageProxy; } }; const optimizeServiceWorker = () => { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready .then(registration => { if (registration.navigationPreload) { registration.navigationPreload.disable().catch(() => { }); } }) .catch(() => { }); } }; const optimizeImages = () => { const imageObserver = new IntersectionObserver( (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute('data-src'); } observer.unobserve(img); } }); }, { rootMargin: '50px 0px', threshold: 0.01, } ); const observeImages = () => { document.querySelectorAll('img[loading="lazy"]').forEach(img => { imageObserver.observe(img); }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', observeImages); } else { observeImages(); } document.addEventListener('yt-navigate-finish', observeImages); }; const prefetchCriticalResources = () => { console.debug('[YT+][Perf] Prefetch disabled (CORS restrictions)'); }; const optimizeLayoutCalculations = () => { let scheduledReads = []; let scheduledWrites = []; let rafScheduled = false; const processScheduled = () => { scheduledReads.forEach(fn => fn()); scheduledReads = []; scheduledWrites.forEach(fn => fn()); scheduledWrites = []; rafScheduled = false; }; window.scheduleRead = fn => { scheduledReads.push(fn); if (!rafScheduled) { rafScheduled = true; requestAnimationFrame(processScheduled); } }; window.scheduleWrite = fn => { scheduledWrites.push(fn); if (!rafScheduled) { rafScheduled = true; requestAnimationFrame(processScheduled); } }; }; const optimizeFonts = () => { if ('fonts' in document && document.head) { const style = document.createElement('style'); style.textContent = ` @font-face { font-family: 'Roboto'; font-display: swap; } @font-face { font-family: 'YouTube Sans'; font-display: swap; } `; document.head.appendChild(style); } }; const deferNonCriticalJS = () => { const scripts = document.querySelectorAll('script[src]'); scripts.forEach(script => { if (!script.hasAttribute('async') && !script.hasAttribute('defer')) { script.setAttribute('defer', ''); } }); }; const optimizeCSS = () => { if (!document.head) { console.debug('[YT+][Perf] document.head not ready, deferring CSS optimization'); return; } const style = document.createElement('style'); style.textContent = ` .html5-video-player, ytd-app, #movie_player { transform: translateZ(0); will-change: transform; } * { -webkit-tap-highlight-color: transparent; } img, video { image-rendering: -webkit-optimize-contrast; } `; document.head.appendChild(style); }; const deferThirdPartyScripts = () => { const blockedScripts = [ 'googletagmanager.com', 'google-analytics.com', 'analytics.js', 'gtag/js', ]; const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.tagName === 'SCRIPT' && node.src) { const shouldBlock = blockedScripts.some(blocked => node.src.includes(blocked)); if (shouldBlock) { console.debug('[YT+][Perf] Blocked third-party script:', node.src); node.remove(); } } }); }); }); observer.observe(document.documentElement, { childList: true, subtree: true, }); }; const optimizeMemory = () => { try { const keysToCheck = Object.keys(localStorage); const oldDataKeys = keysToCheck.filter(key => { try { const data = localStorage.getItem(key); if (data && data.includes('"timestamp"')) { const parsed = JSON.parse(data); const age = Date.now() - (parsed.timestamp || 0); return age > 7 * 24 * 60 * 60 * 1000; } } catch { return false; } return false; }); oldDataKeys.forEach(key => localStorage.removeItem(key)); if (oldDataKeys.length > 0) { console.log(`[YT+][Perf] Cleaned ${oldDataKeys.length} old localStorage entries`); } } catch (e) { console.debug('[YT+][Perf] Could not clean localStorage:', e); } }; const init = () => { console.log('[YT+][Boost] Applying performance optimizations...'); try { blockAdRequests(); optimizeServiceWorker(); optimizeLayoutCalculations(); const applyDOMOptimizations = () => { if (document.head) { optimizeCSS(); optimizeFonts(); } else { console.debug('[YT+][Perf] Waiting for document.head...'); if (document.readyState === 'loading') { document.addEventListener( 'DOMContentLoaded', () => { optimizeCSS(); optimizeFonts(); }, { once: true } ); } else { setTimeout(() => { optimizeCSS(); optimizeFonts(); }, 50); } } }; applyDOMOptimizations(); setTimeout(() => { optimizeImages(); deferThirdPartyScripts(); }, 100); setTimeout(() => { prefetchCriticalResources(); deferNonCriticalJS(); optimizeMemory(); }, 1000); console.log('[YT+][Boost] Performance optimizations applied'); } catch (error) { console.error('[YT+][Boost] Error applying optimizations:', error); } }; if (typeof window !== 'undefined') { window.YouTubePerformanceBoost = { init, version: '2.2', }; } init(); })(); window.YouTubeModuleLoader = (() => { 'use strict'; const loadedModules = new Map(); const loadingPromises = new Map(); const moduleConfigs = { stats: { loaded: false, priority: 'low', loadOnIdle: false, globalName: 'YouTubeStats', }, download: { loaded: false, priority: 'low', loadOnIdle: false, globalName: 'YouTubeDownload', }, comment: { loaded: false, priority: 'medium', loadOnDemand: false, globalName: 'YouTubeComments', }, music: { loaded: false, priority: 'medium', loadOnDemand: false, globalName: 'YouTubeMusic', }, playlist: { loaded: false, priority: 'low', loadOnDemand: false, globalName: 'YouTubePlaylistSearch', }, }; const isModuleLoaded = moduleName => { return moduleConfigs[moduleName]?.loaded || false; }; const loadModule = async moduleName => { if (isModuleLoaded(moduleName)) { return loadedModules.get(moduleName); } if (loadingPromises.has(moduleName)) { return loadingPromises.get(moduleName); } const loadPromise = (async () => { try { const startTime = performance.now(); const config = moduleConfigs[moduleName]; if (!config) { throw new Error(`Unknown module: ${moduleName}`); } const module = window[config.globalName]; if (!module) { console.warn(`[YT+][Loader] Module ${moduleName} not yet initialized, will retry...`); return null; } loadedModules.set(moduleName, module); moduleConfigs[moduleName].loaded = true; const loadTime = performance.now() - startTime; console.log(`[YT+][Loader] Module ${moduleName} ready in ${loadTime.toFixed(2)}ms`); return module; } catch (error) { console.error(`[YT+][Loader] Failed to reference module ${moduleName}:`, error); loadingPromises.delete(moduleName); throw error; } })(); loadingPromises.set(moduleName, loadPromise); return loadPromise; }; const loadModules = async moduleNames => { const promises = moduleNames.map(name => loadModule(name)); return Promise.all(promises); }; const preloadModules = moduleNames => { const callback = () => { moduleNames.forEach(moduleName => { if (!isModuleLoaded(moduleName)) { loadModule(moduleName).catch(err => { if (err) console.debug(`[YT+][Loader] Module ${moduleName} not ready yet`); }); } }); }; if ('requestIdleCallback' in window) { requestIdleCallback(callback, { timeout: 3000 }); } else { setTimeout(callback, 1000); } }; const autoPreload = () => { const allModules = Object.keys(moduleConfigs); preloadModules(allModules); }; const init = () => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoPreload); } else { setTimeout(autoPreload, 100); } }; init(); return { loadModule, loadModules, preloadModules, isModuleLoaded, getLoadedModules: () => Array.from(loadedModules.keys()), getModuleConfigs: () => ({ ...moduleConfigs }), }; })(); const identityFn = value => value; function ensureTrustedTypesPolicy() { if (typeof trustedTypes === 'undefined') { return { createHTML: identityFn, error: null }; } try { if (trustedTypes.defaultPolicy === null) { trustedTypes.createPolicy('default', { createHTML: identityFn, createScriptURL: identityFn, createScript: identityFn, }); } const policy = trustedTypes.defaultPolicy; const createHTML = policy && typeof policy.createHTML === 'function' ? policy.createHTML.bind(policy) : identityFn; const testDiv = document.createElement('div'); testDiv.innerHTML = createHTML('1'); return { createHTML, error: null }; } catch (error) { console.error('TrustedTypes policy creation failed:', error); return { createHTML: identityFn, error }; } } function createNextBrowserTick(existing) { if (existing && typeof existing === 'function' && existing.version >= 2) { return existing; } const SafePromise = (async () => {})().constructor; const queue = typeof queueMicrotask === 'function' ? callback => queueMicrotask(callback) : callback => SafePromise.resolve().then(callback); const scheduler = callback => { if (typeof callback === 'function') { queue(callback); return; } return SafePromise.resolve(); }; scheduler.version = 2; return scheduler; } const { createHTML, error: trustHTMLErr } = ensureTrustedTypesPolicy(); if (trustHTMLErr) { console.error( '[YouTube+] TrustedHTML Error: Script cannot run due to Content Security Policy restrictions', trustHTMLErr ); throw new Error('CSP restriction - cannot initialize TrustedTypes'); } if (typeof window !== 'undefined') { window._ytplusCreateHTML = createHTML; } const nextBrowserTick = createNextBrowserTick( (typeof window !== 'undefined' && window.nextBrowserTick) || undefined ); if ( typeof window !== 'undefined' && (!window.nextBrowserTick || window.nextBrowserTick.version < 2) ) { window.nextBrowserTick = nextBrowserTick; } const executionScript = _communicationKey => { const DEBUG_5084 = false; const DEBUG_5085 = true; const DEBUG_handleNavigateFactory = false; const TAB_AUTO_SWITCH_TO_COMMENTS = false; const MAX_ATTRIBUTE_VALUE = 1e9; const ATTRIBUTE_RESET_VALUE = 9; if ( MAX_ATTRIBUTE_VALUE <= 0 || ATTRIBUTE_RESET_VALUE < 0 || ATTRIBUTE_RESET_VALUE >= MAX_ATTRIBUTE_VALUE ) { console.error( '[YouTube+] Invalid configuration: MAX_ATTRIBUTE_VALUE and ATTRIBUTE_RESET_VALUE must be valid positive numbers' ); } const identityFnLocal = value => value; const ensureTrustedTypesPolicyLocal = () => { if (typeof trustedTypes === 'undefined') { return { createHTML: identityFnLocal, error: null }; } try { if (trustedTypes.defaultPolicy === null) { trustedTypes.createPolicy('default', { createHTML: identityFnLocal, createScriptURL: identityFnLocal, createScript: identityFnLocal, }); } const policy = trustedTypes.defaultPolicy; const createHTMLLocal = policy?.createHTML?.bind?.(policy) ?? identityFnLocal; const testDiv = document.createElement('div'); testDiv.innerHTML = createHTMLLocal('1'); return { createHTML: createHTMLLocal, error: null }; } catch (error) { console.error('[YouTube+] TrustedTypes local policy failed:', error); return { createHTML: identityFnLocal, error }; } }; const createNextBrowserTickLocal = existing => { if (existing?.version >= 2) { return existing; } const SafePromise = (async () => {})().constructor; const queue = typeof queueMicrotask === 'function' ? callback => queueMicrotask(callback) : callback => SafePromise.resolve().then(callback); const scheduler = callback => { if (typeof callback === 'function') { queue(callback); return; } return SafePromise.resolve(); }; scheduler.version = 2; return scheduler; }; const { createHTML: _createHTMLInner, error: trustHTMLErrInner } = ensureTrustedTypesPolicyLocal(); if (trustHTMLErrInner) { console.error( '[YouTube+] TrustedHTML Error: Script cannot run due to CSP restrictions', trustHTMLErrInner ); return; } const nextBrowserTickInner = createNextBrowserTickLocal( (typeof window !== 'undefined' && window.nextBrowserTick) || undefined ); if ( typeof window !== 'undefined' && (!window.nextBrowserTick || window.nextBrowserTick.version < 2) ) { window.nextBrowserTick = nextBrowserTickInner; } try { let executionFinished = 0; if (typeof CustomElementRegistry === 'undefined') return; if (CustomElementRegistry.prototype.define000) return; if (typeof CustomElementRegistry.prototype.define !== 'function') return; const HTMLElement_ = HTMLElement.prototype.constructor; const selectorCache = new Map(); const CACHE_MAX_SIZE = 50; const CACHE_TTL = 5000; const clearExpiredCache = () => { const now = Date.now(); for (const [key, value] of selectorCache.entries()) { if (now - value.timestamp > CACHE_TTL) { selectorCache.delete(key); } } if (selectorCache.size > CACHE_MAX_SIZE) { const entriesToRemove = selectorCache.size - CACHE_MAX_SIZE; const iterator = selectorCache.keys(); for (let i = 0; i < entriesToRemove; i++) { const key = iterator.next().value; if (key !== undefined) { selectorCache.delete(key); } } } }; const cacheCleanupInterval = setInterval(clearExpiredCache, CACHE_TTL); if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { clearInterval(cacheCleanupInterval); selectorCache.clear(); }); } const qsOne = (elm, selector) => { return HTMLElement_.prototype.querySelector.call(elm, selector); }; const _qsAll = (elm, selector) => { return HTMLElement_.prototype.querySelectorAll.call(elm, selector); }; const defineProperties = (p, o) => { if (!p) { console.warn(`defineProperties ERROR: Prototype is undefined`); return; } for (const k of Object.keys(o)) { if (!o[k]) { console.warn(`defineProperties ERROR: Property ${k} is undefined`); delete o[k]; } } return Object.defineProperties(p, o); }; const replaceChildrenPolyfill = function replaceChildren(...new_children) { let el = this.firstChild; while (el) { const next = el.nextSibling; el.remove(); el = next; } this.append(...new_children); }; const pdsBaseDF = Object.getOwnPropertyDescriptors(DocumentFragment.prototype); if (pdsBaseDF.replaceChildren) { defineProperties(DocumentFragment.prototype, { replaceChildren000: pdsBaseDF.replaceChildren, }); } else { DocumentFragment.prototype.replaceChildren000 = replaceChildrenPolyfill; } const pdsBaseNode = Object.getOwnPropertyDescriptors(Node.prototype); if (!pdsBaseNode.appendChild000 && !pdsBaseNode.insertBefore000) { defineProperties(Node.prototype, { appendChild000: pdsBaseNode.appendChild, insertBefore000: pdsBaseNode.insertBefore, }); } const pdsBaseElement = Object.getOwnPropertyDescriptors(Element.prototype); if (!pdsBaseElement.setAttribute000 && !pdsBaseElement.querySelector000) { const nPdsElement = { setAttribute000: pdsBaseElement.setAttribute, getAttribute000: pdsBaseElement.getAttribute, hasAttribute000: pdsBaseElement.hasAttribute, removeAttribute000: pdsBaseElement.removeAttribute, querySelector000: pdsBaseElement.querySelector, }; if (pdsBaseElement.replaceChildren) { nPdsElement.replaceChildren000 = pdsBaseElement.replaceChildren; } else { Element.prototype.replaceChildren000 = replaceChildrenPolyfill; } defineProperties(Element.prototype, nPdsElement); } Element.prototype.setAttribute111 = function (p, v) { if (!p || typeof p !== 'string') { console.warn('[YouTube+] setAttribute111: invalid attribute name', p); return false; } try { const valueStr = v === null || v === undefined ? '' : `${v}`; const currentValue = this.getAttribute000(p); if (currentValue === valueStr) return true; this.setAttribute000(p, valueStr); return true; } catch (error) { console.warn('[YouTube+] setAttribute111 failed:', error, 'attr:', p, 'value:', v); try { this.setAttribute(p, v === null || v === undefined ? '' : `${v}`); return true; } catch (fallbackError) { console.error('[YouTube+] setAttribute111 fallback also failed:', fallbackError); return false; } } }; Element.prototype.incAttribute111 = function (p) { if (!p || typeof p !== 'string') { console.warn('[YouTube+] incAttribute111: invalid attribute name', p); return 0; } try { let v = +this.getAttribute000(p) || 0; v = v > MAX_ATTRIBUTE_VALUE ? ATTRIBUTE_RESET_VALUE : v + 1; this.setAttribute000(p, `${v}`); return v; } catch (error) { console.warn('[YouTube+] incAttribute111 failed:', error, p); return 0; } }; Element.prototype.assignChildren111 = function (previousSiblings, node, nextSiblings) { if (!node) { console.warn('[YouTube+] assignChildren111: node is required'); return; } try { let nodeList = []; for (let t = this.firstChild; t instanceof Node; t = t.nextSibling) { if (t === node) continue; nodeList.push(t); } inPageRearrange = true; if (node.parentNode === this) { let fm = new DocumentFragment(); if (nodeList.length > 0) { fm.replaceChildren000(...nodeList); } if (previousSiblings?.length > 0) { fm.replaceChildren000(...previousSiblings); this.insertBefore000(fm, node); } if (nextSiblings?.length > 0) { fm.replaceChildren000(...nextSiblings); this.appendChild000(fm); } fm.replaceChildren000(); fm = null; } else { this.replaceChildren000(...(previousSiblings || []), node, ...(nextSiblings || [])); } inPageRearrange = false; if (nodeList.length > 0) { for (const t of nodeList) { if (t instanceof Element && t.isConnected === false) { t.remove(); } } } nodeList.length = 0; nodeList = null; } catch (error) { inPageRearrange = false; console.error('[YouTube+] assignChildren111 failed:', error); } }; const DISABLE_FLAGS_SHADYDOM_FREE = true; (() => { let e; if (typeof unsafeWindow === 'undefined') { e = this instanceof Window ? this : window; } else { e = unsafeWindow; } if (!e._ytConfigHacks) { let t = 4; class n extends Set { add(handler) { if (t <= 0) return console.warn('yt.config_ is already applied on the page.'); typeof handler == 'function' && super.add(handler); } } const a = (async () => {})().constructor; const i = new n(); e._ytConfigHacks = i; let l = () => { const ytcsiOriginal = e.ytcsi.originalYtcsi; if (ytcsiOriginal) { e.ytcsi = ytcsiOriginal; l = null; } }; let c = null; const getConfigData = () => { return (e.yt || 0).config_ || (e.ytcfg || 0).data_ || 0; }; const isValidConfig = configData => { return ( typeof configData.INNERTUBE_API_KEY === 'string' && typeof configData.EXPERIMENT_FLAGS === 'object' ); }; const executeHandlers = configData => { for (const handler of i) handler(configData); }; const o = () => { if (t < 1) return; const configData = getConfigData(); if (!isValidConfig(configData)) return; --t; if (t <= 0 && l) l(); c = !0; executeHandlers(configData); }; let f = 1; const d = tParam => { const ytcsiValue = tParam || e.ytcsi; if (ytcsiValue) { return ( (e.ytcsi = new Proxy(ytcsiValue, { get: (proxy, prop) => prop === 'originalYtcsi' ? proxy : (o(), c && --f <= 0 && l && l(), proxy[prop]), })), !0 ); } }; d() || Object.defineProperty(e, 'ytcsi', { get() {}, set: value => (value && (delete e.ytcsi, d(value)), !0), enumerable: !1, configurable: !0, }); const { addEventListener: s, removeEventListener: y } = Document.prototype; function r(removeListener) { o(); if (removeListener) { e.removeEventListener('DOMContentLoaded', r, !1); } } (new a(resolveCallback => { if (typeof AbortSignal === 'undefined') { const cleanupHandler = () => { resolveCallback(); y.call(document, 'yt-page-data-fetched', cleanupHandler, !1); y.call(document, 'yt-navigate-finish', cleanupHandler, !1); y.call(document, 'spfdone', cleanupHandler, !1); }; s.call(document, 'yt-page-data-fetched', cleanupHandler, !1); s.call(document, 'yt-navigate-finish', cleanupHandler, !1); s.call(document, 'spfdone', cleanupHandler, !1); } else { s.call(document, 'yt-page-data-fetched', resolveCallback, { once: !0 }); s.call(document, 'yt-navigate-finish', resolveCallback, { once: !0 }); s.call(document, 'spfdone', resolveCallback, { once: !0 }); } }).then(o), new a(actionCallback => { if (typeof AbortSignal === 'undefined') { const actionHandler = () => { actionCallback(); y.call(document, 'yt-action', actionHandler, !0); }; s.call(document, 'yt-action', actionHandler, !0); } else { s.call(document, 'yt-action', actionCallback, { once: !0, capture: !0 }); } }).then(o), a.resolve().then(() => { if (document.readyState === 'loading') { e.addEventListener('DOMContentLoaded', r, !1); } else { r(); } })); } })(); let configOnce = false; window._ytConfigHacks.add(config_ => { if (configOnce) return; configOnce = true; const EXPERIMENT_FLAGS = config_.EXPERIMENT_FLAGS || 0; const EXPERIMENTS_FORCED_FLAGS = config_.EXPERIMENTS_FORCED_FLAGS || 0; for (const flags of [EXPERIMENT_FLAGS, EXPERIMENTS_FORCED_FLAGS]) { if (flags) { flags.web_watch_chat_hide_button_killswitch = false; flags.web_watch_theater_chat = false; flags.suppress_error_204_logging = true; flags.kevlar_watch_grid = false; if (DISABLE_FLAGS_SHADYDOM_FREE) { flags.enable_shadydom_free_scoped_node_methods = false; flags.enable_shadydom_free_scoped_query_methods = false; flags.enable_shadydom_free_scoped_readonly_properties_batch_one = false; flags.enable_shadydom_free_parent_node = false; flags.enable_shadydom_free_children = false; flags.enable_shadydom_free_last_child = false; } } } }); const mWeakRef = typeof WeakRef === 'function' ? o => (o ? new WeakRef(o) : null) : o => o || null; const kRef = wr => (wr && wr.deref ? wr.deref() : wr); const Promise = (async () => {})().constructor; const delayPn = delay => new Promise(fn => setTimeout(fn, delay)); const insp = o => (o ? o.polymerController || o.inst || o || 0 : o || 0); const setTimeout_ = setTimeout.bind(window); const handlePromiseError = (error, context = 'Unknown') => { if (error) { console.error(`[YouTube+] Promise error in ${context}:`, error); } }; const PromiseExternal = (() => { let capturedResolve; let capturedReject; const h = (resolve, reject) => { capturedResolve = resolve; capturedReject = reject; }; return class PromiseExternalImpl extends Promise { constructor(cb = h) { super(cb); if (cb === h) { this.resolve = capturedResolve; this.reject = capturedReject; } } }; })(); const isPassiveArgSupport = typeof IntersectionObserver === 'function'; const _bubblePassive = isPassiveArgSupport ? { capture: false, passive: true } : false; const capturePassive = isPassiveArgSupport ? { capture: true, passive: true } : true; class Attributer { constructor(list) { this.list = list; this.flag = 0; } makeString() { let k = 1; let s = ''; let i = 0; while (this.flag >= k) { if (this.flag & k) { s += this.list[i]; } i++; k <<= 1; } return s; } } const mLoaded = new Attributer('icp'); const wrSelfMap = new WeakMap(); const elements = new Proxy( { related: null, comments: null, infoExpander: null, }, { get(target, prop) { return kRef(target[prop]); }, set(target, prop, value) { if (value) { let wr = wrSelfMap.get(value); if (!wr) { wr = mWeakRef(value); wrSelfMap.set(value, wr); } target[prop] = wr; } else { target[prop] = null; } return true; }, } ); const getMainInfo = () => { const { infoExpander } = elements; if (!infoExpander) return null; const mainInfo = infoExpander.matches('[tyt-main-info]') ? infoExpander : infoExpander.querySelector000('[tyt-main-info]'); return mainInfo || null; }; const _asyncWrap = asyncFn => { return () => { Promise.resolve().then(asyncFn); }; }; let pageType = null; let pageLang = 'en'; const langWords = { en: { info: 'Info', videos: 'Videos', playlist: 'Playlist', }, jp: { info: '情報', videos: '動画', playlist: '再生リスト', }, tw: { info: '資訊', videos: '影片', playlist: '播放清單', }, cn: { info: '资讯', videos: '视频', playlist: '播放列表', }, du: { info: 'Info', videos: 'Videos', playlist: 'Playlist', }, fr: { info: 'Info', videos: 'Vidéos', playlist: 'Playlist', }, kr: { info: '정보', videos: '동영상', playlist: '재생목록', }, ru: { info: 'Описание', videos: 'Видео', playlist: 'Плейлист', }, tr: { info: 'Bilgi', videos: 'Videolar', playlist: 'Oynatma Listesi', }, }; const svgComments = ``.trim(); const svgVideos = ``.trim(); const svgInfo = ``.trim(); const svgPlayList = ``.trim(); const svgDiag1 = ``; const svgDiag2 = ``; const getGMT = () => { const m = new Date('2023-01-01T00:00:00Z'); return m.getDate() === 1 ? `+${m.getHours()}` : `-${24 - m.getHours()}`; }; function getWord(tag) { return langWords[pageLang]?.[tag] || langWords['en']?.[tag] || ''; } const svgElm = (w, h, vw, vh, p, m) => `${p}`; const hiddenTabsByUserCSS = 0; function getTabsHTML() { const sTabBtnVideos = `${svgElm(16, 16, 90, 90, svgVideos)}${getWord('videos')}`; const sTabBtnInfo = `${svgElm(16, 16, 60, 60, svgInfo)}${getWord('info')}`; const sTabBtnPlayList = `${svgElm(16, 16, 20, 20, svgPlayList)}${getWord('playlist')}`; const str1 = `
`; const str_fbtns = `
`.replace(/[\r\n]+/g, ''); const str_tabs = [ `${sTabBtnInfo}${str1}${str_fbtns}`, `${svgElm(16, 16, 120, 120, svgComments)}${str1}${str_fbtns}`, `${sTabBtnVideos}${str1}${str_fbtns}`, `${sTabBtnPlayList}${str1}${str_fbtns}`, ].join(''); const addHTML = `
${str_tabs}
`; return addHTML; } function getLang() { const htmlLang = ((document || 0).documentElement || 0).lang || ''; const langMap = { en: 'en', 'en-GB': 'en', de: 'du', 'de-DE': 'du', fr: 'fr', 'fr-CA': 'fr', 'fr-FR': 'fr', 'zh-Hant': 'tw', 'zh-Hant-HK': 'tw', 'zh-Hant-TW': 'tw', 'zh-Hans': 'cn', 'zh-Hans-CN': 'cn', ja: 'jp', 'ja-JP': 'jp', ko: 'kr', 'ko-KR': 'kr', ru: 'ru', 'ru-RU': 'ru', }; return langMap[htmlLang] || 'en'; } function getLangForPage() { const lang = getLang(); pageLang = langWords[lang] ? lang : 'en'; } const _locks = {}; const lockGet = new Proxy(_locks, { get(target, prop) { return target[prop] || 0; }, set(_target, _prop, _val) { return true; }, }); const lockSet = new Proxy(_locks, { get(target, prop) { if (target[prop] > MAX_ATTRIBUTE_VALUE) target[prop] = ATTRIBUTE_RESET_VALUE; return (target[prop] = (target[prop] || 0) + 1); }, set(_target, _prop, _val) { return true; }, }); const videosElementProvidedPromise = new PromiseExternal(); const navigateFinishedPromise = new PromiseExternal(); let isRightTabsInserted = false; const rightTabsProvidedPromise = new PromiseExternal(); const infoExpanderElementProvidedPromise = new PromiseExternal(); const pluginsDetected = {}; const pluginDetectObserver = new MutationObserver(mutations => { let changeOnRoot = false; const newPlugins = []; const attributeChangedSet = new Set(); for (const mutation of mutations) { if (mutation.target === document) changeOnRoot = true; let detected = ''; switch (mutation.attributeName) { case 'data-ytlstm-new-layout': case 'data-ytlstm-overlay-text-shadow': case 'data-ytlstm-theater-mode': detected = 'external.ytlstm'; attributeChangedSet.add(detected); break; } if (detected && !pluginsDetected[detected]) { pluginsDetected[detected] = true; newPlugins.push(detected); } } if (elements.flexy && attributeChangedSet.has('external.ytlstm')) { elements.flexy.setAttribute( 'tyt-external-ytlstm', document.querySelector('[data-ytlstm-theater-mode]') ? '1' : '0' ); } if (changeOnRoot) { try { if (document.body) { pluginDetectObserver.observe(document.body, { attributes: true }); } else { document.addEventListener( 'DOMContentLoaded', () => { try { pluginDetectObserver.observe(document.body, { attributes: true }); } catch (observeError) { console.error( '[YouTube+] pluginDetectObserver.observe failed after DOMContentLoaded:', observeError ); } }, { once: true } ); } } catch (observeError) { console.error('[YouTube+] pluginDetectObserver.observe failed:', observeError); } } for (const detected of newPlugins) { const pluginItem = plugin[`${detected}`]; if (pluginItem) { pluginItem.activate(); } else { console.warn(`No Plugin Activator for ${detected}`); } } }); pluginDetectObserver.observe(document.documentElement, { attributes: true }); if (document.body) pluginDetectObserver.observe(document.body, { attributes: true }); navigateFinishedPromise.then(() => { pluginDetectObserver.observe(document.documentElement, { attributes: true }); if (document.body) pluginDetectObserver.observe(document.body, { attributes: true }); }); const funcCanCollapse = function (_s) { const { playlist: playlistElm, flexy: ytdFlexyElm } = elements; let doAttributeChange = 0; if (playlistElm && ytdFlexyElm) { if (playlistElm.closest('[hidden]')) { doAttributeChange = 2; } else if (playlistElm.hasAttribute000('collapsed')) { doAttributeChange = 2; } else { doAttributeChange = 1; } } else if (ytdFlexyElm) { doAttributeChange = 2; } if (doAttributeChange === 1) { if (getAttributeSafe(ytdFlexyElm, 'tyt-playlist-expanded') !== '') { setAttributeSafe(ytdFlexyElm, 'tyt-playlist-expanded', ''); } } else if (doAttributeChange === 2) { if (ytdFlexyElm.hasAttribute000('tyt-playlist-expanded')) { removeAttributeSafe(ytdFlexyElm, 'tyt-playlist-expanded'); } } }; const aoChatAttrChangeFn = async lockId => { if (lockGet['aoChatAttrAsyncLock'] !== lockId) return; const { chat: chatElm, flexy: ytdFlexyElm } = elements; if (chatElm && ytdFlexyElm) { const isChatCollapsed = chatElm.hasAttribute000('collapsed'); if (isChatCollapsed) { setAttributeSafe(ytdFlexyElm, 'tyt-chat-collapsed', ''); } else { removeAttributeSafe(ytdFlexyElm, 'tyt-chat-collapsed'); } setAttributeSafe(ytdFlexyElm, 'tyt-chat', isChatCollapsed ? '-' : '+'); } }; const aoPlayListAttrChangeFn = async lockId => { if (lockGet['aoPlayListAttrAsyncLock'] !== lockId) return; const { playlist: playlistElm, flexy: ytdFlexyElm } = elements; if (playlistElm && ytdFlexyElm) { if (playlistElm.hasAttribute000('collapsed')) { removeAttributeSafe(ytdFlexyElm, 'tyt-playlist-expanded'); } else { setAttributeSafe(ytdFlexyElm, 'tyt-playlist-expanded', ''); } } else if (ytdFlexyElm) { removeAttributeSafe(ytdFlexyElm, 'tyt-playlist-expanded'); } }; const aoChat = new MutationObserver(() => { Promise.resolve(lockSet['aoChatAttrAsyncLock']) .then(aoChatAttrChangeFn) .catch(err => handlePromiseError(err, 'aoChatAttrChange')); }); const aoPlayList = new MutationObserver(() => { Promise.resolve(lockSet['aoPlayListAttrAsyncLock']) .then(aoPlayListAttrChangeFn) .catch(err => handlePromiseError(err, 'aoPlayListAttrChange')); }); const aoComment = new MutationObserver(async mutations => { const commentsArea = elements.comments; const ytdFlexyElm = elements.flexy; if (!commentsArea) return; let bfHidden = false; let bfCommentsVideoId = false; let bfCommentDisabled = false; for (const mutation of mutations) { if (mutation.attributeName === 'hidden' && mutation.target === commentsArea) { bfHidden = true; } else if ( mutation.attributeName === 'tyt-comments-video-id' && mutation.target === commentsArea ) { bfCommentsVideoId = true; } else if ( mutation.attributeName === 'tyt-comments-data-status' && mutation.target === commentsArea ) { bfCommentDisabled = true; } } if (bfHidden) { if (!commentsArea.hasAttribute000('hidden')) { Promise.resolve(commentsArea) .then(eventMap['settingCommentsVideoId']) .catch(err => handlePromiseError(err, 'settingCommentsVideoId')); } Promise.resolve(lockSet['removeKeepCommentsScrollerLock']) .then(removeKeepCommentsScroller) .catch(err => handlePromiseError(err, 'removeKeepCommentsScroller')); } if ((bfHidden || bfCommentsVideoId || bfCommentDisabled) && ytdFlexyElm) { const commentsDataStatus = +( getAttributeSafe(commentsArea, 'tyt-comments-data-status') || '0' ); if (commentsDataStatus === 2) { setAttributeSafe(ytdFlexyElm, 'tyt-comment-disabled', ''); } else if (commentsDataStatus === 1) { removeAttributeSafe(ytdFlexyElm, 'tyt-comment-disabled'); } Promise.resolve(lockSet['checkCommentsShouldBeHiddenLock']) .then(eventMap['checkCommentsShouldBeHidden']) .catch(err => handlePromiseError(err, 'checkCommentsShouldBeHidden')); const lockId = lockSet['rightTabReadyLock01']; await rightTabsProvidedPromise.then(); if (lockGet['rightTabReadyLock01'] !== lockId) return; if (elements.comments !== commentsArea) return; if (commentsArea.isConnected === false) return; if (commentsArea.closest('#tab-comments')) { const shouldTabVisible = !commentsArea.closest('[hidden]'); document .querySelector('[tyt-tab-content="#tab-comments"]') .classList.toggle('tab-btn-hidden', !shouldTabVisible); } } }); const ioComment = new IntersectionObserver( entries => { for (const entry of entries) { const { target } = entry; const cnt = insp(target); if ( entry.isIntersecting && target instanceof HTMLElement_ && typeof cnt.calculateCanCollapse === 'function' ) { cnt.calculateCanCollapse(true); setAttributeSafe(target, 'io-intersected', ''); const ytdFlexyElm = elements.flexy; if (ytdFlexyElm && !ytdFlexyElm.hasAttribute000('keep-comments-scroller')) { setAttributeSafe(ytdFlexyElm, 'keep-comments-scroller', ''); } } else if (target.hasAttribute000('io-intersected')) { removeAttributeSafe(target, 'io-intersected'); } } }, { threshold: [0], rootMargin: '32px', } ); let bFixForResizedTabLater = false; let lastRoRightTabsWidth = 0; const roRightTabs = new ResizeObserver(entries => { const entry = entries[entries.length - 1]; const width = Math.round(entry.borderBoxSize.inlineSize); if (lastRoRightTabsWidth !== width) { lastRoRightTabsWidth = width; if ((tabAStatus & 2) === 2) { bFixForResizedTabLater = false; Promise.resolve(1).then(eventMap['fixForTabDisplay']); } else { bFixForResizedTabLater = true; } } }); const getAttributeSafe = (element, attrName) => { if (!element || !attrName) return null; try { if (typeof element.getAttribute000 === 'function') { return element.getAttribute000(attrName); } return element.getAttribute(attrName); } catch { return element.getAttribute(attrName); } }; const setAttributeSafe = (element, attrName, value) => { if (!element || !attrName) return; try { if (typeof element.setAttribute111 === 'function') { element.setAttribute111(attrName, value); } else { element.setAttribute(attrName, value); } } catch { element.setAttribute(attrName, value); } }; const removeAttributeSafe = (element, attrName) => { if (!element || !attrName) return; try { if (typeof element.removeAttribute000 === 'function') { element.removeAttribute000(attrName); } else { element.removeAttribute(attrName); } } catch { element.removeAttribute(attrName); } }; const findTabLinks = () => { let links = document.querySelectorAll('#material-tabs a[tyt-tab-content]'); if (links.length === 0) { links = document.querySelectorAll('#right-tabs a[tyt-tab-content]'); } return links; }; const updateTabLinkState = (link, isActive) => { if (isActive) { link.classList.add('active'); } else { link.classList.remove('active'); } }; const updateTabContentVisibility = (content, isActive) => { if (isActive) { content.classList.remove('tab-content-hidden'); removeAttributeSafe(content, 'tyt-hidden'); } else { content.classList.add('tab-content-hidden'); if (!content.hasAttribute('tyt-hidden')) { setAttributeSafe(content, 'tyt-hidden', ''); } } }; const switchToTab = activeLinkParam => { try { let activeLink = activeLinkParam; if (typeof activeLink === 'string') { const selector = `a[tyt-tab-content="${activeLink}"]`; activeLink = document.querySelector(selector) || null; if (!activeLink) { console.warn(`[YouTube+] switchToTab: could not find tab with selector "${selector}"`); } } const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) { console.warn('[YouTube+] switchToTab: flexy element not found'); return; } const links = findTabLinks(); if (links.length === 0) { console.error('[YouTube+] switchToTab: CRITICAL - no tabs found at all!'); return; } let activatedCount = 0; let deactivatedCount = 0; for (const link of links) { try { const contentSelector = getAttributeSafe(link, 'tyt-tab-content'); if (!contentSelector) { console.warn('[YouTube+] switchToTab: link missing tyt-tab-content attribute'); continue; } const content = document.querySelector(contentSelector); if (!content) { console.warn( `[YouTube+] switchToTab: content not found for selector "${contentSelector}"` ); continue; } const isActive = link === activeLink; updateTabLinkState(link, isActive); updateTabContentVisibility(content, isActive); if (isActive) { activatedCount++; } else { deactivatedCount++; } } catch (linkError) { console.warn('[YouTube+] switchToTab: error processing link', linkError); } } const switchingTo = activeLink ? getAttributeSafe(activeLink, 'tyt-tab-content') || '' : ''; if (DEBUG_5085) { console.log( `[YouTube+] switchToTab: switching to "${switchingTo}", activated ${activatedCount}, deactivated ${deactivatedCount}` ); } if (switchingTo) { lastTab = switchingTo; lastPanel = switchingTo; } const currentChat = getAttributeSafe(ytdFlexyElm, 'tyt-chat'); if (currentChat === '') { removeAttributeSafe(ytdFlexyElm, 'tyt-chat'); } setAttributeSafe(ytdFlexyElm, 'tyt-tab', switchingTo); if (switchingTo) { bFixForResizedTabLater = false; Promise.resolve(0).then(eventMap['fixForTabDisplay']).catch(console.warn); } } catch (error) { console.error('[YouTube+] switchToTab: critical error', error); console.error(error.stack); } }; let tabAStatus = 0; const flagCheckers = { 1: elem => elem.hasAttribute000('theater'), 2: elem => getAttributeSafe(elem, 'tyt-tab'), 4: elem => getAttributeSafe(elem, 'tyt-chat') === '-', 8: elem => getAttributeSafe(elem, 'tyt-chat') === '+', 16: elem => elem.hasAttribute000('is-two-columns_'), 32: elem => elem.hasAttribute000('tyt-egm-panel_'), 64: () => !!document.fullscreenElement, 128: elem => elem.hasAttribute000('tyt-playlist-expanded'), 4096: elem => elem.getAttribute('tyt-external-ytlstm') === '1', }; const checkFlagCondition = (element, flagBit) => { const checker = flagCheckers[flagBit]; return checker ? checker(element) : false; }; const calculationFn = (initialResult = 0, flag) => { const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) return initialResult; let result = initialResult; for (const flagBit of Object.keys(flagCheckers)) { const bit = Number(flagBit); if (flag & bit) { result |= bit; if (!checkFlagCondition(ytdFlexyElm, bit)) { result -= bit; } } } return result; }; function isTheater() { return Boolean(elements.flexy?.hasAttribute000('theater')); } function getTheaterButton() { return document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button'); } function ytBtnSetTheater() { if (!isTheater()) { getTheaterButton()?.click(); } } function ytBtnCancelTheater() { if (isTheater()) { getTheaterButton()?.click(); } } function getSuitableElement(selector) { const matchedElements = document.querySelectorAll(selector); let bestIndex = -1; let maxDepth = -1; for (let i = 0; i < matchedElements.length; i++) { const depth = matchedElements[i].getElementsByTagName('*').length; if (depth > maxDepth) { maxDepth = depth; bestIndex = i; } } return bestIndex >= 0 ? matchedElements[bestIndex] : null; } function tryExpandUsingCollapsedState(cnt) { if (!cnt || typeof cnt.collapsed !== 'boolean') return false; if (typeof cnt.setCollapsedState === 'function') { cnt.setCollapsedState({ setLiveChatCollapsedStateAction: { collapsed: false, }, }); if (cnt.collapsed === false) return true; } cnt.collapsed = false; if (cnt.collapsed === false) return true; if (cnt.isHiddenByUser === true && cnt.collapsed === true) { cnt.isHiddenByUser = false; cnt.collapsed = false; return cnt.collapsed === false; } return false; } function tryExpandUsingButton() { let button = document.querySelector( 'ytd-live-chat-frame#chat[collapsed] > .ytd-live-chat-frame#show-hide-button' ); if (button) { button = button.querySelector000('div.yt-spec-touch-feedback-shape') || button.querySelector000('ytd-toggle-button-renderer'); button?.click(); } } function ytBtnExpandChat() { const dom = getSuitableElement('ytd-live-chat-frame#chat'); const cnt = insp(dom); if (tryExpandUsingCollapsedState(cnt)) return; tryExpandUsingButton(); } function tryCollapseUsingCollapsedState(cnt) { if (!cnt || typeof cnt.collapsed !== 'boolean') return false; if (typeof cnt.setCollapsedState === 'function') { cnt.setCollapsedState({ setLiveChatCollapsedStateAction: { collapsed: true, }, }); if (cnt.collapsed === true) return true; } cnt.collapsed = true; if (cnt.collapsed === true) return true; if (cnt.isHiddenByUser === false && cnt.collapsed === false) { cnt.isHiddenByUser = true; cnt.collapsed = true; return cnt.collapsed === true; } return false; } function tryCollapseUsingButton() { let button = document.querySelector( 'ytd-live-chat-frame#chat:not([collapsed]) > .ytd-live-chat-frame#show-hide-button' ); if (button) { button = button.querySelector000('div.yt-spec-touch-feedback-shape') || button.querySelector000('ytd-toggle-button-renderer'); button?.click(); } } function ytBtnCollapseChat() { const dom = getSuitableElement('ytd-live-chat-frame#chat'); const cnt = insp(dom); if (tryCollapseUsingCollapsedState(cnt)) return; tryCollapseUsingButton(); } function normalizeToArray(arr) { if (!arr) return []; return 'length' in arr ? arr : [arr]; } function createHideAction(panelId) { return { changeEngagementPanelVisibilityAction: { targetId: panelId, visibility: 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN', }, }; } function createShowAction(panelId) { return { showEngagementPanelEndpoint: { panelIdentifier: panelId, }, }; } function buildPanelAction(entry) { if (!entry) return null; const { panelId, toHide, toShow } = entry; if (toHide === true && !toShow) { return createHideAction(panelId); } if (toShow === true && !toHide) { return createShowAction(panelId); } return null; } function executeEngagementPanelCommands(ytdFlexyElm, actions) { if (actions.length === 0) return; const cnt = insp(ytdFlexyElm); cnt.resolveCommand( { signalServiceEndpoint: { signal: 'CLIENT_SIGNAL', actions, }, }, {}, false ); } function ytBtnEgmPanelCore(arr) { const arrayParam = normalizeToArray(arr); const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm || arrayParam.length === 0) return; const actions = arrayParam.map(buildPanelAction).filter(Boolean); executeEngagementPanelCommands(ytdFlexyElm, actions); } function ytBtnCloseEngagementPanels() { const actions = []; for (const panelElm of document.querySelectorAll( `ytd-watch-flexy[tyt-tab] #panels.ytd-watch-flexy ytd-engagement-panel-section-list-renderer[target-id][visibility]:not([hidden])` )) { if ( panelElm.getAttribute('visibility') === 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED' && !panelElm.closest('[hidden]') ) { actions.push({ panelId: panelElm.getAttribute000('target-id'), toHide: true, }); } } ytBtnEgmPanelCore(actions); } function ytBtnOpenPlaylist() { const cnt = insp(elements.playlist); if (cnt && typeof cnt.collapsed === 'boolean') { cnt.collapsed = false; } } function ytBtnClosePlaylist() { const cnt = insp(elements.playlist); if (cnt && typeof cnt.collapsed === 'boolean') { cnt.collapsed = true; } } const updateChatLocation498 = function () { if (this.is !== 'ytd-watch-grid') { this.updatePageMediaQueries(); this.schedulePlayerSizeUpdate_(); } }; const mirrorNodeWS = new WeakMap(); const dummyNode = document.createElement('noscript'); const __j4836__ = Symbol(); const __j5744__ = Symbol(); const __j5733__ = Symbol(); const monitorDataChangedByDOMMutation = async function (_mutations) { const nodeWR = this; const node = kRef(nodeWR); if (!node) return; const cnt = insp(node); const __lastChanged__ = cnt[__j5733__]; const val = cnt.data ? cnt.data[__j4836__] || 1 : 0; if (__lastChanged__ !== val) { cnt[__j5733__] = val > 0 ? (cnt.data[__j4836__] = Date.now()) : 0; await Promise.resolve(); attributeInc(node, 'tyt-data-change-counter'); } }; const moChangeReflection = function (mutations) { const nodeWR = this; const node = kRef(nodeWR); if (!node) return; const originElement = kRef(node[__j5744__] || null) || null; if (!originElement) return; const cnt = insp(node); const oriCnt = insp(originElement); if (mutations) { let bfDataChangeCounter = false; for (const mutation of mutations) { if ( mutation.attributeName === 'tyt-clone-refresh-count' && mutation.target === originElement ) { bfDataChangeCounter = true; } else if ( mutation.attributeName === 'tyt-data-change-counter' && mutation.target === originElement ) { bfDataChangeCounter = true; } } if (bfDataChangeCounter && oriCnt.data) { node.replaceWith(dummyNode); cnt.data = { ...oriCnt.data }; dummyNode.replaceWith(node); } } }; const attributeInc = (elm, prop) => { let v = (+elm.getAttribute000(prop) || 0) + 1; if (v > MAX_ATTRIBUTE_VALUE) v = ATTRIBUTE_RESET_VALUE; elm.setAttribute000(prop, v); return v; }; const isChannelId = x => { return typeof x === 'string' && x.length === 24 && /^UC[-_a-zA-Z0-9+=.]{22}$/.test(x); }; const infoFix = lockId => { if (lockId !== null && lockGet['infoFixLock'] !== lockId) return; const { infoExpander } = elements; const infoContainer = (infoExpander ? infoExpander.parentNode : null) || document.querySelector('#tab-info'); const ytdFlexyElm = elements.flexy; if (!infoContainer || !ytdFlexyElm) return; if (infoExpander) { const match = infoExpander.matches('#tab-info > [class]') || infoExpander.matches('#tab-info > [tyt-main-info]'); if (!match) return; } const requireElements = [ ...document.querySelectorAll( 'ytd-watch-metadata.ytd-watch-flexy div[slot="extra-content"] > *, ytd-watch-metadata.ytd-watch-flexy #extra-content > *' ), ] .filter(elm => { return typeof elm.is == 'string'; }) .map(elmParam => { const { is } = elmParam; let elm = elmParam; while (elm instanceof HTMLElement_) { const q = [...elm.querySelectorAll(is)].filter(e => insp(e).data); if (q.length >= 1) return q[0]; elm = elm.parentNode; } }) .filter(elm => !!elm && typeof elm.is === 'string'); const source = requireElements.map(entry => { const inst = insp(entry); return { data: inst.data, tag: inst.is, elm: entry, }; }); let noscript_ = document.querySelector('noscript#aythl'); if (!noscript_) { noscript_ = document.createElement('noscript'); noscript_.id = 'aythl'; inPageRearrange = true; ytdFlexyElm.insertBefore000(noscript_, ytdFlexyElm.firstChild); inPageRearrange = false; } const noscript = noscript_; let requiredUpdate = false; const mirrorElmSet = new Set(); const targetParent = infoContainer; for (const { data, tag: tag, elm: s } of source) { let mirrorNode = mirrorNodeWS.get(s); mirrorNode = mirrorNode ? kRef(mirrorNode) : mirrorNode; if (!mirrorNode) { const cnt = insp(s); const cProto = cnt.constructor.prototype; const element = document.createElement(tag); noscript.appendChild(element); mirrorNode = element; mirrorNode[__j5744__] = mWeakRef(s); const nodeWR = mWeakRef(mirrorNode); if (s && s instanceof Node) { try { new MutationObserver(moChangeReflection.bind(nodeWR)).observe(s, { attributes: true, attributeFilter: ['tyt-clone-refresh-count', 'tyt-data-change-counter'], }); } catch (observeError) { console.error( '[YouTube+] Failed to observe source element for mirror reflection:', observeError, s ); } } s.jy8432 = 1; if ( !(cProto instanceof Node) && !cProto._dataChanged496 && typeof cProto._createPropertyObserver === 'function' ) { cProto._dataChanged496 = function () { const cntInner = this; const node = cntInner.hostElement || cntInner; if (node.jy8432) { attributeInc(node, 'tyt-data-change-counter'); } }; cProto._createPropertyObserver('data', '_dataChanged496', undefined); } else if ( !(cProto instanceof Node) && !cProto._dataChanged496 && cProto.useSignals === true && insp(s).signalProxy ) { const dataSignal = cnt?.signalProxy?.signalCache?.data; if ( dataSignal && typeof dataSignal.setWithPath === 'function' && !dataSignal.setWithPath573 && !dataSignal.controller573 ) { dataSignal.controller573 = mWeakRef(cnt); dataSignal.setWithPath573 = dataSignal.setWithPath; dataSignal.setWithPath = function (...args) { const controller = kRef(this.controller573 || null) || null; controller && typeof controller._dataChanged496k === 'function' && Promise.resolve(controller) .then(controller._dataChanged496k) .catch(err => handlePromiseError(err, 'setWithPath_dataChanged496k')); return this.setWithPath573(...args); }; cProto._dataChanged496 = function () { const controller = this; const node = controller.hostElement || controller; if (node.jy8432) { attributeInc(node, 'tyt-data-change-counter'); } }; cProto._dataChanged496k = controller => controller._dataChanged496(); } } if (!cProto._dataChanged496) { if (s && s instanceof Node) { try { new MutationObserver( monitorDataChangedByDOMMutation.bind(mirrorNode[__j5744__]) ).observe(s, { attributes: true, childList: true, subtree: true }); } catch (observeError) { console.error( '[YouTube+] Failed to observe source element for data-change monitoring:', observeError, s ); } } } mirrorNodeWS.set(s, nodeWR); requiredUpdate = true; } else if (mirrorNode.parentNode !== targetParent) { requiredUpdate = true; } if (!requiredUpdate) { const cloneNodeCnt = insp(mirrorNode); if (cloneNodeCnt.data !== data) { requiredUpdate = true; } } mirrorElmSet.add(mirrorNode); source.mirrored = mirrorNode; } const mirroElmArr = [...mirrorElmSet]; mirrorElmSet.clear(); if (!requiredUpdate) { let e = infoExpander ? -1 : 0; for (let n = targetParent.firstChild; n instanceof Node; n = n.nextSibling) { const target = e < 0 ? infoExpander : mirroElmArr[e]; e++; if (n !== target) { requiredUpdate = true; break; } } if (!requiredUpdate && e !== mirroElmArr.length + 1) requiredUpdate = true; } if (requiredUpdate) { if (infoExpander) { targetParent.assignChildren111(null, infoExpander, mirroElmArr); } else { targetParent.replaceChildren000(...mirroElmArr); } for (const mirrorElm of mirroElmArr) { const j = attributeInc(mirrorElm, 'tyt-clone-refresh-count'); const oriElm = kRef(mirrorElm[__j5744__] || null) || null; if (oriElm) { oriElm.setAttribute111('tyt-clone-refresh-count', j); } } } mirroElmArr.length = 0; source.length = 0; }; const layoutFix = lockId => { if (lockGet['layoutFixLock'] !== lockId) return; const secondaryWrapper = document.querySelector( '#secondary-inner.style-scope.ytd-watch-flexy > secondary-wrapper' ); if (secondaryWrapper) { const secondaryInner = secondaryWrapper.parentNode; const chatContainer = document.querySelector( '#columns.style-scope.ytd-watch-flexy [tyt-chat-container]' ); if ( secondaryInner.firstChild !== secondaryInner.lastChild || (chatContainer && !chatContainer.closest('secondary-wrapper')) ) { const w = []; const w2 = []; for ( let node = secondaryInner.firstChild; node instanceof Node; node = node.nextSibling ) { if (node === chatContainer && chatContainer) { } else if (node === secondaryWrapper) { for ( let node2 = secondaryWrapper.firstChild; node2 instanceof Node; node2 = node2.nextSibling ) { if (node2 === chatContainer && chatContainer) { } else { if (node2.id === 'right-tabs' && chatContainer) { w2.push(chatContainer); } w2.push(node2); } } } else { w.push(node); } } inPageRearrange = true; secondaryWrapper.replaceChildren000(...w, ...w2); inPageRearrange = false; const chatElm = elements.chat; const chatCnt = insp(chatElm); if ( chatCnt && typeof chatCnt.urlChanged === 'function' && secondaryWrapper.contains(chatElm) ) { if (typeof chatCnt.urlChangedAsync12 === 'function') { DEBUG_5085 && console.log('elements.chat urlChangedAsync12', 61); chatCnt.urlChanged(); } else { DEBUG_5085 && console.log('elements.chat urlChangedAsync12', 62); setTimeout(() => chatCnt.urlChanged(), 136); } } } } }; let lastPanel = ''; let lastTab = ''; const aoEgmPanels = new MutationObserver(() => { Promise.resolve(lockSet['updateEgmPanelsLock']) .then(updateEgmPanels) .catch(err => handlePromiseError(err, 'aoEgmPanels_updateEgmPanels')); }); const removeKeepCommentsScroller = async lockId => { if (lockGet['removeKeepCommentsScrollerLock'] !== lockId) return; await Promise.resolve(); if (lockGet['removeKeepCommentsScrollerLock'] !== lockId) return; const ytdFlexyFlm = elements.flexy; if (ytdFlexyFlm) { ytdFlexyFlm.removeAttribute000('keep-comments-scroller'); } }; const updateEgmPanels = async lockId => { if (lockId !== lockGet['updateEgmPanelsLock']) return; await navigateFinishedPromise.then().catch(console.warn); if (lockId !== lockGet['updateEgmPanelsLock']) return; const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) return; let newVisiblePanels = []; let newHiddenPanels = []; let allVisiblePanels = []; for (const panelElm of document.querySelectorAll('[tyt-egm-panel][target-id][visibility]')) { const visibility = panelElm.getAttribute000('visibility'); if (visibility === 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN' || panelElm.closest('[hidden]')) { if (panelElm.hasAttribute000('tyt-visible-at')) { panelElm.removeAttribute000('tyt-visible-at'); newHiddenPanels.push(panelElm); } } else if ( visibility === 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED' && !panelElm.closest('[hidden]') ) { const visibleAt = panelElm.getAttribute000('tyt-visible-at'); if (!visibleAt) { panelElm.setAttribute111('tyt-visible-at', Date.now()); newVisiblePanels.push(panelElm); } allVisiblePanels.push(panelElm); } } if (newVisiblePanels.length >= 1 && allVisiblePanels.length >= 2) { const targetVisible = newVisiblePanels[newVisiblePanels.length - 1]; const actions = []; for (const panelElm of allVisiblePanels) { if (panelElm === targetVisible) continue; actions.push({ panelId: panelElm.getAttribute000('target-id'), toHide: true, }); } if (actions.length >= 1) { ytBtnEgmPanelCore(actions); } } if (allVisiblePanels.length >= 1) { ytdFlexyElm.setAttribute111('tyt-egm-panel_', ''); } else { ytdFlexyElm.removeAttribute000('tyt-egm-panel_'); } newVisiblePanels.length = 0; newVisiblePanels = null; newHiddenPanels.length = 0; newHiddenPanels = null; allVisiblePanels.length = 0; allVisiblePanels = null; }; const checkElementExist = (css, exclude) => { for (const p of document.querySelectorAll(css)) { if (!p.closest(exclude)) return p; } return null; }; let fixInitialTabStateK = 0; const { handleNavigateFactory } = (() => { let isLoadStartListened = false; function extractCommentId(anchor) { const href = anchor.getAttribute('href') || ''; const match = /[&?]lc=([\w_.-]+)/.exec(href); return match ? match[1] : null; } function findCommentIdFromHeader(header) { const anchor = _querySelector.call(header, 'a[href*="lc="]'); return anchor ? extractCommentId(anchor) : null; } function findLinkedCommentByBadge() { const badgeElement = document.querySelector( `#tab-comments ytd-comments ytd-comment-renderer > #linked-comment-badge span:not(:empty)` ); if (!badgeElement) return null; const commentRendererElm = closestFromAnchor.call(badgeElement, 'ytd-comment-renderer'); if (!commentRendererElm) return null; const header = _querySelector.call(commentRendererElm, '#header-author'); if (!header) return null; const commentId = findCommentIdFromHeader(header); if (!commentId) return null; return { lc: commentId, commentRendererElm }; } function findCommentById(commentId) { const anchor = document.querySelector( `#tab-comments ytd-comments ytd-comment-renderer #header-author a[href*="lc=${commentId}"]` ); if (!anchor) return null; const commentRendererElm = closestFromAnchor.call(anchor, 'ytd-comment-renderer'); if (!commentRendererElm) return null; return { lc: commentId, commentRendererElm }; } function findLcComment(lc) { return typeof lc === 'undefined' ? findLinkedCommentByBadge() : findCommentById(lc); } function validateBadgeSwapConditions(r1Data, r2Data) { return ( typeof r1Data.linkedCommentBadge === 'object' && typeof r2Data.linkedCommentBadge === 'undefined' ); } function cleanBadgeTrackingParams(badge) { const cleaned = { ...badge }; if (cleaned?.metadataBadgeRenderer?.trackingParams) { delete cleaned.metadataBadgeRenderer.trackingParams; } return cleaned; } function validateParentCompatibility(v1, v2) { if (v1.parent !== v2.parent) { return false; } const { nodeName } = v2.parent; return nodeName === 'YTD-COMMENTS' || nodeName === 'YTD-ITEM-SECTION-RENDERER'; } function reorderContentsToFront(parentCnt, targetIndex) { const contents = parentCnt.data?.contents; if (!contents) { console.warn('Contents not found in parent'); return; } parentCnt.data = { ...parentCnt.data, contents: [ contents[targetIndex], ...contents.slice(0, targetIndex), ...contents.slice(targetIndex + 1), ], }; } function lcSwapFuncA(targetLcId, currentLcId) { try { const r1 = findLcComment(currentLcId).commentRendererElm; const r2 = findLcComment(targetLcId).commentRendererElm; const r1Data = insp(r1).data; const r2Data = insp(r2).data; if (!validateBadgeSwapConditions(r1Data, r2Data)) { return false; } const badge = cleanBadgeTrackingParams(r1Data.linkedCommentBadge); const v1 = findContentsRenderer(r1); const v2 = findContentsRenderer(r2); if (!validateParentCompatibility(v1, v2)) { return false; } if (v2.index < 0) { return false; } if (v2.parent.nodeName === 'YTD-COMMENT-REPLIES-RENDERER') { return lcSwapFuncB(targetLcId, currentLcId, badge); } reorderContentsToFront(insp(v2.parent), v2.index); return lcSwapFuncB(targetLcId, currentLcId, badge); } catch (e) { console.warn('lcSwapFuncA error:', e); return false; } } function lcSwapFuncB(targetLcId, currentLcId, _p) { let done = 0; try { const r1 = findLcComment(currentLcId).commentRendererElm; const r1cnt = insp(r1); const r2 = findLcComment(targetLcId).commentRendererElm; const r2cnt = insp(r2); const r1d = r1cnt.data; const p = { ..._p }; r1d.linkedCommentBadge = null; delete r1d.linkedCommentBadge; const q = { ...r1d }; q.linkedCommentBadge = null; delete q.linkedCommentBadge; r1cnt.data = { ...q }; r2cnt.data = { ...r2cnt.data, linkedCommentBadge: p }; done = 1; } catch (e) { console.warn(e); } return done === 1; } const loadStartFx = async evt => { const media = (evt || 0).target || 0; if (media.nodeName === 'VIDEO' || media.nodeName === 'AUDIO') { } else return; const newMedia = media; const media1 = common.getMediaElement(0); const media2 = common.getMediaElements(2); if (media1 !== null && media2.length > 0) { if (newMedia !== media1 && media1.paused === false) { if (isVideoPlaying(media1)) { Promise.resolve(newMedia) .then(video => video.paused === false && video.pause()) .catch(console.warn); } } else if (newMedia === media1) { for (const s of media2) { if (s.paused === false) { Promise.resolve(s) .then(secondaryMedia => secondaryMedia.paused === false && secondaryMedia.pause()) .catch(console.warn); break; } } } else { Promise.resolve(media1) .then(video1 => video1.paused === false && video1.pause()) .catch(console.warn); } } }; const getBrowsableEndPoint = req => { let valid = false; let endpoint = req ? req.command : null; if ( endpoint && (endpoint.commandMetadata || 0).webCommandMetadata && endpoint.watchEndpoint ) { const { videoId } = endpoint.watchEndpoint; const { url } = endpoint.commandMetadata.webCommandMetadata; if (typeof videoId === 'string' && typeof url === 'string' && url.indexOf('lc=') > 0) { const m = /^\/watch\?v=([\w_-]+)&lc=([\w_.-]+)$/.exec(url); if (m && m[1] === videoId) { const targetLc = findLcComment(m[2]); const currentLc = targetLc ? findLcComment() : null; if (targetLc && currentLc) { let done = 0; if (targetLc.lc === currentLc.lc) { done = 1; } else if (lcSwapFuncA(targetLc.lc, currentLc.lc)) { done = 1; } if (done === 1) { common.xReplaceState(history.state, url); return; } } } } } if ( endpoint && (endpoint.commandMetadata || 0).webCommandMetadata && endpoint.browseEndpoint && isChannelId(endpoint.browseEndpoint.browseId) ) { valid = true; } else if ( endpoint && (endpoint.browseEndpoint || endpoint.searchEndpoint) && !endpoint.urlEndpoint && !endpoint.watchEndpoint ) { if (endpoint.browseEndpoint && endpoint.browseEndpoint.browseId === 'FEwhat_to_watch') { const playerMedia = common.getMediaElement(1); if (playerMedia && playerMedia.paused === false) valid = true; } else if (endpoint.commandMetadata && endpoint.commandMetadata.webCommandMetadata) { const meta = endpoint.commandMetadata.webCommandMetadata; if (meta && meta.url && meta.webPageType) { valid = true; } } } if (!valid) endpoint = null; return endpoint; }; const shouldUseMiniPlayer = () => { const isSubTypeExist = document.querySelector( 'ytd-page-manager#page-manager > ytd-browse[page-subtype]' ); if (isSubTypeExist) return true; const movie_player = [...document.querySelectorAll('#movie_player')].filter( e => !e.closest('[hidden]') )[0]; if (movie_player) { const media = qsOne(movie_player, 'video[class], audio[class]'); if ( media && media.currentTime > 3 && media.duration - media.currentTime > 3 && media.paused === false ) { return true; } } return false; }; const conditionFulfillment = req => { const command = req ? req.command : null; DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 0801', command); if (!command) return; if (command && (command.commandMetadata || 0).webCommandMetadata && command.watchEndpoint) { } else if ( command && (command.commandMetadata || 0).webCommandMetadata && command.browseEndpoint && isChannelId(command.browseEndpoint.browseId) ) { } else if ( command && (command.browseEndpoint || command.searchEndpoint) && !command.urlEndpoint && !command.watchEndpoint ) { } else { return false; } if (!shouldUseMiniPlayer()) return false; if (pageType !== 'watch') return false; return true; }; let u38 = 0; const fixChannelAboutPopup = async t38 => { let promise = new PromiseExternal(); const f = () => { promise && promise.resolve(); promise = null; }; document.addEventListener('yt-navigate-finish', f, false); await promise.then(); promise = null; document.removeEventListener('yt-navigate-finish', f, false); if (t38 !== u38) return; setTimeout(() => { const currentAbout = [...document.querySelectorAll('ytd-about-channel-renderer')].filter( e => !e.closest('[hidden]') )[0]; let okay = false; if (currentAbout) { const popupContainer = currentAbout.closest('ytd-popup-container'); if (popupContainer) { const cnt = insp(popupContainer); let arr = null; try { arr = cnt.handleGetOpenedPopupsAction_(); } catch {} if (arr && arr.length === 0) okay = true; } else { okay = true; } } else { okay = true; } if (okay) { const descriptionModel = [ ...document.querySelectorAll('yt-description-preview-view-model'), ].filter(e => !e.closest('[hidden]'))[0]; if (descriptionModel) { const button = [...descriptionModel.querySelectorAll('button')].filter( e => !e.closest('[hidden]') && `${e.textContent}`.trim().length > 0 )[0]; if (button) { button.click(); } } } }, 80); }; const createHandleNavigate = handleNavigate => { return function (...args) { const req = args[0]; if (u38 > MAX_ATTRIBUTE_VALUE) u38 = ATTRIBUTE_RESET_VALUE; const t38 = ++u38; const $this = this; const $arguments = args; let endpoint = null; if (conditionFulfillment(req)) { endpoint = getBrowsableEndPoint(req); } if (!endpoint || !shouldUseMiniPlayer()) return handleNavigate.call($this, ...$arguments); const ytdAppElm = document.querySelector('ytd-app'); const ytdAppCnt = insp(ytdAppElm); let watchEndpoint = null; try { watchEndpoint = ytdAppCnt.data.response.currentVideoEndpoint.watchEndpoint || null; } catch { watchEndpoint = null; } if (typeof watchEndpoint !== 'object') watchEndpoint = null; const once = { once: true }; if (watchEndpoint !== null && !('playlistId' in watchEndpoint)) { let wObject = mWeakRef(watchEndpoint); const N = 3; let count = 0; Object.defineProperty(kRef(wObject) || {}, 'playlistId', { get() { count++; if (count === N) { delete this.playlistId; } return '*'; }, set(value) { delete this.playlistId; this.playlistId = value; }, enumerable: false, configurable: true, }); let playlistClearout = null; let timeoutid = 0; Promise.race([ new Promise(r => { timeoutid = setTimeout(r, 4000); }), new Promise(r => { playlistClearout = () => { if (timeoutid > 0) { clearTimeout(timeoutid); timeoutid = 0; } r(); }; document.addEventListener('yt-page-type-changed', playlistClearout, once); }), ]) .then(() => { if (timeoutid !== 0) { playlistClearout && document.removeEventListener('yt-page-type-changed', playlistClearout, once); timeoutid = 0; } playlistClearout = null; count = N - 1; const endpointObj = kRef(wObject); wObject = null; return endpointObj ? endpointObj.playlistId : null; }) .catch(console.warn); } if (!isLoadStartListened) { isLoadStartListened = true; document.addEventListener('loadstart', loadStartFx, true); } const endpointURL = `${endpoint?.commandMetadata?.webCommandMetadata?.url || ''}`; if ( endpointURL && endpointURL.endsWith('/about') && /\/channel\/UC[-_a-zA-Z0-9+=.]{22}\/about/.test(endpointURL) ) { fixChannelAboutPopup(t38); } handleNavigate.call($this, ...$arguments); }; }; return { handleNavigateFactory: createHandleNavigate }; })(); const common = (() => { let mediaModeLock = 0; const MEDIA_TYPE = { UNKNOWN: 0, VIDEO: 1, AUDIO: 2, }; const VIDEO_SELECTORS = [ '#movie_player video[src]', 'ytd-player#ytd-player video[src]', 'ytd-browse[role="main"] video[src]', ]; const AUDIO_BASE = 'audio.video-stream.html5-main-video[src]'; const AUDIO_SELECTOR_TEMPLATES = [ '#movie_player', 'ytd-player#ytd-player', 'ytd-browse[role="main"]', ]; const findMediaElement = () => { return ( document.querySelector('.video-stream.html5-main-video') || document.querySelector('#movie_player video, #movie_player audio') || document.querySelector('body video[src], body audio[src]') ); }; const getMediaTypeFromElement = element => { if (element.nodeName === 'VIDEO') return MEDIA_TYPE.VIDEO; if (element.nodeName === 'AUDIO') return MEDIA_TYPE.AUDIO; return MEDIA_TYPE.UNKNOWN; }; const detectMediaType = () => { const element = findMediaElement(); if (!element) return; mediaModeLock = getMediaTypeFromElement(element); }; const getVideoSelector = i => VIDEO_SELECTORS[i] || VIDEO_SELECTORS[0]; const getAudioSelector = i => { const template = AUDIO_SELECTOR_TEMPLATES[i] || AUDIO_SELECTOR_TEMPLATES[0]; return `${template} ${AUDIO_BASE}`; }; const getMediaElementSelector = i => { if (mediaModeLock === MEDIA_TYPE.UNKNOWN) detectMediaType(); if (!mediaModeLock) return null; return mediaModeLock === MEDIA_TYPE.VIDEO ? getVideoSelector(i) : getAudioSelector(i); }; const replaceHistoryState = (state, url) => { try { history.replaceState(state, '', url); } catch { } }; const getYtdApp = () => { const element = document.querySelector('ytd-app'); const controller = insp(element); return { element, controller }; }; const updateYtdAppState = (state, url) => { if (!state.endpoint) return; try { const { controller } = getYtdApp(); controller?.replaceState(state.endpoint, '', url); } catch { } }; const queryMediaElement = selector => { return selector ? document.querySelector(selector) : null; }; const queryMediaElements = selector => { return selector ? document.querySelectorAll(selector) : []; }; return { xReplaceState(s, u) { replaceHistoryState(s, u); updateYtdAppState(s, u); }, getMediaElement(i) { const selector = getMediaElementSelector(i); return queryMediaElement(selector); }, getMediaElements(i) { const selector = getMediaElementSelector(i); return queryMediaElements(selector); }, }; })(); let inPageRearrange = false; let tmpLastVideoId = ''; const getCurrentVideoId = () => { const ytdFlexyElm = elements.flexy; const ytdFlexyCnt = insp(ytdFlexyElm); if (ytdFlexyCnt && typeof ytdFlexyCnt.videoId === 'string') return ytdFlexyCnt.videoId; if (ytdFlexyElm && typeof ytdFlexyElm.videoId === 'string') return ytdFlexyElm.videoId; console.log('video id not found'); return ''; }; const holdInlineExpanderAlwaysExpanded = inlineExpanderCnt => { console.log('holdInlineExpanderAlwaysExpanded'); if (inlineExpanderCnt.alwaysShowExpandButton === true) { inlineExpanderCnt.alwaysShowExpandButton = false; } if (typeof (inlineExpanderCnt.collapseLabel || 0) === 'string') { inlineExpanderCnt.collapseLabel = ''; } if (typeof (inlineExpanderCnt.expandLabel || 0) === 'string') { inlineExpanderCnt.expandLabel = ''; } if (inlineExpanderCnt.showCollapseButton === true) { inlineExpanderCnt.showCollapseButton = false; } if (inlineExpanderCnt.showExpandButton === true) inlineExpanderCnt.showExpandButton = false; if (inlineExpanderCnt.expandButton instanceof HTMLElement_) { inlineExpanderCnt.expandButton = null; inlineExpanderCnt.expandButton.remove(); } }; const fixInlineExpanderDisplay = inlineExpanderCnt => { try { inlineExpanderCnt.updateIsAttributedExpanded(); } catch (e) { console.warn('[YouTube+] updateIsAttributedExpanded failed:', e); } try { inlineExpanderCnt.updateIsFormattedExpanded(); } catch (e) { console.warn('[YouTube+] updateIsFormattedExpanded failed:', e); } try { inlineExpanderCnt.updateTextOnSnippetTypeChange(); } catch (e) { console.warn('[YouTube+] updateTextOnSnippetTypeChange failed:', e); } try { inlineExpanderCnt.updateStyles(); } catch (e) { console.warn('[YouTube+] updateStyles failed:', e); } }; const fixInlineExpanderMethods = inlineExpanderCnt => { if (inlineExpanderCnt && !inlineExpanderCnt.__$$idncjk8487$$__) { inlineExpanderCnt.__$$idncjk8487$$__ = true; inlineExpanderCnt.updateTextOnSnippetTypeChange = function () { true || (this.isResetMutation && this.mutationCallback()); }; inlineExpanderCnt.isResetMutation = true; fixInlineExpanderDisplay(inlineExpanderCnt); } }; const fixInlineExpanderContent = () => { const mainInfo = getMainInfo(); if (!mainInfo) return; const inlineExpanderElm = mainInfo.querySelector('ytd-text-inline-expander'); const inlineExpanderCnt = insp(inlineExpanderElm); fixInlineExpanderMethods(inlineExpanderCnt); }; const plugin = { minibrowser: { activated: false, toUse: true, activate() { if (this.activated) return; if (!isPassiveArgSupport) return; this.activated = true; const ytdAppElm = document.querySelector('ytd-app'); const ytdAppCnt = insp(ytdAppElm); if (!ytdAppCnt) return; const cProto = ytdAppCnt.constructor.prototype; if (!cProto.handleNavigate) return; if (cProto.handleNavigate.__ma355__) return; cProto.handleNavigate = handleNavigateFactory(cProto.handleNavigate); cProto.handleNavigate.__ma355__ = 1; }, }, autoExpandInfoDesc: { activated: false, toUse: false, mo: null, promiseReady: new PromiseExternal(), moFn(lockId) { if (lockGet['autoExpandInfoDescAttrAsyncLock'] !== lockId) return; const mainInfo = getMainInfo(); if (!mainInfo) return; switch (((mainInfo || 0).nodeName || '').toLowerCase()) { case 'ytd-expander': if (mainInfo.hasAttribute000('collapsed')) { let success = false; try { insp(mainInfo).handleMoreTap(new Event('tap')); success = true; } catch {} if (success) mainInfo.setAttribute111('tyt-no-less-btn', ''); } break; case 'ytd-expandable-video-description-body-renderer': const inlineExpanderElm = mainInfo.querySelector('ytd-text-inline-expander'); const inlineExpanderCnt = insp(inlineExpanderElm); if (inlineExpanderCnt && inlineExpanderCnt.isExpanded === false) { inlineExpanderCnt.isExpanded = true; inlineExpanderCnt.isExpandedChanged(); } break; } }, activate() { if (this.activated) return; this.moFn = this.moFn.bind(this); this.mo = new MutationObserver(() => { Promise.resolve(lockSet['autoExpandInfoDescAttrAsyncLock']) .then(this.moFn) .catch(console.warn); }); this.activated = true; this.promiseReady.resolve(); }, async onMainInfoSet(mainInfo) { await this.promiseReady.then(); if (mainInfo && mainInfo instanceof Element && mainInfo.isConnected) { if (mainInfo.nodeName.toLowerCase() === 'ytd-expander') { try { this.mo.observe(mainInfo, { attributes: true, attributeFilter: ['collapsed', 'attr-8ifv7'], }); } catch (observeError) { console.error( '[YouTube+] Failed to observe mainInfo (expander):', observeError, mainInfo ); } } else { try { this.mo.observe(mainInfo, { attributes: true, attributeFilter: ['attr-8ifv7'] }); } catch (observeError) { console.error('[YouTube+] Failed to observe mainInfo:', observeError, mainInfo); } } try { mainInfo.incAttribute111('attr-8ifv7'); } catch (e) { console.warn('[YouTube+] mainInfo.incAttribute111 failed', e); } } }, }, fullChannelNameOnHover: { activated: false, toUse: true, mo: null, ro: null, promiseReady: new PromiseExternal(), checkResize: 0, mouseEnterFn(evt) { const target = evt ? evt.target : null; if (!(target instanceof HTMLElement_)) return; const metaDataElm = target.closest('ytd-watch-metadata'); metaDataElm.classList.remove('tyt-metadata-hover-resized'); this.checkResize = Date.now() + 300; metaDataElm.classList.add('tyt-metadata-hover'); }, mouseLeaveFn(evt) { const target = evt ? evt.target : null; if (!(target instanceof HTMLElement_)) return; const metaDataElm = target.closest('ytd-watch-metadata'); metaDataElm.classList.remove('tyt-metadata-hover-resized'); metaDataElm.classList.remove('tyt-metadata-hover'); }, moFn(lockId) { if (lockGet['fullChannelNameOnHoverAttrAsyncLock'] !== lockId) return; const uploadInfo = document.querySelector( '#primary.ytd-watch-flexy ytd-watch-metadata #upload-info' ); if (!uploadInfo) return; const evtOpt = { passive: true, capture: false }; uploadInfo.removeEventListener('pointerenter', this.mouseEnterFn, evtOpt); uploadInfo.removeEventListener('pointerleave', this.mouseLeaveFn, evtOpt); uploadInfo.addEventListener('pointerenter', this.mouseEnterFn, evtOpt); uploadInfo.addEventListener('pointerleave', this.mouseLeaveFn, evtOpt); }, async onNavigateFinish() { await this.promiseReady.then(); const uploadInfo = document.querySelector( '#primary.ytd-watch-flexy ytd-watch-metadata #upload-info' ); if (!uploadInfo) return; if (uploadInfo && uploadInfo instanceof Element && uploadInfo.isConnected) { try { this.mo.observe(uploadInfo, { attributes: true, attributeFilter: ['hidden', 'attr-3wb0k'], }); } catch (observeError) { console.error('[YouTube+] Failed to observe uploadInfo:', observeError, uploadInfo); } try { uploadInfo.incAttribute111('attr-3wb0k'); } catch (e) { console.warn('[YouTube+] uploadInfo.incAttribute111 failed', e); } try { if (this.ro && typeof this.ro.observe === 'function') this.ro.observe(uploadInfo); } catch (observeError) { console.error( '[YouTube+] Failed to observe uploadInfo with ResizeObserver:', observeError, uploadInfo ); } } }, activate() { if (this.activated) return; if (!isPassiveArgSupport) return; this.activated = true; this.mouseEnterFn = this.mouseEnterFn.bind(this); this.mouseLeaveFn = this.mouseLeaveFn.bind(this); this.moFn = this.moFn.bind(this); this.mo = new MutationObserver(() => { Promise.resolve(lockSet['fullChannelNameOnHoverAttrAsyncLock']) .then(this.moFn) .catch(console.warn); }); this.ro = new ResizeObserver(mutations => { if (Date.now() > this.checkResize) return; for (const mutation of mutations) { const uploadInfo = mutation.target; if (uploadInfo && mutation.contentRect.width > 0 && mutation.contentRect.height > 0) { const metaDataElm = uploadInfo.closest('ytd-watch-metadata'); if (metaDataElm.classList.contains('tyt-metadata-hover')) { metaDataElm.classList.add('tyt-metadata-hover-resized'); } break; } } }); this.promiseReady.resolve(); }, }, 'external.ytlstm': { activated: false, toUse: true, activate() { if (this.activated) return; this.activated = true; document.documentElement.classList.add('external-ytlstm'); }, }, }; if (sessionStorage.__$$tmp_UseAutoExpandInfoDesc$$__) plugin.autoExpandInfoDesc.toUse = true; const __attachedSymbol__ = Symbol(); const makeInitAttached = tag => { const inPageRearrange_ = inPageRearrange; inPageRearrange = false; for (const elm of document.querySelectorAll(`${tag}`)) { const cnt = insp(elm) || 0; if (typeof cnt.attached498 === 'function' && !elm[__attachedSymbol__]) { Promise.resolve(elm).then(eventMap[`${tag}::attached`]).catch(console.warn); } } inPageRearrange = inPageRearrange_; }; const getGeneralChatElement = async () => { for (let i = 2; i-- > 0; ) { const t = document.querySelector( '#columns.style-scope.ytd-watch-flexy ytd-live-chat-frame#chat' ); if (t instanceof Element) return t; if (i > 0) { console.log('ytd-live-chat-frame::attached - delayPn(200)'); await delayPn(200); } } return null; }; const nsTemplateObtain = () => { let nsTemplate = document.querySelector('ytd-watch-flexy noscript[ns-template]'); if (!nsTemplate) { nsTemplate = document.createElement('noscript'); nsTemplate.setAttribute('ns-template', ''); document.querySelector('ytd-watch-flexy').appendChild(nsTemplate); } return nsTemplate; }; const isPageDOM = (elm, selector) => { if (!elm || !(elm instanceof Element) || !elm.nodeName) return false; if (!elm.closest(selector)) return false; if (elm.isConnected !== true) return false; return true; }; const invalidFlexyParent = hostElement => { if (hostElement instanceof HTMLElement) { const hasFlexyParent = HTMLElement.prototype.closest.call(hostElement, 'ytd-watch-flexy'); if (!hasFlexyParent) return true; const currentFlexy = elements.flexy; if (currentFlexy && currentFlexy !== hasFlexyParent) return true; } return false; }; let headerMutationObserver = null; let headerMutationTmpNode = null; const eventMap = { ceHack: () => { mLoaded.flag |= 2; document.documentElement.setAttribute111('tabview-loaded', mLoaded.makeString()); retrieveCE('ytd-watch-flexy') .then(eventMap['ytd-watch-flexy::defined']) .catch(console.warn); retrieveCE('ytd-expander').then(eventMap['ytd-expander::defined']).catch(console.warn); retrieveCE('ytd-watch-next-secondary-results-renderer') .then(eventMap['ytd-watch-next-secondary-results-renderer::defined']) .catch(err => console.warn( '[YouTube+] retrieveCE ytd-watch-next-secondary-results-renderer failed:', err ) ); retrieveCE('ytd-comments-header-renderer') .then(eventMap['ytd-comments-header-renderer::defined']) .catch(err => console.warn('[YouTube+] retrieveCE ytd-comments-header-renderer failed:', err) ); retrieveCE('ytd-live-chat-frame') .then(eventMap['ytd-live-chat-frame::defined']) .catch(err => console.warn('[YouTube+] retrieveCE ytd-live-chat-frame failed:', err)); retrieveCE('ytd-comments') .then(eventMap['ytd-comments::defined']) .catch(err => console.warn('[YouTube+] retrieveCE ytd-comments failed:', err)); retrieveCE('ytd-engagement-panel-section-list-renderer') .then(eventMap['ytd-engagement-panel-section-list-renderer::defined']) .catch(err => console.warn( '[YouTube+] retrieveCE ytd-engagement-panel-section-list-renderer failed:', err ) ); retrieveCE('ytd-watch-metadata') .then(eventMap['ytd-watch-metadata::defined']) .catch(err => console.warn('[YouTube+] retrieveCE ytd-watch-metadata failed:', err)); retrieveCE('ytd-playlist-panel-renderer') .then(eventMap['ytd-playlist-panel-renderer::defined']) .catch(err => console.warn('[YouTube+] retrieveCE ytd-playlist-panel-renderer failed:', err) ); retrieveCE('ytd-expandable-video-description-body-renderer') .then(eventMap['ytd-expandable-video-description-body-renderer::defined']) .catch(err => console.warn( '[YouTube+] retrieveCE ytd-expandable-video-description-body-renderer failed:', err ) ); }, fixForTabDisplay: isResize => { bFixForResizedTabLater = false; for (const element of document.querySelectorAll('[io-intersected]')) { const cnt = insp(element); if (element instanceof HTMLElement_ && typeof cnt.calculateCanCollapse === 'function') { try { cnt.calculateCanCollapse(true); } catch (e) { console.warn('[YouTube+] calculateCanCollapse failed:', e); } } } if (!isResize && lastTab === '#tab-info') { for (const element of document.querySelectorAll( '#tab-info ytd-video-description-infocards-section-renderer, #tab-info yt-chip-cloud-renderer, #tab-info ytd-horizontal-card-list-renderer, #tab-info yt-horizontal-list-renderer' )) { const cnt = insp(element); if (element instanceof HTMLElement_ && typeof cnt.notifyResize === 'function') { try { cnt.notifyResize(); } catch (e) { console.warn('[YouTube+] notifyResize failed for tab-info:', e); } } } for (const element of document.querySelectorAll('#tab-info ytd-text-inline-expander')) { const cnt = insp(element); if (element instanceof HTMLElement_ && typeof cnt.resize === 'function') { cnt.resize(false); } fixInlineExpanderDisplay(cnt); } } if (!isResize && typeof lastTab === 'string' && lastTab.startsWith('#tab-')) { const tabContent = document.querySelector('.tab-content-cld:not(.tab-content-hidden)'); if (tabContent) { const renderers = tabContent.querySelectorAll('yt-chip-cloud-renderer'); for (const renderer of renderers) { const cnt = insp(renderer); if (typeof cnt.notifyResize === 'function') { try { cnt.notifyResize(); } catch (e) { console.warn('[YouTube+] notifyResize failed for renderer:', e); } } } } } }, 'ytd-watch-flexy::defined': cProto => { if ( !cProto.updateChatLocation498 && typeof cProto.updateChatLocation === 'function' && cProto.updateChatLocation.length === 0 ) { cProto.updateChatLocation498 = cProto.updateChatLocation; cProto.updateChatLocation = updateChatLocation498; } }, 'ytd-watch-next-secondary-results-renderer::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-watch-next-secondary-results-renderer::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-watch-next-secondary-results-renderer::detached']) .catch(console.warn); } return this.detached498(); }; } makeInitAttached('ytd-watch-next-secondary-results-renderer'); }, 'ytd-watch-next-secondary-results-renderer::attached': hostElement => { if (invalidFlexyParent(hostElement)) return; DEBUG_5084 && console.log(5084, 'ytd-watch-next-secondary-results-renderer::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if ( hostElement instanceof HTMLElement_ && hostElement.matches('#columns #related ytd-watch-next-secondary-results-renderer') && !hostElement.matches( '#right-tabs ytd-watch-next-secondary-results-renderer, [hidden] ytd-watch-next-secondary-results-renderer' ) ) { elements.related = hostElement.closest('#related'); hostElement.setAttribute111('tyt-videos-list', ''); } }, 'ytd-watch-next-secondary-results-renderer::detached': hostElement => { DEBUG_5084 && console.log(5084, 'ytd-watch-next-secondary-results-renderer::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; if (hostElement.hasAttribute000('tyt-videos-list')) { elements.related = null; hostElement.removeAttribute000('tyt-videos-list'); } console.log('ytd-watch-next-secondary-results-renderer::detached', hostElement); }, settingCommentsVideoId: hostElement => { if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } const cnt = insp(hostElement); const { comments: commentsArea } = elements; if ( commentsArea !== hostElement || hostElement.isConnected !== true || cnt.isAttached !== true || !cnt.data || cnt.hidden !== false ) { return; } const { flexy: ytdFlexyElm } = elements; const ytdFlexyCnt = ytdFlexyElm ? insp(ytdFlexyElm) : null; if (ytdFlexyCnt && ytdFlexyCnt.videoId) { hostElement.setAttribute111('tyt-comments-video-id', ytdFlexyCnt.videoId); } else { hostElement.removeAttribute000('tyt-comments-video-id'); } }, checkCommentsShouldBeHidden: lockId => { if (lockGet['checkCommentsShouldBeHiddenLock'] !== lockId) return; const { comments: commentsArea, flexy: ytdFlexyElm } = elements; if (commentsArea && ytdFlexyElm && !commentsArea.hasAttribute000('hidden')) { const ytdFlexyCnt = insp(ytdFlexyElm); if (typeof ytdFlexyCnt.videoId === 'string') { const commentsVideoId = commentsArea.getAttribute('tyt-comments-video-id'); if (commentsVideoId && commentsVideoId !== ytdFlexyCnt.videoId) { commentsArea.setAttribute111('hidden', ''); } } } }, 'ytd-comments::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-comments::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-comments::detached']) .catch(console.warn); } return this.detached498(); }; } cProto._createPropertyObserver('data', '_dataChanged498', undefined); cProto._dataChanged498 = function () { Promise.resolve(this.hostElement) .then(eventMap['ytd-comments::_dataChanged498']) .catch(console.warn); }; makeInitAttached('ytd-comments'); }, 'ytd-comments::_dataChanged498': hostElement => { if (!hostElement.hasAttribute000('tyt-comments-area')) return; let commentsDataStatus = 0; const cnt = insp(hostElement); const data = cnt ? cnt.data : null; const contents = data ? data.contents : null; if (data) { if (contents && contents.length === 1 && contents[0].messageRenderer) { commentsDataStatus = 2; } if (contents && contents.length > 1 && contents[0].commentThreadRenderer) { commentsDataStatus = 1; } } if (commentsDataStatus) { hostElement.setAttribute111('tyt-comments-data-status', commentsDataStatus); } else { hostElement.removeAttribute000('tyt-comments-data-status'); } Promise.resolve(hostElement).then(eventMap['settingCommentsVideoId']).catch(console.warn); }, 'ytd-comments::attached': async hostElement => { if (invalidFlexyParent(hostElement)) return; DEBUG_5084 && console.log(5084, 'ytd-comments::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if (!hostElement || hostElement.id !== 'comments') return; elements.comments = hostElement; console.log('ytd-comments::attached'); Promise.resolve(hostElement).then(eventMap['settingCommentsVideoId']).catch(console.warn); if (hostElement && hostElement instanceof Element && hostElement.isConnected) { try { aoComment.observe(hostElement, { attributes: true }); } catch (observeError) { console.error('[YouTube+] Failed to observe comments element:', observeError); } } hostElement.setAttribute111('tyt-comments-area', ''); const lockId = lockSet['rightTabReadyLock02']; await rightTabsProvidedPromise.then(); if (lockGet['rightTabReadyLock02'] !== lockId) return; if (elements.comments !== hostElement) return; if (hostElement.isConnected === false) return; DEBUG_5085 && console.log(7932, 'comments'); if (hostElement && !hostElement.closest('#right-tabs')) { document.querySelector('#tab-comments').assignChildren111(null, hostElement, null); } else { const shouldTabVisible = elements.comments && elements.comments.closest('#tab-comments') && !elements.comments.closest('[hidden]'); document .querySelector('[tyt-tab-content="#tab-comments"]') .classList.toggle('tab-btn-hidden', !shouldTabVisible); Promise.resolve(lockSet['removeKeepCommentsScrollerLock']) .then(removeKeepCommentsScroller) .catch(console.warn); } TAB_AUTO_SWITCH_TO_COMMENTS && switchToTab('#tab-comments'); }, 'ytd-comments::detached': hostElement => { DEBUG_5084 && console.log(5084, 'ytd-comments::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; if (hostElement.hasAttribute000('tyt-comments-area')) { hostElement.removeAttribute000('tyt-comments-area'); aoComment.disconnect(); aoComment.takeRecords(); elements.comments = null; document .querySelector('[tyt-tab-content="#tab-comments"]') .classList.add('tab-btn-hidden'); Promise.resolve(lockSet['removeKeepCommentsScrollerLock']) .then(removeKeepCommentsScroller) .catch(console.warn); } }, 'ytd-comments-header-renderer::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-comments-header-renderer::attached']) .catch(console.warn); } Promise.resolve(this.hostElement) .then(eventMap['ytd-comments-header-renderer::dataChanged']) .catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-comments-header-renderer::detached']) .catch(console.warn); } return this.detached498(); }; } if (!cProto.dataChanged498 && typeof cProto.dataChanged === 'function') { cProto.dataChanged498 = cProto.dataChanged; cProto.dataChanged = function () { Promise.resolve(this.hostElement) .then(eventMap['ytd-comments-header-renderer::dataChanged']) .catch(console.warn); return this.dataChanged498(); }; } makeInitAttached('ytd-comments-header-renderer'); }, 'ytd-comments-header-renderer::attached': hostElement => { if (invalidFlexyParent(hostElement)) return; DEBUG_5084 && console.log(5084, 'ytd-comments-header-renderer::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if (!hostElement || !hostElement.classList.contains('ytd-item-section-renderer')) return; const targetElement = document.querySelector( '[tyt-comments-area] ytd-comments-header-renderer' ); if (hostElement === targetElement) { hostElement.setAttribute111('tyt-comments-header-field', ''); } else { const { parentNode } = hostElement; if ( parentNode instanceof HTMLElement_ && parentNode.querySelector('[tyt-comments-header-field]') ) { hostElement.setAttribute111('tyt-comments-header-field', ''); } } }, 'ytd-comments-header-renderer::detached': hostElement => { DEBUG_5084 && console.log(5084, 'ytd-comments-header-renderer::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; if (hostElement.hasAttribute000('field-of-cm-count')) { hostElement.removeAttribute000('field-of-cm-count'); const cmCount = document.querySelector('#tyt-cm-count'); if ( cmCount && !document.querySelector('#tab-comments ytd-comments-header-renderer[field-of-cm-count]') ) { cmCount.textContent = ''; } } if (hostElement.hasAttribute000('tyt-comments-header-field')) { hostElement.removeAttribute000('tyt-comments-header-field'); } }, 'ytd-comments-header-renderer::dataChanged': hostElement => { if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } const ytdFlexyElm = elements.flexy; let b = false; const cnt = insp(hostElement); if ( cnt && hostElement.closest('#tab-comments') && document.querySelector('#tab-comments ytd-comments-header-renderer') === hostElement ) { b = true; } else if ( hostElement instanceof HTMLElement_ && hostElement.parentNode instanceof HTMLElement_ && hostElement.parentNode.querySelector('[tyt-comments-header-field]') ) { b = true; } if (b) { hostElement.setAttribute111('tyt-comments-header-field', ''); ytdFlexyElm && ytdFlexyElm.removeAttribute000('tyt-comment-disabled'); } if ( hostElement.hasAttribute000('tyt-comments-header-field') && hostElement.isConnected === true ) { if (!headerMutationObserver) { headerMutationObserver = new MutationObserver( eventMap['ytd-comments-header-renderer::deferredCounterUpdate'] ); } try { const hdrTarget = hostElement && hostElement.parentNode; if ( headerMutationObserver && hdrTarget && (hdrTarget instanceof Element || hdrTarget instanceof Node) ) { headerMutationObserver.observe(hdrTarget, { subtree: false, childList: true, }); } } catch (observeError) { console.error( '[YouTube+] Failed to observe header parent node:', observeError, hostElement && hostElement.parentNode ); } if (!headerMutationTmpNode) { headerMutationTmpNode = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); } const tmpNode = headerMutationTmpNode; hostElement.insertAdjacentElement('afterend', tmpNode); tmpNode.remove(); } }, 'ytd-comments-header-renderer::deferredCounterUpdate': () => { const nodes = document.querySelectorAll( '#tab-comments ytd-comments-header-renderer[class]' ); if (nodes.length === 1) { const hostElement = nodes[0]; const cnt = insp(hostElement); const { data } = cnt; if (!data) return; let ez = ''; if ( data.commentsCount && data.commentsCount.runs && data.commentsCount.runs.length >= 1 ) { let max = -1; const z = data.commentsCount.runs .map(e => { const c = e.text.replace(/\D+/g, '').length; if (c > max) max = c; return [e.text, c]; }) .filter(a => a[1] === max); if (z.length >= 1) { ez = z[0][0]; } } else if (data.countText && data.countText.runs && data.countText.runs.length >= 1) { let max = -1; const z = data.countText.runs .map(e => { const c = e.text.replace(/\D+/g, '').length; if (c > max) max = c; return [e.text, c]; }) .filter(a => a[1] === max); if (z.length >= 1) { ez = z[0][0]; } } const cmCount = document.querySelector('#tyt-cm-count'); if (ez) { hostElement.setAttribute111('field-of-cm-count', ''); cmCount && (cmCount.textContent = ez.trim()); } else { hostElement.removeAttribute000('field-of-cm-count'); cmCount && (cmCount.textContent = ''); console.warn('no text for #tyt-cm-count'); } } }, 'ytd-expander::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-expander::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-expander::detached']) .catch(console.warn); } return this.detached498(); }; } if (!cProto.calculateCanCollapse498 && typeof cProto.calculateCanCollapse === 'function') { cProto.calculateCanCollapse498 = cProto.calculateCanCollapse; cProto.calculateCanCollapse = funcCanCollapse; } if (!cProto.childrenChanged498 && typeof cProto.childrenChanged === 'function') { cProto.childrenChanged498 = cProto.childrenChanged; cProto.childrenChanged = function () { Promise.resolve(this.hostElement) .then(eventMap['ytd-expander::childrenChanged']) .catch(console.warn); return this.childrenChanged498(); }; } makeInitAttached('ytd-expander'); }, 'ytd-expander::childrenChanged': hostElement => { if ( hostElement instanceof Node && hostElement.hasAttribute000('hidden') && hostElement.hasAttribute000('tyt-main-info') && hostElement.firstElementChild ) { hostElement.removeAttribute('hidden'); } }, 'ytd-expandable-video-description-body-renderer::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-expandable-video-description-body-renderer::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-expandable-video-description-body-renderer::detached']) .catch(console.warn); } return this.detached498(); }; } makeInitAttached('ytd-expandable-video-description-body-renderer'); }, 'ytd-expandable-video-description-body-renderer::attached': async hostElement => { if ( hostElement instanceof HTMLElement_ && isPageDOM(hostElement, '[tyt-info-renderer]') && !hostElement.matches('[tyt-main-info]') ) { elements.infoExpander = hostElement; console.log(128384, elements.infoExpander); infoExpanderElementProvidedPromise.resolve(); hostElement.setAttribute111('tyt-main-info', ''); if (plugin.autoExpandInfoDesc.toUse) { plugin.autoExpandInfoDesc.onMainInfoSet(hostElement); } const lockId = lockSet['rightTabReadyLock03']; await rightTabsProvidedPromise.then(); if (lockGet['rightTabReadyLock03'] !== lockId) return; if (elements.infoExpander !== hostElement) return; if (hostElement.isConnected === false) return; console.log(7932, 'infoExpander'); elements.infoExpander.classList.add('tyt-main-info'); const { infoExpander } = elements; const inlineExpanderElm = infoExpander.querySelector('ytd-text-inline-expander'); if (inlineExpanderElm) { const mo = new MutationObserver(() => { const p = document.querySelector('#tab-info ytd-text-inline-expander'); sessionStorage.__$$tmp_UseAutoExpandInfoDesc$$__ = p && p.hasAttribute('is-expanded') ? '1' : ''; if (p) fixInlineExpanderContent(); }); mo.observe(inlineExpanderElm, { attributes: ['is-expanded', 'attr-6v8qu', 'hidden'], subtree: true, }); inlineExpanderElm.incAttribute111('attr-6v8qu'); const cnt = insp(inlineExpanderElm); if (cnt) fixInlineExpanderDisplay(cnt); } if (infoExpander && !infoExpander.closest('#right-tabs')) { document.querySelector('#tab-info').assignChildren111(null, infoExpander, null); } else if (document.querySelector('[tyt-tab-content="#tab-info"]')) { const shouldTabVisible = elements.infoExpander && elements.infoExpander.closest('#tab-info'); document .querySelector('[tyt-tab-content="#tab-info"]') .classList.toggle('tab-btn-hidden', !shouldTabVisible); } Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); } DEBUG_5084 && console.log(5084, 'ytd-expandable-video-description-body-renderer::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if (isPageDOM(hostElement, '#tab-info [tyt-main-info]')) { } else if (!hostElement.closest('#tab-info')) { const bodyRenderer = hostElement; let bodyRendererNew = document.querySelector( 'ytd-expandable-video-description-body-renderer[tyt-info-renderer]' ); if (!bodyRendererNew) { bodyRendererNew = document.createElement( 'ytd-expandable-video-description-body-renderer' ); bodyRendererNew.setAttribute('tyt-info-renderer', ''); nsTemplateObtain().appendChild(bodyRendererNew); } const cnt = insp(bodyRendererNew); cnt.data = { ...insp(bodyRenderer).data }; const inlineExpanderElm = bodyRendererNew.querySelector('ytd-text-inline-expander'); const inlineExpanderCnt = insp(inlineExpanderElm); fixInlineExpanderMethods(inlineExpanderCnt); elements.infoExpanderRendererBack = bodyRenderer; elements.infoExpanderRendererFront = bodyRendererNew; bodyRenderer.setAttribute('tyt-info-renderer-back', ''); bodyRendererNew.setAttribute('tyt-info-renderer-front', ''); } }, 'ytd-expandable-video-description-body-renderer::detached': async hostElement => { if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; if (hostElement.hasAttribute000('tyt-main-info')) { DEBUG_5084 && console.log(5084, 'ytd-expandable-video-description-body-renderer::detached'); elements.infoExpander = null; hostElement.removeAttribute000('tyt-main-info'); } }, 'ytd-expander::attached': async hostElement => { if (invalidFlexyParent(hostElement)) return; if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if ( hostElement instanceof HTMLElement_ && hostElement.matches('[tyt-comments-area] #contents ytd-expander#expander') && !hostElement.matches('[hidden] ytd-expander#expander') ) { hostElement.setAttribute111('tyt-content-comment-entry', ''); try { if ( ioComment && hostElement && (hostElement instanceof Element || hostElement instanceof Node) ) { ioComment.observe(hostElement); } } catch (observeError) { console.error( '[YouTube+] Failed to observe expander (ioComment):', observeError, hostElement ); } } }, 'ytd-expander::detached': hostElement => { if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; if (hostElement.hasAttribute000('tyt-content-comment-entry')) { ioComment.unobserve(hostElement); hostElement.removeAttribute000('tyt-content-comment-entry'); } else if (hostElement.hasAttribute000('tyt-main-info')) { DEBUG_5084 && console.log(5084, 'ytd-expander::detached'); elements.infoExpander = null; hostElement.removeAttribute000('tyt-main-info'); } }, 'ytd-live-chat-frame::defined': cProto => { let lastDomAction = 0; if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { lastDomAction = Date.now(); if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-live-chat-frame::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { lastDomAction = Date.now(); if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-live-chat-frame::detached']) .catch(console.warn); } return this.detached498(); }; } if ( typeof cProto.urlChanged === 'function' && !cProto.urlChanged66 && !cProto.urlChangedAsync12 && cProto.urlChanged.length === 0 ) { cProto.urlChanged66 = cProto.urlChanged; let ath = 0; cProto.urlChangedAsync12 = async function () { await this.__urlChangedAsyncT689__; ath = (ath & 1073741823) + 1; const t = ath; const chatframe = this.chatframe || (this.$ || 0).chatframe || 0; if (chatframe instanceof HTMLIFrameElement) { if (chatframe.contentDocument === null) { await Promise.resolve('#').catch(console.warn); if (t !== ath) return; } await new Promise(resolve => setTimeout_(resolve, 1)).catch(console.warn); if (t !== ath) return; const isBlankPage = !this.data || this.collapsed; const p1 = new Promise(resolve => setTimeout_(resolve, 706)).catch(console.warn); const p2 = new Promise(resolve => { new IntersectionObserver((entries, observer) => { for (const entry of entries) { const rect = entry.boundingClientRect || 0; if (isBlankPage || (rect.width > 0 && rect.height > 0)) { observer.disconnect(); resolve('#'); break; } } }).observe && (function () { try { if ( chatframe && (chatframe instanceof Element || chatframe instanceof Node) ) { new IntersectionObserver((entries, observer) => { for (const entry of entries) { const rect = entry.boundingClientRect || 0; if (isBlankPage || (rect.width > 0 && rect.height > 0)) { observer.disconnect(); resolve('#'); break; } } }).observe(chatframe); } else { resolve('#'); } } catch (observeError) { console.error( '[YouTube+] Failed to observe chatframe with IntersectionObserver:', observeError, chatframe ); try { resolve('#'); } catch {} } })(); }).catch(console.warn); await Promise.race([p1, p2]); if (t !== ath) return; } this.urlChanged66(); }; cProto.urlChanged = function () { this.__urlChangedAsyncT688__ = (this.__urlChangedAsyncT688__ & 1073741823) + 1; const t = this.__urlChangedAsyncT688__; nextBrowserTick(() => { if (t !== this.__urlChangedAsyncT688__) return; this.urlChangedAsync12(); }); }; } makeInitAttached('ytd-live-chat-frame'); }, 'ytd-live-chat-frame::attached': async hostElement => { if (invalidFlexyParent(hostElement)) return; DEBUG_5084 && console.log(5084, 'ytd-live-chat-frame::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if (!hostElement || hostElement.id !== 'chat') return; console.log('ytd-live-chat-frame::attached'); const lockId = lockSet['ytdLiveAttachedLock']; const chatElem = await getGeneralChatElement(); if (lockGet['ytdLiveAttachedLock'] !== lockId) return; if (chatElem === hostElement) { elements.chat = chatElem; if (chatElem && chatElem instanceof Element && chatElem.isConnected) { try { aoChat.observe(chatElem, { attributes: true }); } catch (observeError) { console.error('[YouTube+] Failed to observe chat element:', observeError); } } const isFlexyReady = elements.flexy instanceof Element; chatElem.setAttribute111('tyt-active-chat-frame', isFlexyReady ? 'CF' : 'C'); const chatContainer = chatElem ? chatElem.closest('#chat-container') || chatElem : null; if (chatContainer && !chatContainer.hasAttribute000('tyt-chat-container')) { for (const p of document.querySelectorAll('[tyt-chat-container]')) { p.removeAttribute000('[tyt-chat-container]'); } chatContainer.setAttribute111('tyt-chat-container', ''); } const cnt = insp(hostElement); const q = cnt.__urlChangedAsyncT688__; cnt.__urlChangedAsyncT689__ = new PromiseExternal(); const p = cnt.__urlChangedAsyncT689__; setTimeout_(() => { if (p !== cnt.__urlChangedAsyncT689__) return; if (cnt.isAttached === true && hostElement.isConnected === true) { p.resolve(); if (q === cnt.__urlChangedAsyncT688__) { cnt.urlChanged(); } } }, 320); Promise.resolve(lockSet['layoutFixLock']).then(layoutFix); } else { console.log('Issue found in ytd-live-chat-frame::attached', chatElem, hostElement); } }, 'ytd-live-chat-frame::detached': hostElement => { DEBUG_5084 && console.log(5084, 'ytd-live-chat-frame::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; console.log('ytd-live-chat-frame::detached'); if (hostElement.hasAttribute000('tyt-active-chat-frame')) { aoChat.disconnect(); aoChat.takeRecords(); hostElement.removeAttribute000('tyt-active-chat-frame'); elements.chat = null; const ytdFlexyElm = elements.flexy; if (ytdFlexyElm) { ytdFlexyElm.removeAttribute000('tyt-chat-collapsed'); ytdFlexyElm.setAttribute111('tyt-chat', ''); } } }, 'ytd-engagement-panel-section-list-renderer::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-engagement-panel-section-list-renderer::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-engagement-panel-section-list-renderer::detached']) .catch(console.warn); } return this.detached498(); }; } makeInitAttached('ytd-engagement-panel-section-list-renderer'); }, 'ytd-engagement-panel-section-list-renderer::bindTarget': hostElement => { if ( hostElement.matches( '#panels.ytd-watch-flexy > ytd-engagement-panel-section-list-renderer[target-id][visibility]' ) ) { hostElement.setAttribute111('tyt-egm-panel', ''); Promise.resolve(lockSet['updateEgmPanelsLock']).then(updateEgmPanels).catch(console.warn); if (hostElement && hostElement instanceof Element && hostElement.isConnected) { try { aoEgmPanels.observe(hostElement, { attributes: true, attributeFilter: ['visibility', 'hidden'], }); } catch (observeError) { console.error('[YouTube+] Failed to observe engagement panel:', observeError); } } } }, 'ytd-engagement-panel-section-list-renderer::attached': hostElement => { if (invalidFlexyParent(hostElement)) return; DEBUG_5084 && console.log(5084, 'ytd-engagement-panel-section-list-renderer::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if ( !hostElement.matches( '#panels.ytd-watch-flexy > ytd-engagement-panel-section-list-renderer' ) ) { return; } if (hostElement.hasAttribute000('target-id') && hostElement.hasAttribute000('visibility')) { Promise.resolve(hostElement) .then(eventMap['ytd-engagement-panel-section-list-renderer::bindTarget']) .catch(console.warn); } else { hostElement.setAttribute000('tyt-egm-panel-jclmd', ''); moEgmPanelReady.observe(hostElement, { attributes: true, attributeFilter: ['visibility', 'target-id'], }); } }, 'ytd-engagement-panel-section-list-renderer::detached': hostElement => { DEBUG_5084 && console.log(5084, 'ytd-engagement-panel-section-list-renderer::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; if (hostElement.hasAttribute000('tyt-egm-panel')) { hostElement.removeAttribute000('tyt-egm-panel'); Promise.resolve(lockSet['updateEgmPanelsLock']).then(updateEgmPanels).catch(console.warn); } else if (hostElement.hasAttribute000('tyt-egm-panel-jclmd')) { hostElement.removeAttribute000('tyt-egm-panel-jclmd'); moEgmPanelReadyClearFn(); } }, 'ytd-watch-metadata::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-watch-metadata::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-watch-metadata::detached']) .catch(console.warn); } return this.detached498(); }; } makeInitAttached('ytd-watch-metadata'); }, 'ytd-watch-metadata::attached': hostElement => { if (invalidFlexyParent(hostElement)) return; DEBUG_5084 && console.log(5084, 'ytd-watch-metadata::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; if (plugin.fullChannelNameOnHover.activated) { plugin.fullChannelNameOnHover.onNavigateFinish(); } }, 'ytd-watch-metadata::detached': hostElement => { DEBUG_5084 && console.log(5084, 'ytd-watch-metadata::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected === false) { } }, 'ytd-playlist-panel-renderer::defined': cProto => { if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-playlist-panel-renderer::attached']) .catch(console.warn); } return this.attached498(); }; } if (!cProto.detached498 && typeof cProto.detached === 'function') { cProto.detached498 = cProto.detached; cProto.detached = function () { if (!inPageRearrange) { Promise.resolve(this.hostElement) .then(eventMap['ytd-playlist-panel-renderer::detached']) .catch(console.warn); } return this.detached498(); }; } makeInitAttached('ytd-playlist-panel-renderer'); }, 'ytd-playlist-panel-renderer::attached': hostElement => { if (invalidFlexyParent(hostElement)) return; DEBUG_5084 && console.log(5084, 'ytd-playlist-panel-renderer::attached'); if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true; if ( !(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest('noscript') ) { return; } if (hostElement.isConnected !== true) return; elements.playlist = hostElement; if (hostElement && hostElement instanceof Element && hostElement.isConnected) { try { aoPlayList.observe(hostElement, { attributes: true, attributeFilter: ['hidden', 'collapsed', 'attr-1y6nu'], }); } catch (observeError) { console.error('[YouTube+] Failed to observe playlist element:', observeError); } } hostElement.incAttribute111('attr-1y6nu'); }, 'ytd-playlist-panel-renderer::detached': hostElement => { DEBUG_5084 && console.log(5084, 'ytd-playlist-panel-renderer::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected === false) { } }, _yt_playerProvided: () => { mLoaded.flag |= 4; document.documentElement.setAttribute111('tabview-loaded', mLoaded.makeString()); }, relatedElementProvided: target => { if (target.closest('[hidden]')) return; elements.related = target; console.log('relatedElementProvided'); videosElementProvidedPromise.resolve(); }, onceInfoExpanderElementProvidedPromised: () => { console.log('hide-default-text-inline-expander'); const ytdFlexyElm = elements.flexy; if (ytdFlexyElm) { ytdFlexyElm.setAttribute111('hide-default-text-inline-expander', ''); } }, refreshSecondaryInner: lockId => { if (lockGet['refreshSecondaryInnerLock'] !== lockId) return; const ytdFlexyElm = elements.flexy; if ( ytdFlexyElm && ytdFlexyElm.matches( 'ytd-watch-flexy[theater][full-bleed-player]:not([full-bleed-no-max-width-columns])' ) ) { ytdFlexyElm.setAttribute111('full-bleed-no-max-width-columns', ''); } const { related } = elements; if (related && related.isConnected && !related.closest('#right-tabs #tab-videos')) { document.querySelector('#tab-videos').assignChildren111(null, related, null); } const { infoExpander } = elements; if ( infoExpander && infoExpander.isConnected && !infoExpander.closest('#right-tabs #tab-info') ) { document.querySelector('#tab-info').assignChildren111(null, infoExpander, null); } else { } const commentsArea = elements.comments; if (commentsArea) { const { isConnected } = commentsArea; if (isConnected && !commentsArea.closest('#right-tabs #tab-comments')) { const tab = document.querySelector('#tab-comments'); tab.assignChildren111(null, commentsArea, null); } else { } } }, 'yt-navigate-finish': _evt => { const ytdAppElm = document.querySelector( 'ytd-page-manager#page-manager.style-scope.ytd-app' ); const ytdAppCnt = insp(ytdAppElm); pageType = ytdAppCnt ? (ytdAppCnt.data || 0).page : null; if (!document.querySelector('ytd-watch-flexy #player')) return; const flexyArr = [...document.querySelectorAll('ytd-watch-flexy')].filter( e => !e.closest('[hidden]') && e.querySelector('#player') ); if (flexyArr.length === 1) { elements.flexy = flexyArr[0]; if (isRightTabsInserted) { Promise.resolve(lockSet['refreshSecondaryInnerLock']) .then(eventMap['refreshSecondaryInner']) .catch(console.warn); Promise.resolve(lockSet['removeKeepCommentsScrollerLock']) .then(removeKeepCommentsScroller) .catch(console.warn); } else { navigateFinishedPromise.resolve(); if (plugin.minibrowser.toUse) plugin.minibrowser.activate(); if (plugin.autoExpandInfoDesc.toUse) plugin.autoExpandInfoDesc.activate(); if (plugin.fullChannelNameOnHover.toUse) plugin.fullChannelNameOnHover.activate(); } const { chat } = elements; if (chat instanceof Element) { chat.setAttribute111('tyt-active-chat-frame', 'CF'); } const { infoExpander } = elements; if (infoExpander && infoExpander.closest('#right-tabs')) { Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); } Promise.resolve(lockSet['layoutFixLock']).then(layoutFix); if (plugin.fullChannelNameOnHover.activated) { plugin.fullChannelNameOnHover.onNavigateFinish(); } } }, onceInsertRightTabs: () => { try { const { related } = elements; if (!related) { console.warn('[YouTube+] onceInsertRightTabs: related element not found'); return; } let rightTabs = document.querySelector('#right-tabs'); if (!rightTabs && related) { getLangForPage(); const docTmp = document.createElement('template'); const tabsHTML = getTabsHTML(); if (!tabsHTML) { console.error('[YouTube+] onceInsertRightTabs: getTabsHTML returned empty'); return; } docTmp.innerHTML = _createHTMLInner(tabsHTML); const newElm = docTmp.content.firstElementChild; if (!newElm) { console.error('[YouTube+] onceInsertRightTabs: failed to create tabs element'); return; } inPageRearrange = true; if (related.parentNode) { related.parentNode.insertBefore000(newElm, related); } else { console.error('[YouTube+] onceInsertRightTabs: related element has no parent'); inPageRearrange = false; return; } inPageRearrange = false; rightTabs = newElm; const commentsTab = rightTabs.querySelector('[tyt-tab-content="#tab-comments"]'); if (commentsTab) { commentsTab.classList.add('tab-btn-hidden'); } const secondaryWrapper = document.createElement('secondary-wrapper'); secondaryWrapper.classList.add('tabview-secondary-wrapper'); const secondaryInner = document.querySelector( '#secondary-inner.style-scope.ytd-watch-flexy' ); if (secondaryInner) { inPageRearrange = true; secondaryWrapper.replaceChildren000(...secondaryInner.childNodes); secondaryInner.insertBefore000(secondaryWrapper, secondaryInner.firstChild); inPageRearrange = false; } const materialTabs = rightTabs.querySelector('#material-tabs'); if (materialTabs) { materialTabs.removeEventListener('click', eventMap['tabs-btn-click'], true); materialTabs.addEventListener('click', eventMap['tabs-btn-click'], true); console.log('[YouTube+] Tab click handler attached successfully to #material-tabs'); const tabButtons = materialTabs.querySelectorAll('.tab-btn[tyt-tab-content]'); console.log(`[YouTube+] Found ${tabButtons.length} tab buttons`); tabButtons.forEach(btn => { btn.style.cursor = 'pointer'; btn.setAttribute('role', 'tab'); btn.setAttribute('tabindex', '0'); }); } else { console.error('[YouTube+] CRITICAL: #material-tabs not found after creation!'); console.log('[YouTube+] rightTabs HTML:', rightTabs.outerHTML?.substring(0, 500)); } inPageRearrange = true; if (rightTabs && !rightTabs.closest('secondary-wrapper')) { secondaryWrapper.appendChild000(rightTabs); } inPageRearrange = false; } else if (rightTabs) { console.log('[YouTube+] rightTabs already exists, checking handlers'); const materialTabs = rightTabs.querySelector('#material-tabs'); if (materialTabs) { materialTabs.removeEventListener('click', eventMap['tabs-btn-click'], true); materialTabs.addEventListener('click', eventMap['tabs-btn-click'], true); console.log('[YouTube+] Re-attached tab click handler'); } } if (rightTabs) { isRightTabsInserted = true; const ioTabBtns = new IntersectionObserver( entries => { for (const entry of entries) { const rect = entry.boundingClientRect; entry.target.classList.toggle('tab-btn-visible', rect.width && rect.height); } }, { rootMargin: '0px' } ); const tabButtons = document.querySelectorAll('.tab-btn[tyt-tab-content]'); console.log(`[YouTube+] Observing ${tabButtons.length} tab buttons`); for (const btn of tabButtons) { if (btn && btn instanceof Element && btn.isConnected) { try { ioTabBtns.observe(btn); } catch (observeError) { console.warn('[YouTube+] Failed to observe tab button:', btn, observeError); } } } if (!related.closest('#right-tabs')) { const tabVideos = document.querySelector('#tab-videos'); if (tabVideos) { tabVideos.assignChildren111(null, related, null); } } const { infoExpander } = elements; if (infoExpander && !infoExpander.closest('#right-tabs')) { const tabInfo = document.querySelector('#tab-info'); if (tabInfo) { tabInfo.assignChildren111(null, infoExpander, null); } } const commentsArea = elements.comments; if (commentsArea && !commentsArea.closest('#right-tabs')) { const tabComments = document.querySelector('#tab-comments'); if (tabComments) { tabComments.assignChildren111(null, commentsArea, null); } } rightTabsProvidedPromise.resolve(); roRightTabs.disconnect(); if (rightTabs && rightTabs instanceof Element && rightTabs.isConnected) { try { roRightTabs.observe(rightTabs); } catch (observeError) { console.error( '[YouTube+] Failed to observe rightTabs with ResizeObserver:', observeError ); } } const ytdFlexyElm = elements.flexy; if (ytdFlexyElm && ytdFlexyElm instanceof Element && ytdFlexyElm.isConnected) { const aoFlexy = new MutationObserver(eventMap['aoFlexyFn']); try { aoFlexy.observe(ytdFlexyElm, { attributes: true }); } catch (observeError) { console.error('[YouTube+] Failed to observe ytdFlexyElm:', observeError); } Promise.resolve(lockSet['fixInitialTabStateLock']) .then(eventMap['fixInitialTabStateFn']) .catch(console.warn); ytdFlexyElm.incAttribute111('attr-7qlsy'); } } } catch (error) { console.error('[YouTube+] onceInsertRightTabs: critical error', error); console.error(error.stack); } }, aoFlexyFn: () => { Promise.resolve(lockSet['checkCommentsShouldBeHiddenLock']) .then(eventMap['checkCommentsShouldBeHidden']) .catch(console.warn); Promise.resolve(lockSet['refreshSecondaryInnerLock']) .then(eventMap['refreshSecondaryInner']) .catch(console.warn); Promise.resolve(lockSet['tabsStatusCorrectionLock']) .then(eventMap['tabsStatusCorrection']) .catch(console.warn); const videoId = getCurrentVideoId(); if (videoId !== tmpLastVideoId) { tmpLastVideoId = videoId; Promise.resolve(lockSet['updateOnVideoIdChangedLock']) .then(eventMap['updateOnVideoIdChanged']) .catch(console.warn); } }, twoColumnChanged10: lockId => { if (lockId !== lockGet['twoColumnChanged10Lock']) return; for (const continuation of document.querySelectorAll( '#tab-videos ytd-watch-next-secondary-results-renderer ytd-continuation-item-renderer' )) { if (continuation.closest('[hidden]')) continue; const cnt = insp(continuation); if (typeof cnt.showButton === 'boolean') { if (cnt.showButton === false) continue; cnt.showButton = false; const behavior = cnt.ytRendererBehavior || cnt; if (typeof behavior.invalidate === 'function') { behavior.invalidate(!1); } } } }, tabsStatusCorrection: lockId => { if (lockId !== lockGet['tabsStatusCorrectionLock']) return; const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) return; const p = tabAStatus; const q = calculationFn(p, 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 4096); let resetForPanelDisappeared = false; let special = 0; let actioned = false; if (plugin['external.ytlstm'].activated) { if (q & 64) { } else if ( (p & (1 | 2 | 4 | 8 | 16 | 4096)) === (1 | 0 | 0 | 8 | 16 | 4096) && (q & (1 | 2 | 4 | 8 | 16 | 4096)) === (1 | 0 | 4 | 0 | 16 | 4096) ) { special = 3; } else if ( (q & (1 | 16)) === (1 | 16) && document.querySelector('[data-ytlstm-theater-mode]') ) { special = 1; } else if ( (q & (1 | 8 | 16)) === (1 | 8 | 16) && document.querySelector('[is-two-columns_][theater][tyt-chat="+"]') ) { special = 2; } } if (special) { } else if ((p & 128) === 0 && (q & 128) === 128) { lastPanel = 'playlist'; } else if ((p & 8) === 0 && (q & 8) === 8) { lastPanel = 'chat'; } else if ( (((p & 4) === 4 && (q & (4 | 8)) === (0 | 0)) || ((p & 8) === 8 && (q & (4 | 8)) === (0 | 0))) && lastPanel === 'chat' ) { lastPanel = lastTab || ''; resetForPanelDisappeared = true; } else if ((p & (4 | 8)) === 8 && (q & (4 | 8)) === 4 && lastPanel === 'chat') { lastPanel = lastTab || ''; resetForPanelDisappeared = true; } else if ((p & 128) === 128 && (q & 128) === 0 && lastPanel === 'playlist') { lastPanel = lastTab || ''; resetForPanelDisappeared = true; } tabAStatus = q; if (special) { if (special === 1) { if (ytdFlexyElm.getAttribute('tyt-chat') !== '+') { ytBtnExpandChat(); } if (ytdFlexyElm.getAttribute('tyt-tab')) { switchToTab(null); } } else if (special === 2) { ytBtnCollapseChat(); } else if (special === 3) { ytBtnCancelTheater(); if (lastTab) { switchToTab(lastTab); } } return; } let bFixForResizedTab = false; if ((q ^ 2) === 2 && bFixForResizedTabLater) { bFixForResizedTab = true; } if (((p & 16) === 16) & ((q & 16) === 0)) { Promise.resolve(lockSet['twoColumnChanged10Lock']) .then(eventMap['twoColumnChanged10']) .catch(console.warn); } if (((p & 2) === 2) ^ ((q & 2) === 2) && (q & 2) === 2) { bFixForResizedTab = true; } if ((p & 2) === 0 && (q & 2) === 2 && (p & 128) === 128 && (q & 128) === 128) { lastPanel = lastTab || ''; ytBtnClosePlaylist(); actioned = true; } if ( (p & (8 | 128)) === (0 | 128) && (q & (8 | 128)) === (8 | 128) && lastPanel === 'chat' ) { lastPanel = lastTab || ''; ytBtnClosePlaylist(); actioned = true; } if ( (p & (1 | 2 | 4 | 8 | 16 | 32 | 64 | 128)) === (1 | 2 | 0 | 8 | 16) && (q & (1 | 2 | 4 | 8 | 16 | 32 | 64 | 128)) === (0 | 2 | 0 | 8 | 16) ) { lastPanel = lastTab || ''; ytBtnCollapseChat(); actioned = true; } if ( (p & (2 | 128)) === (2 | 0) && (q & (2 | 128)) === (2 | 128) && lastPanel === 'playlist' ) { switchToTab(null); actioned = true; } if ( (p & (8 | 128)) === (8 | 0) && (q & (8 | 128)) === (8 | 128) && lastPanel === 'playlist' ) { lastPanel = lastTab || ''; ytBtnCollapseChat(); actioned = true; } if ((p & (1 | 16 | 128)) === (1 | 16) && (q & (1 | 16 | 128)) === (1 | 16 | 128)) { ytBtnCancelTheater(); actioned = true; } if ((p & (1 | 16 | 128)) === (16 | 128) && (q & (1 | 16 | 128)) === (1 | 16 | 128)) { lastPanel = lastTab || ''; ytBtnClosePlaylist(); actioned = true; } if ((q & 64) === 64) { actioned = false; } else if ((p & 64) === 64 && (q & 64) === 0) { if ((q & 32) === 32) { ytBtnCloseEngagementPanels(); } if ((q & (2 | 8)) === (2 | 8)) { if (lastPanel === 'chat') { switchToTab(null); actioned = true; } else if (lastPanel) { ytBtnCollapseChat(); actioned = true; } } } else if ( (p & (1 | 2 | 8 | 16 | 32)) === (1 | 0 | 0 | 16 | 0) && (q & (1 | 2 | 8 | 16 | 32)) === (1 | 0 | 8 | 16 | 0) ) { ytBtnCancelTheater(); actioned = true; } else if ( (p & (1 | 16 | 32)) === (0 | 16 | 0) && (q & (1 | 16 | 32)) === (0 | 16 | 32) && (q & (2 | 8)) > 0 ) { if (q & 2) { switchToTab(null); actioned = true; } if (q & 8) { ytBtnCollapseChat(); actioned = true; } } else if ( (p & (1 | 16 | 8 | 2)) === (16 | 8) && (q & (1 | 16 | 8 | 2)) === 16 && (q & 128) === 0 ) { if (lastTab) { switchToTab(lastTab); actioned = true; } } else if ((p & 1) === 0 && (q & 1) === 1) { if ((q & 32) === 32) { ytBtnCloseEngagementPanels(); } if ((p & 9) === 8 && (q & 9) === 9) { ytBtnCollapseChat(); } switchToTab(null); actioned = true; } else if ((p & 3) === 1 && (q & 3) === 3) { ytBtnCancelTheater(); actioned = true; } else if ((p & 10) === 2 && (q & 10) === 10) { switchToTab(null); actioned = true; } else if ((p & (8 | 32)) === (0 | 32) && (q & (8 | 32)) === (8 | 32)) { ytBtnCloseEngagementPanels(); actioned = true; } else if ((p & (2 | 32)) === (0 | 32) && (q & (2 | 32)) === (2 | 32)) { ytBtnCloseEngagementPanels(); actioned = true; } else if ((p & (2 | 8)) === (0 | 8) && (q & (2 | 8)) === (2 | 8)) { ytBtnCollapseChat(); actioned = true; } else if ((p & 1) === 1 && (q & (1 | 32)) === (0 | 0)) { if (lastPanel === 'chat') { ytBtnExpandChat(); actioned = true; } else if (lastPanel === lastTab && lastTab) { switchToTab(lastTab); actioned = true; } } if (!actioned && (q & 128) === 128) { lastPanel = 'playlist'; if ((q & 2) === 2) { switchToTab(null); actioned = true; } } let shouldDoAutoFix = false; if ((p & 2) === 2 && (q & (2 | 128)) === (0 | 128)) { } else if ((p & 8) === 8 && (q & (8 | 128)) === (0 | 128)) { } else if ( !actioned && (p & (1 | 16)) === 16 && (q & (1 | 16 | 8 | 2 | 32 | 64)) === (16 | 0 | 0) ) { shouldDoAutoFix = true; } else if ((q & (1 | 2 | 4 | 8 | 16 | 32 | 64 | 128)) === (4 | 16)) { shouldDoAutoFix = true; } if (shouldDoAutoFix) { console.log(388, 'd'); if (lastPanel === 'chat') { console.log(388, 'd1c'); ytBtnExpandChat(); actioned = true; } else if (lastPanel === 'playlist') { console.log(388, 'd1p'); ytBtnOpenPlaylist(); actioned = true; } else if (lastTab) { console.log(388, 'd2t'); switchToTab(lastTab); actioned = true; } else if (resetForPanelDisappeared) { console.log(388, 'd2d'); Promise.resolve(lockSet['fixInitialTabStateLock']) .then(eventMap['fixInitialTabStateFn']) .catch(console.warn); actioned = true; } } if (bFixForResizedTab) { bFixForResizedTabLater = false; Promise.resolve(0).then(eventMap['fixForTabDisplay']).catch(console.warn); } if (((p & 16) === 16) ^ ((q & 16) === 16)) { Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); Promise.resolve(lockSet['removeKeepCommentsScrollerLock']) .then(removeKeepCommentsScroller) .catch(console.warn); Promise.resolve(lockSet['layoutFixLock']).then(layoutFix).catch(console.warn); } }, updateOnVideoIdChanged: lockId => { if (lockId !== lockGet['updateOnVideoIdChangedLock']) return; const videoId = tmpLastVideoId; if (!videoId) return; const bodyRenderer = elements.infoExpanderRendererBack; const bodyRendererNew = elements.infoExpanderRendererFront; if (bodyRendererNew && bodyRenderer) { insp(bodyRendererNew).data = insp(bodyRenderer).data; } Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); }, fixInitialTabStateFn: async lockId => { if (lockGet['fixInitialTabStateLock'] !== lockId) return; const delayTime = fixInitialTabStateK > 0 ? 200 : 1; await delayPn(delayTime); if (lockGet['fixInitialTabStateLock'] !== lockId) return; const kTab = document.querySelector('[tyt-tab]'); const qTab = !kTab || kTab.getAttribute('tyt-tab') === '' ? checkElementExist('ytd-watch-flexy[is-two-columns_]', '[hidden]') : null; if (checkElementExist('ytd-playlist-panel-renderer#playlist', '[hidden], [collapsed]')) { DEBUG_5085 && console.log('fixInitialTabStateFn 1p'); switchToTab(null); } else if (checkElementExist('ytd-live-chat-frame#chat', '[hidden], [collapsed]')) { DEBUG_5085 && console.log('fixInitialTabStateFn 1a'); switchToTab(null); if (checkElementExist('ytd-watch-flexy[theater]', '[hidden]')) { ytBtnCollapseChat(); } } else if (qTab) { const hasTheater = qTab.hasAttribute('theater'); if (hasTheater) { DEBUG_5085 && console.log('fixInitialTabStateFn 1c'); switchToTab(null); } else { if (DEBUG_5085) { console.log('fixInitialTabStateFn 1b'); } const btn0 = document.querySelector('.tab-btn-visible'); if (btn0) { switchToTab(btn0); } else { switchToTab(null); } } } else { DEBUG_5085 && console.log('fixInitialTabStateFn 1z'); } fixInitialTabStateK++; }, 'tabs-btn-click': evt => { try { const { target } = evt; if (!target) { console.warn('[YouTube+] tabs-btn-click: no target element'); return; } let tabBtn = target; const hasTabBtnClass = tabBtn && ((tabBtn.classList && tabBtn.classList.contains('tab-btn')) || (tabBtn.className && typeof tabBtn.className === 'string' && tabBtn.className.includes('tab-btn'))); if (!hasTabBtnClass && tabBtn && typeof tabBtn.closest === 'function') { tabBtn = tabBtn.closest('.tab-btn'); } if (!tabBtn) { if (DEBUG_5085) { console.warn('[YouTube+] tabs-btn-click: could not find tab button'); } return; } const hasTabContent = tabBtn.hasAttribute('tyt-tab-content'); if (!hasTabContent) { console.warn('[YouTube+] tabs-btn-click: button missing tyt-tab-content attribute'); return; } evt.preventDefault(); evt.stopPropagation(); evt.stopImmediatePropagation(); const tabContent = getAttributeSafe(tabBtn, 'tyt-tab-content'); if (DEBUG_5085) { console.log('[YouTube+] Tab clicked:', tabContent, tabBtn); } if (tabContent) { switchToTab(tabBtn); } else { console.warn('[YouTube+] tabs-btn-click: no tab content value found'); } } catch (error) { console.error('[YouTube+] tabs-btn-click: error handling click', error); console.error(error.stack); } }, }; Promise.all([videosElementProvidedPromise, navigateFinishedPromise]) .then(eventMap['onceInsertRightTabs']) .catch(console.warn); Promise.all([navigateFinishedPromise, infoExpanderElementProvidedPromise]) .then(eventMap['onceInfoExpanderElementProvidedPromised']) .catch(console.warn); const isCustomElementsProvided = typeof customElements !== 'undefined' && typeof (customElements || 0).whenDefined === 'function'; const promiseForCustomYtElementsReady = isCustomElementsProvided ? Promise.resolve(0) : new Promise(callback => { const EVENT_KEY_ON_REGISTRY_READY = 'ytI-ce-registry-created'; if (typeof customElements === 'undefined') { if (!('__CE_registry' in document)) { Object.defineProperty(document, '__CE_registry', { get() { }, set(nv) { if (typeof nv == 'object') { delete this.__CE_registry; this.__CE_registry = nv; this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY)); } return true; }, enumerable: false, configurable: true, }); } let eventHandler = _evt => { document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false); const callbackFn = callback; eventHandler = null; callbackFn(); }; document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false); } else { callback(); } }); const _retrieveCE = async nodeName => { try { isCustomElementsProvided || (await promiseForCustomYtElementsReady); await customElements.whenDefined(nodeName); } catch (e) { console.warn(e); } }; const retrieveCE = async nodeName => { try { isCustomElementsProvided || (await promiseForCustomYtElementsReady); await customElements.whenDefined(nodeName); const dummy = document.querySelector(nodeName) || document.createElement(nodeName); const cProto = insp(dummy).constructor.prototype; return cProto; } catch (e) { console.warn(e); } }; const moOverallRes = { _yt_playerProvided: () => (window || 0)._yt_player || 0 || 0, }; let promiseWaitNext = null; const moOverall = new MutationObserver(() => { if (promiseWaitNext) { promiseWaitNext.resolve(); promiseWaitNext = null; } if (typeof moOverallRes._yt_playerProvided === 'function') { const r = moOverallRes._yt_playerProvided(); if (r) { moOverallRes._yt_playerProvided = r; eventMap._yt_playerProvided(); } } }); moOverall.observe(document, { subtree: true, childList: true }); const moEgmPanelReady = new MutationObserver(mutations => { for (const mutation of mutations) { const { target } = mutation; if (!target.hasAttribute000('tyt-egm-panel-jclmd')) continue; if (target.hasAttribute000('target-id') && target.hasAttribute000('visibility')) { target.removeAttribute000('tyt-egm-panel-jclmd'); moEgmPanelReadyClearFn(); Promise.resolve(target) .then(eventMap['ytd-engagement-panel-section-list-renderer::bindTarget']) .catch(console.warn); } } }); const moEgmPanelReadyClearFn = () => { if (document.querySelector('[tyt-egm-panel-jclmd]') === null) { moEgmPanelReady.takeRecords(); moEgmPanelReady.disconnect(); } }; document.addEventListener('yt-navigate-finish', eventMap['yt-navigate-finish'], false); document.addEventListener( 'animationstart', evt => { const f = eventMap[evt.animationName]; if (typeof f === 'function') f(evt.target); }, capturePassive ); mLoaded.flag |= 1; document.documentElement.setAttribute111('tabview-loaded', mLoaded.makeString()); promiseForCustomYtElementsReady.then(eventMap['ceHack']).catch(console.warn); executionFinished = 1; } catch (e) { console.log('error 0xF491'); console.error(e); } }; const styles = { main: ` @keyframes relatedElementProvided{0%{background-position-x:3px;}100%{background-position-x:4px;}} html[tabview-loaded="icp"] #related.ytd-watch-flexy{animation:relatedElementProvided 1ms linear 0s 1 normal forwards;} html[tabview-loaded="icp"] #right-tabs #related.ytd-watch-flexy,html[tabview-loaded="icp"] [hidden] #related.ytd-watch-flexy,html[tabview-loaded="icp"] #right-tabs ytd-expander#expander,html[tabview-loaded="icp"] [hidden] ytd-expander#expander,html[tabview-loaded="icp"] ytd-comments ytd-expander#expander{animation:initial;} #secondary.ytd-watch-flexy{position:relative;} #secondary-inner.style-scope.ytd-watch-flexy{height:100%;} #secondary-inner secondary-wrapper{display:flex;flex-direction:column;flex-wrap:nowrap;box-sizing:border-box;padding:0;margin:0;border:0;height:100%;max-height:calc(100vh - var(--ytd-toolbar-height,56px));position:absolute;top:0;right:0;left:0;contain:strict;padding:var(--ytd-margin-6x) var(--ytd-margin-6x) var(--ytd-margin-6x) 0;} #right-tabs{position:relative;display:flex;padding:0;margin:0;flex-grow:1;flex-direction:column;} [tyt-tab=""] #right-tabs{flex-grow:0;} [tyt-tab=""] #right-tabs .tab-content{border:0;} #right-tabs .tab-content{flex-grow:1;} ytd-watch-flexy[hide-default-text-inline-expander] #primary.style-scope.ytd-watch-flexy ytd-text-inline-expander{display:none;} ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden{--comment-pre-load-sizing:90px;visibility:collapse;z-index:-1;position:fixed!important;left:2px;top:2px;width:var(--comment-pre-load-sizing)!important;height:var(--comment-pre-load-sizing)!important;display:block!important;pointer-events:none!important;overflow:hidden;contain:strict;border:0;margin:0;padding:0;} ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments>ytd-item-section-renderer#sections{display:block!important;overflow:hidden;height:var(--comment-pre-load-sizing);width:var(--comment-pre-load-sizing);contain:strict;border:0;margin:0;padding:0;} ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments>ytd-item-section-renderer#sections>#contents{display:flex!important;flex-direction:row;gap:60px;overflow:hidden;height:var(--comment-pre-load-sizing);width:var(--comment-pre-load-sizing);contain:strict;border:0;margin:0;padding:0;} ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents{--comment-pre-load-display:none;} ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents>*:only-of-type,ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents>*:last-child{--comment-pre-load-display:block;} ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents>*{display:var(--comment-pre-load-display)!important;} #right-tabs #material-tabs{position:relative;display:flex;padding:0;border:1px solid var(--ytd-searchbox-legacy-border-color);overflow:hidden;} [tyt-tab] #right-tabs #material-tabs{border-radius:12px;} [tyt-tab^="#"] #right-tabs #material-tabs{border-radius:12px 12px 0 0;} ytd-watch-flexy:not([is-two-columns_]) #right-tabs #material-tabs{outline:0;} #right-tabs #material-tabs a.tab-btn[tyt-tab-content]>*{pointer-events:none;} #right-tabs #material-tabs a.tab-btn[tyt-tab-content]>.font-size-right{pointer-events:initial;display:none;} ytd-watch-flexy #right-tabs .tab-content{padding:0;box-sizing:border-box;display:block;border:1px solid var(--ytd-searchbox-legacy-border-color);border-top:0;position:relative;top:0;display:flex;flex-direction:row;overflow:hidden;border-radius:0 0 12px 12px;} ytd-watch-flexy:not([is-two-columns_]) #right-tabs .tab-content{height:100%;} ytd-watch-flexy #right-tabs .tab-content-cld{box-sizing:border-box;position:relative;display:block;width:100%;overflow:auto;--tab-content-padding:var(--ytd-margin-4x);padding:var(--tab-content-padding);contain:layout paint;} .tab-content-cld,#right-tabs,.tab-content{transition:none;animation:none;} #right-tabs #emojis.ytd-commentbox{inset:auto 0 auto 0;width:auto;} ytd-watch-flexy[is-two-columns_] #right-tabs .tab-content-cld{height:100%;width:100%;contain:size layout paint style;position:absolute;} ytd-watch-flexy #right-tabs .tab-content-cld.tab-content-hidden{display:none;width:100%;contain:size layout paint style;} @supports (color:var(--tabview-tab-btn-define)){ ytd-watch-flexy #right-tabs .tab-btn{background:var(--yt-spec-general-background-a);} html{--tyt-tab-btn-flex-grow:1;--tyt-tab-btn-flex-basis:0%;--tyt-tab-bar-color-1-def:#ff4533;--tyt-tab-bar-color-2-def:var(--yt-brand-light-red);--tyt-tab-bar-color-1:var(--main-color,var(--tyt-tab-bar-color-1-def));--tyt-tab-bar-color-2:var(--main-color,var(--tyt-tab-bar-color-2-def));} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]{flex:var(--tyt-tab-btn-flex-grow) 1 var(--tyt-tab-btn-flex-basis);position:relative;display:inline-block;text-decoration:none;text-transform:uppercase;--tyt-tab-btn-color:var(--yt-spec-text-secondary);color:var(--tyt-tab-btn-color);text-align:center;padding:14px 8px 10px;border:0;border-bottom:4px solid transparent;font-weight:500;font-size:12px;line-height:18px;cursor:pointer;transition:border 200ms linear 100ms;background-color:var(--ytd-searchbox-legacy-button-color);text-transform:var(--yt-button-text-transform,inherit);user-select:none!important;overflow:hidden;white-space:nowrap;text-overflow:clip;} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]>svg{height:18px;padding-right:0;vertical-align:bottom;opacity:.5;margin-right:0;color:var(--yt-button-color,inherit);fill:var(--iron-icon-fill-color,currentcolor);stroke:var(--iron-icon-stroke-color,none);pointer-events:none;} ytd-watch-flexy #right-tabs .tab-btn{--tabview-btn-txt-ml:8px;} ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"]{--tabview-btn-txt-ml:0;} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]>svg+span{margin-left:var(--tabview-btn-txt-ml);} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].active{font-weight:500;outline:0;--tyt-tab-btn-color:var(--yt-spec-text-primary);background-color:var(--ytd-searchbox-legacy-button-focus-color);border-bottom:2px var(--tyt-tab-bar-color-2) solid;} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].active svg{opacity:.9;} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]:not(.active):hover{background-color:var(--ytd-searchbox-legacy-button-hover-color);--tyt-tab-btn-color:var(--yt-spec-text-primary);} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]:not(.active):hover svg{opacity:.9;} ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].tab-btn-hidden{display:none;} ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"],ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"]:hover{--tyt-tab-btn-color:var(--yt-spec-icon-disabled);} ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"] span#tyt-cm-count:empty{display:none;} ytd-watch-flexy #right-tabs .tab-btn span#tyt-cm-count:empty::after{display:inline-block;width:4em;text-align:left;font-size:inherit;color:currentColor;transform:scaleX(.8);} } @supports (color:var(--tyt-cm-count-define)){ ytd-watch-flexy{--tyt-x-loading-content-letter-spacing:2px;} html{--tabview-text-loading:"Loading";--tabview-text-fetching:"Fetching";--tabview-panel-loading:var(--tabview-text-loading);} html:lang(ja){--tabview-text-loading:"読み込み中";--tabview-text-fetching:"フェッチ..";} html:lang(ko){--tabview-text-loading:"로딩..";--tabview-text-fetching:"가져오기..";} html:lang(zh-Hant){--tabview-text-loading:"載入中";--tabview-text-fetching:"擷取中";} html:lang(zh-Hans){--tabview-text-loading:"加载中";--tabview-text-fetching:"抓取中";} html:lang(ru){--tabview-text-loading:"Загрузка";--tabview-text-fetching:"Получение";} ytd-watch-flexy #right-tabs .tab-btn span#tyt-cm-count:empty::after{content:var(--tabview-text-loading);letter-spacing:var(--tyt-x-loading-content-letter-spacing);} } @supports (color:var(--tabview-font-size-btn-define)){ .font-size-right{display:inline-flex;flex-direction:column;position:absolute;right:0;top:0;bottom:0;width:16px;padding:4px 0;justify-content:space-evenly;align-content:space-evenly;pointer-events:none;} html body ytd-watch-flexy.style-scope .font-size-btn{user-select:none!important;} .font-size-btn{--tyt-font-size-btn-display:none;display:var(--tyt-font-size-btn-display,none);width:12px;height:12px;color:var(--yt-spec-text-secondary);background-color:var(--yt-spec-badge-chip-background);box-sizing:border-box;cursor:pointer;transform-origin:left top;margin:0;padding:0;position:relative;font-family:'Menlo','Lucida Console','Monaco','Consolas',monospace;line-height:100%;font-weight:900;transition:background-color 90ms linear,color 90ms linear;pointer-events:all;} .font-size-btn:hover{background-color:var(--yt-spec-text-primary);color:var(--yt-spec-general-background-a);} @supports (zoom:.5){ .tab-btn .font-size-btn{--tyt-font-size-btn-display:none;} .tab-btn.active:hover .font-size-btn{--tyt-font-size-btn-display:inline-block;} body ytd-watch-flexy:not([is-two-columns_]) #columns.ytd-watch-flexy{flex-direction:column;} body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy{display:block;width:100%;box-sizing:border-box;} body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy secondary-wrapper{padding-left:var(--ytd-margin-6x);contain:content;height:initial;} body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy secondary-wrapper #right-tabs{overflow:auto;} [tyt-chat="+"] { --tyt-chat-grow: 1;} [tyt-chat="+"] secondary-wrapper>[tyt-chat-container]{flex-grow:var(--tyt-chat-grow);flex-shrink:0;display:flex;flex-direction:column;} [tyt-chat="+"] secondary-wrapper>[tyt-chat-container]>#chat{flex-grow:var(--tyt-chat-grow);} ytd-watch-flexy[is-two-columns_]:not([theater]) #columns.style-scope.ytd-watch-flexy{min-height:calc(100vh - var(--ytd-toolbar-height,56px));} ytd-watch-flexy[is-two-columns_]:not([full-bleed-player]) ytd-live-chat-frame#chat{min-height:initial!important;height:initial!important;} ytd-watch-flexy[tyt-tab^="#"]:not([is-two-columns_]):not([tyt-chat="+"]) #right-tabs{min-height:var(--ytd-watch-flexy-chat-max-height);} body ytd-watch-flexy:not([is-two-columns_]) #chat.ytd-watch-flexy{margin-top:0;} body ytd-watch-flexy:not([is-two-columns_]) ytd-watch-metadata.ytd-watch-flexy{margin-bottom:0;} ytd-watch-metadata.ytd-watch-flexy ytd-metadata-row-container-renderer{display:none;} #tab-info [show-expand-button] #expand-sizer.ytd-text-inline-expander{visibility:initial;} #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#left-arrow-container.ytd-video-description-infocards-section-renderer>#left-arrow,#tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#right-arrow-container.ytd-video-description-infocards-section-renderer>#right-arrow{border:6px solid transparent;opacity:.65;} #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#left-arrow-container.ytd-video-description-infocards-section-renderer>#left-arrow:hover,#tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#right-arrow-container.ytd-video-description-infocards-section-renderer>#right-arrow:hover{opacity:1;} #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>div#left-arrow-container::before{content:'';background:transparent;width:40px;display:block;height:40px;position:absolute;left:-20px;top:0;z-index:-1;} #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>div#right-arrow-container::before{content:'';background:transparent;width:40px;display:block;height:40px;position:absolute;right:-20px;top:0;z-index:-1;} body ytd-watch-flexy[is-two-columns_][tyt-egm-panel_] #columns.style-scope.ytd-watch-flexy #panels.style-scope.ytd-watch-flexy{flex-grow:1;flex-shrink:0;display:flex;flex-direction:column;} body ytd-watch-flexy[is-two-columns_][tyt-egm-panel_] #columns.style-scope.ytd-watch-flexy #panels.style-scope.ytd-watch-flexy ytd-engagement-panel-section-list-renderer[target-id][visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"]{height:initial;max-height:initial;min-height:initial;flex-grow:1;flex-shrink:0;display:flex;flex-direction:column;} secondary-wrapper [visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] ytd-transcript-renderer:not(:empty),secondary-wrapper [visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] #body.ytd-transcript-renderer:not(:empty),secondary-wrapper [visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] #content.ytd-transcript-renderer:not(:empty){flex-grow:1;height:initial;max-height:initial;min-height:initial;} secondary-wrapper #content.ytd-engagement-panel-section-list-renderer{position:relative;} secondary-wrapper #content.ytd-engagement-panel-section-list-renderer>[panel-target-id]:only-child{contain:style size;} secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-list-renderer.ytd-transcript-search-panel-renderer{flex-grow:1;contain:strict;} secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-renderer.style-scope.ytd-transcript-segment-list-renderer{contain:layout paint style;} secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-renderer.style-scope.ytd-transcript-segment-list-renderer>.segment{contain:layout paint style;} body ytd-watch-flexy[theater] #secondary.ytd-watch-flexy{margin-top:var(--ytd-margin-3x);padding-top:0;} body ytd-watch-flexy[theater] secondary-wrapper{margin-top:0;padding-top:0;} body ytd-watch-flexy[theater] #chat.ytd-watch-flexy{margin-bottom:var(--ytd-margin-2x);} ytd-watch-flexy[theater] #right-tabs .tab-btn[tyt-tab-content]{padding:8px 4px 6px;border-bottom:0 solid transparent;} ytd-watch-flexy[theater] #playlist.ytd-watch-flexy{margin-bottom:var(--ytd-margin-2x);} ytd-watch-flexy[theater] ytd-playlist-panel-renderer[collapsible][collapsed] .header.ytd-playlist-panel-renderer{padding:6px 8px;} #tab-comments ytd-comments#comments [field-of-cm-count]{margin-top:0;} #tab-info>ytd-expandable-video-description-body-renderer{margin-bottom:var(--ytd-margin-3x);} #tab-info [class]:last-child{margin-bottom:0;padding-bottom:0;} #tab-info ytd-rich-metadata-row-renderer ytd-rich-metadata-renderer{max-width:initial;} ytd-watch-flexy[is-two-columns_] secondary-wrapper #chat.ytd-watch-flexy{margin-bottom:var(--ytd-margin-3x);} ytd-watch-flexy[tyt-tab] tp-yt-paper-tooltip{white-space:nowrap;contain:content;} ytd-watch-info-text tp-yt-paper-tooltip.style-scope.ytd-watch-info-text{margin-bottom:-300px;margin-top:-96px;} [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata{font-size:1.2rem;line-height:1.8rem;} [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata yt-animated-rolling-number{font-size:inherit;} [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata #info-container.style-scope.ytd-watch-info-text{align-items:center;} ytd-watch-flexy[hide-default-text-inline-expander]{--tyt-bottom-watch-metadata-margin:6px;} [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata>#description-inner.ytd-watch-metadata{margin:6px 12px;} [hide-default-text-inline-expander] ytd-watch-metadata[title-headline-xs] h1.ytd-watch-metadata{font-size:1.8rem;} ytd-watch-flexy[is-two-columns_][hide-default-text-inline-expander] #below.style-scope.ytd-watch-flexy ytd-merch-shelf-renderer{padding:0;border:0;margin:0;} ytd-watch-flexy[is-two-columns_][hide-default-text-inline-expander] #below.style-scope.ytd-watch-flexy ytd-watch-metadata.ytd-watch-flexy{margin-bottom:6px;} #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model--horizontal .yt-video-attribute-view-model__link-container .yt-video-attribute-view-model__hero-section{flex-shrink:0;} #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model__overflow-menu{background:var(--yt-emoji-picker-category-background-color);border-radius:99px;} #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model--image-square.yt-video-attribute-view-model--image-large .yt-video-attribute-view-model__hero-section{max-height:128px;} #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model--image-large .yt-video-attribute-view-model__hero-section{max-width:128px;} #tab-info ytd-reel-shelf-renderer #items.yt-horizontal-list-renderer ytd-reel-item-renderer.yt-horizontal-list-renderer{max-width:142px;} ytd-watch-info-text#ytd-watch-info-text.style-scope.ytd-watch-metadata #view-count.style-scope.ytd-watch-info-text,ytd-watch-info-text#ytd-watch-info-text.style-scope.ytd-watch-metadata #date-text.style-scope.ytd-watch-info-text{align-items:center;} ytd-watch-info-text:not([detailed]) #info.ytd-watch-info-text a.yt-simple-endpoint.yt-formatted-string{pointer-events:none;} body ytd-app>ytd-popup-container>tp-yt-iron-dropdown>#contentWrapper>[slot="dropdown-content"]{backdrop-filter:none;} #tab-info [tyt-clone-refresh-count]{overflow:visible!important;} #tab-info #items.ytd-horizontal-card-list-renderer yt-video-attribute-view-model.ytd-horizontal-card-list-renderer{contain:layout;} #tab-info #thumbnail-container.ytd-structured-description-channel-lockup-renderer,#tab-info ytd-media-lockup-renderer[is-compact] #thumbnail-container.ytd-media-lockup-renderer{flex-shrink:0;} secondary-wrapper ytd-donation-unavailable-renderer{--ytd-margin-6x:var(--ytd-margin-2x);--ytd-margin-5x:var(--ytd-margin-2x);--ytd-margin-4x:var(--ytd-margin-2x);--ytd-margin-3x:var(--ytd-margin-2x);} [tyt-no-less-btn] #less{display:none;} .tyt-metadata-hover-resized #purchase-button,.tyt-metadata-hover-resized #sponsor-button,.tyt-metadata-hover-resized #analytics-button,.tyt-metadata-hover-resized #subscribe-button{display:none!important;} .tyt-metadata-hover #upload-info{max-width:max-content;min-width:max-content;flex-basis:100vw;flex-shrink:0;} .tyt-info-invisible{display:none;} [tyt-playlist-expanded] secondary-wrapper>ytd-playlist-panel-renderer#playlist{overflow:auto;flex-shrink:1;flex-grow:1;max-height:unset!important;} [tyt-playlist-expanded] secondary-wrapper>ytd-playlist-panel-renderer#playlist>#container{max-height:unset!important;} secondary-wrapper ytd-playlist-panel-renderer{--ytd-margin-6x:var(--ytd-margin-3x);} #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #playlist-thumbnail.style-scope.ytd-structured-description-playlist-lockup-renderer{max-width:100%;} #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #lockup-container.ytd-structured-description-playlist-lockup-renderer{padding:1px;} #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #thumbnail.ytd-structured-description-playlist-lockup-renderer{outline:1px solid rgba(127,127,127,.5);} ytd-live-chat-frame#chat[collapsed] ytd-message-renderer~#show-hide-button.ytd-live-chat-frame>ytd-toggle-button-renderer.ytd-live-chat-frame{padding:0;} ytd-watch-flexy{--tyt-bottom-watch-metadata-margin:12px;} ytd-watch-flexy[rounded-info-panel],ytd-watch-flexy[rounded-player-large]{--tyt-rounded-a1:12px;} #bottom-row.style-scope.ytd-watch-metadata .item.ytd-watch-metadata{margin-right:var(--tyt-bottom-watch-metadata-margin,12px);margin-top:var(--tyt-bottom-watch-metadata-margin,12px);} #cinematics{contain:layout style size;} ytd-watch-flexy[is-two-columns_]{contain:layout style;} .yt-spec-touch-feedback-shape--touch-response .yt-spec-touch-feedback-shape__fill{background-color:transparent;} body[data-ytlstm-theater-mode] #secondary-inner[class] > secondary-wrapper[class]:not(#chat-container):not(#chat) {display: flex !important;} body[data-ytlstm-theater-mode] secondary-wrapper {all: unset;height: 100vh;} body[data-ytlstm-theater-mode] #right-tabs {display: none;} body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] [tyt-chat="+"] {--tyt-chat-grow: unset;} body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #columns.style-scope.ytd-watch-flexy, body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #secondary.style-scope.ytd-watch-flexy, body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #secondary-inner.style-scope.ytd-watch-flexy, body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] secondary-wrapper, body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #chat-container.style-scope, body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] [tyt-chat-container].style-scope {pointer-events: none;} body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #chat[class] {pointer-events: auto;} .playlist-items.ytd-playlist-panel-renderer {background-color: transparent !important;} @supports (color: var(--tyt-fix-20251124)) { #below ytd-watch-metadata .ytTextCarouselItemViewModelImageType { height: 16px; width: 16px;} #below ytd-watch-metadata yt-text-carousel-item-view-model { column-gap: 6px;} #below ytd-watch-metadata ytd-watch-info-text#ytd-watch-info-text { font-size: inherit; line-height: inherit;} } `, }; (async () => { const communicationKey = `ck-${Date.now()}-${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`; const Promise = (async () => {})().constructor; if (!document.documentElement) { await Promise.resolve(0); while (!document.documentElement) { await new Promise(resolve => nextBrowserTick(resolve)).then().catch(console.warn); } } const sourceURL = 'debug://tabview-youtube/tabview.execution.js'; const textContent = `(${executionScript})("${communicationKey}");${'\n\n'}//# sourceURL=${sourceURL}${'\n'}`; let script = document.createElement('script'); const existingScript = document.querySelector('script[nonce]'); if (existingScript && existingScript.nonce) { script.nonce = existingScript.nonce; } if (typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy) { script.textContent = trustedTypes.defaultPolicy.createScript(textContent); } else { script.textContent = textContent; } (document.head || document.documentElement).appendChild(script); script.remove(); script = null; const style = document.createElement('style'); const sourceURLMainCSS = 'debug://tabview-youtube/tabview.main.css'; style.textContent = `${styles['main'].trim()}${'\n\n'}/*# sourceURL=${sourceURLMainCSS} */${'\n'}`; document.documentElement.appendChild(style); })(); window.YouTubePlusDOMUtils = (() => { 'use strict'; const logError = (module, message, error) => { console.error(`[YouTube+][${module}] ${message}:`, error); }; const createElement = (tag, props = {}, children = []) => { const validTags = /^[a-z][a-z0-9-]*$/i; if (!validTags.test(tag)) { logError('createElement', 'Invalid tag name', new Error(`Tag "${tag}" is not allowed`)); return document.createElement('div'); } const element = document.createElement(tag); Object.entries(props).forEach(([key, value]) => { if (key === 'className') { element.className = value; } else if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else if (key.startsWith('on') && typeof value === 'function') { element.addEventListener(key.substring(2).toLowerCase(), value); } else if (key === 'dataset' && typeof value === 'object') { Object.assign(element.dataset, value); } else if (key === 'innerHTML' || key === 'outerHTML') { logError( 'createElement', 'Direct HTML injection prevented', new Error('Use children array instead') ); } else { try { element.setAttribute(key, value); } catch (e) { logError('createElement', `Failed to set attribute ${key}`, e); } } }); children.forEach(child => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { element.appendChild(child); } }); return element; }; const createButton = ({ text, className, onClick, title, ariaLabel, disabled = false }) => { const button = createElement('button', { className: className || '', title: title || text, 'aria-label': ariaLabel || text, disabled, onClick, }); if (text) { button.textContent = text; } return button; }; const createIcon = (iconClass, title = '') => { return createElement('span', { className: `ytp-icon ${iconClass}`, 'aria-hidden': 'true', title, }); }; const selectorCache = new Map(); const CACHE_MAX_SIZE = 100; const CACHE_MAX_AGE = 10000; let cacheHits = 0; let cacheMisses = 0; const querySelector = (selector, nocache = false, parent = document) => { if (nocache) return parent.querySelector(selector); const cacheKey = `${selector}:${parent === document ? 'doc' : parent.id || 'custom'}`; const now = Date.now(); const cached = selectorCache.get(cacheKey); if (cached?.element?.isConnected && now - cached.timestamp < CACHE_MAX_AGE) { cacheHits++; return cached.element; } if (cached) { selectorCache.delete(cacheKey); } const element = parent.querySelector(selector); cacheMisses++; if (element) { if (selectorCache.size >= CACHE_MAX_SIZE) { const firstKey = selectorCache.keys().next().value; selectorCache.delete(firstKey); } selectorCache.set(cacheKey, { element, timestamp: now }); } return element; }; const querySelectorBatch = (selectors, parent = document) => { const results = {}; for (const selector of selectors) { results[selector] = querySelector(selector, false, parent); } return results; }; const clearSelectorCache = () => { selectorCache.clear(); cacheHits = 0; cacheMisses = 0; }; const getCacheStats = () => { const total = cacheHits + cacheMisses; return { size: selectorCache.size, hits: cacheHits, misses: cacheMisses, hitRate: total > 0 ? `${((cacheHits / total) * 100).toFixed(2)}%` : 'N/A', }; }; const waitForElement = (selector, timeout = 5000, parent = document.body) => { return new Promise((resolve, reject) => { if (!selector || typeof selector !== 'string') { reject(new Error('Selector must be a non-empty string')); return; } if (!parent || !(parent instanceof Element)) { reject(new Error('Parent must be a valid DOM element')); return; } try { const element = parent.querySelector(selector); if (element) { resolve( (element)); return; } } catch { reject(new Error(`Invalid selector: ${selector}`)); return; } const controller = new AbortController(); let observer = null; const timeoutId = setTimeout(() => { controller.abort(); if (observer) observer.disconnect(); reject(new Error(`Timeout waiting for element: ${selector}`)); }, timeout); observer = new MutationObserver(() => { try { const element = parent.querySelector(selector); if (element) { clearTimeout(timeoutId); observer.disconnect(); resolve( (element)); } } catch (e) { clearTimeout(timeoutId); observer.disconnect(); reject(e); } }); observer.observe(parent, { childList: true, subtree: true, }); }); }; const isElementVisible = element => { if (!element) return false; const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }; const getElementOffset = element => { if (!element) return { top: 0, left: 0 }; const rect = element.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; return { top: rect.top + scrollTop, left: rect.left + scrollLeft, }; }; const insertAfter = (newElement, referenceElement) => { if (!newElement || !referenceElement) return; referenceElement.parentNode?.insertBefore(newElement, referenceElement.nextSibling); }; const removeAllChildren = element => { if (!element) return; while (element.firstChild) { element.removeChild(element.firstChild); } }; const addClasses = (element, classes) => { if (!element || !Array.isArray(classes)) return; element.classList.add(...classes); }; const removeClasses = (element, classes) => { if (!element || !Array.isArray(classes)) return; element.classList.remove(...classes); }; const toggleClass = (element, className, force) => { if (!element || !className) return; element.classList.toggle(className, force); }; return { createElement, createButton, createIcon, querySelector, querySelectorBatch, clearSelectorCache, getCacheStats, waitForElement, isElementVisible, getElementOffset, insertAfter, removeAllChildren, addClasses, removeClasses, toggleClass, }; })(); window.YouTubePlusValidationUtils = (() => { 'use strict'; const isValidURL = url => { if (typeof url !== 'string') return false; try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol); } catch { return false; } }; const isValidEmail = email => { if (typeof email !== 'string') return false; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }; const isValidVideoId = videoId => { if (typeof videoId !== 'string') return false; return /^[a-zA-Z0-9_-]{11}$/.test(videoId); }; const isValidPlaylistId = playlistId => { if (typeof playlistId !== 'string') return false; return /^[a-zA-Z0-9_-]+$/.test(playlistId); }; const sanitizeHTML = html => { if (typeof html !== 'string') return ''; return html.replace(/[<>&"'\/`=]/g, char => { const entities = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', }; return entities[char] || char; }); }; const sanitizeFilename = filename => { if (typeof filename !== 'string') return ''; return filename.replace(/[^a-z0-9_\-\.]/gi, '_').substring(0, 255); }; const parseNumber = (value, defaultValue = 0, options = {}) => { const { min = -Infinity, max = Infinity, integer = false } = options; const num = Number(value); if (isNaN(num)) return defaultValue; let result = Math.max(min, Math.min(max, num)); if (integer) result = Math.floor(result); return result; }; const hasRequiredProps = (obj, requiredProps) => { if (!obj || typeof obj !== 'object') return false; return requiredProps.every(prop => prop in obj); }; const isNonEmptyString = str => { return typeof str === 'string' && str.trim().length > 0; }; const isNonEmptyArray = arr => { return Array.isArray(arr) && arr.length > 0; }; const isValidColor = color => { if (typeof color !== 'string') return false; if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color)) return true; if (/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/.test(color)) return true; const namedColors = ['red', 'blue', 'green', 'yellow', 'black', 'white', 'transparent']; return namedColors.includes(color.toLowerCase()); }; const isValidTimestamp = timestamp => { if (typeof timestamp !== 'string') return false; return /^(?:\d{1,2}:)?\d{1,2}:\d{2}$/.test(timestamp); }; const parseTimestamp = timestamp => { if (!isValidTimestamp(timestamp) && !/^\d+$/.test(timestamp)) return 0; const parts = String(timestamp).split(':').map(Number); if (parts.length === 1) return parts[0]; if (parts.length === 2) return parts[0] * 60 + parts[1]; if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; return 0; }; const formatTimestamp = (seconds, includeHours = false) => { if (typeof seconds !== 'number' || isNaN(seconds)) return '0:00'; const validSeconds = Math.max(0, Math.floor(seconds)); const hours = Math.floor(validSeconds / 3600); const minutes = Math.floor((validSeconds % 3600) / 60); const secs = validSeconds % 60; const pad = num => String(num).padStart(2, '0'); if (hours > 0 || includeHours) { return `${hours}:${pad(minutes)}:${pad(secs)}`; } return `${minutes}:${pad(secs)}`; }; const isValidQuality = quality => { if (typeof quality !== 'string') return false; const validQualities = [ '144p', '240p', '360p', '480p', '720p', '1080p', '1440p', '2160p', 'auto', ]; return validQualities.includes(quality.toLowerCase()); }; const clamp = (value, min, max) => { return Math.max(min, Math.min(max, value)); }; return { isValidURL, isValidEmail, isValidVideoId, isValidPlaylistId, sanitizeHTML, sanitizeFilename, parseNumber, hasRequiredProps, isNonEmptyString, isNonEmptyArray, isValidColor, isValidTimestamp, parseTimestamp, formatTimestamp, isValidQuality, clamp, }; })(); (function () { 'use strict'; const formatNumber = (num, locale = 'en-US') => { if (num === null || num === undefined) { return '0'; } const parsed = typeof num === 'string' ? parseFloat(num) : num; if (Number.isNaN(parsed)) { return '0'; } try { return new Intl.NumberFormat(locale).format(parsed); } catch { return parsed.toLocaleString('en-US'); } }; const parseNumberSafely = num => { if (num === null || num === undefined) return null; const parsed = typeof num === 'string' ? parseFloat(num) : num; return Number.isNaN(parsed) ? null : parsed; }; const tryIntlFormat = (num, decimals, locale) => { if (typeof Intl === 'undefined' || !Intl.NumberFormat) return null; try { return new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: decimals, }).format(num); } catch { return null; } }; const formatCompactManual = (num, decimals) => { const abs = Math.abs(num); const sign = num < 0 ? '-' : ''; if (abs >= 1e9) return `${sign}${(abs / 1e9).toFixed(decimals)}B`; if (abs >= 1e6) return `${sign}${(abs / 1e6).toFixed(decimals)}M`; if (abs >= 1e3) return `${sign}${(abs / 1e3).toFixed(decimals)}K`; return num.toString(); }; const formatCompactNumber = (num, decimals = 1, locale = 'en-US') => { const parsed = parseNumberSafely(num); if (parsed === null) return '0'; const intlResult = tryIntlFormat(parsed, decimals, locale); if (intlResult) return intlResult; return formatCompactManual(parsed, decimals); }; const formatNumberWithExact = (num, decimals = 1) => { const full = formatNumber(num); const short = formatCompactNumber(num, decimals); return { short, full }; }; const formatBytes = (bytes, decimals = 2) => { if (bytes === 0 || bytes === null || bytes === undefined) { return '0 Bytes'; } const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); const size = parseFloat((bytes / Math.pow(k, i)).toFixed(dm)); return `${size} ${sizes[i]}`; }; const formatDuration = (seconds, includeHours = false) => { if (seconds === null || seconds === undefined || Number.isNaN(seconds)) { return '0:00'; } const totalSeconds = Math.floor(Math.abs(seconds)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const secs = totalSeconds % 60; const parts = []; if (hours > 0 || includeHours) { parts.push(hours.toString()); parts.push(minutes.toString().padStart(2, '0')); } else { parts.push(minutes.toString()); } parts.push(secs.toString().padStart(2, '0')); return parts.join(':'); }; const parseDuration = duration => { if (!duration || typeof duration !== 'string') { return 0; } if (duration.startsWith('PT')) { const hoursMatch = duration.match(/(\d+)H/); const minutesMatch = duration.match(/(\d+)M/); const secondsMatch = duration.match(/(\d+)S/); const hours = hoursMatch ? parseInt(hoursMatch[1], 10) : 0; const minutes = minutesMatch ? parseInt(minutesMatch[1], 10) : 0; const seconds = secondsMatch ? parseInt(secondsMatch[1], 10) : 0; return hours * 3600 + minutes * 60 + seconds; } const parts = duration.split(':').map(p => parseInt(p, 10)); if (parts.some(p => Number.isNaN(p))) { return 0; } if (parts.length === 3) { return parts[0] * 3600 + parts[1] * 60 + parts[2]; } if (parts.length === 2) { return parts[0] * 60 + parts[1]; } if (parts.length === 1) { return parts[0]; } return 0; }; const formatPercentage = (value, decimals = 1) => { if (value === null || value === undefined || Number.isNaN(value)) { return '0%'; } return `${value.toFixed(decimals)}%`; }; const formatRatioAsPercentage = (numerator, denominator, decimals = 1) => { if (!denominator || denominator === 0) { return '0%'; } const percentage = (numerator / denominator) * 100; return formatPercentage(percentage, decimals); }; const clamp = (value, min, max) => { return Math.min(Math.max(value, min), max); }; const parseNumber = (value, defaultValue = 0) => { if (typeof value === 'number') { return Number.isNaN(value) ? defaultValue : value; } if (typeof value !== 'string') { return defaultValue; } const cleaned = value.replace(/[^\d.-]/g, ''); const parsed = parseFloat(cleaned); return Number.isNaN(parsed) ? defaultValue : parsed; }; const parseOrdinalNumber = num => { const parsed = typeof num === 'string' ? parseInt(num, 10) : num; return Number.isNaN(parsed) ? 0 : parsed; }; const getIntlOrdinal = (num, locale) => { if (typeof Intl === 'undefined' || !Intl.PluralRules) return null; try { const pr = new Intl.PluralRules(locale, { type: 'ordinal' }); const rule = pr.select(num); const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th', }; return `${num}${suffixes[rule] || 'th'}`; } catch { return null; } }; const getEnglishOrdinalSuffix = num => { const j = num % 10; const k = num % 100; if (j === 1 && k !== 11) return 'st'; if (j === 2 && k !== 12) return 'nd'; if (j === 3 && k !== 13) return 'rd'; return 'th'; }; const formatOrdinal = (num, locale = 'en') => { const parsed = parseOrdinalNumber(num); if (parsed === 0) return '0'; const intlResult = getIntlOrdinal(parsed, locale); if (intlResult) return intlResult; return `${parsed}${getEnglishOrdinalSuffix(parsed)}`; }; const formatWithUnit = (count, singular, plural = null) => { const formatted = formatNumber(count); const unit = count === 1 ? singular : plural || `${singular}s`; return `${formatted} ${unit}`; }; const NumberUtils = { formatNumber, formatCompactNumber, formatNumberWithExact, formatBytes, formatDuration, parseDuration, formatPercentage, formatRatioAsPercentage, clamp, parseNumber, formatOrdinal, formatWithUnit, }; if (typeof window !== 'undefined') { window.YouTubePlusNumberUtils = NumberUtils; } if (typeof module !== 'undefined' && module.exports) { module.exports = NumberUtils; } })(); (function () { 'use strict'; const tryExtractFromParams = url => { try { const urlObj = new URL(url, window.location.origin); const vParam = urlObj.searchParams.get('v'); return vParam && isValidVideoId(vParam) ? vParam : null; } catch { return null; } }; const tryExtractFromPatterns = url => { const patterns = [ /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/, /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/, /youtube\.com\/v\/([a-zA-Z0-9_-]{11})/, /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/, ]; for (const pattern of patterns) { const match = url.match(pattern); if (match?.[1]) return match[1]; } return null; }; const extractVideoId = (url = window.location.href) => { if (!url || typeof url !== 'string') return null; try { const fromParams = tryExtractFromParams(url); if (fromParams) return fromParams; return tryExtractFromPatterns(url); } catch (error) { console.error('[URLUtils] Error extracting video ID:', error); return null; } }; const channelPatterns = [ { type: 'channel', regex: /^\/channel\/([a-zA-Z0-9_-]+)/, prefix: '' }, { type: 'handle', regex: /^\/@([a-zA-Z0-9_-]+)/, prefix: '@' }, { type: 'custom', regex: /^\/c\/([a-zA-Z0-9_-]+)/, prefix: '' }, { type: 'user', regex: /^\/user\/([a-zA-Z0-9_-]+)/, prefix: '' }, ]; const tryMatchChannelPattern = (pathname, pattern) => { const match = pathname.match(pattern.regex); if (match && match[1]) { return { type: pattern.type, id: pattern.prefix ? `${pattern.prefix}${match[1]}` : match[1], }; } return null; }; const extractChannelIdentifier = (url = window.location.href) => { if (!url || typeof url !== 'string') { return null; } try { const urlObj = new URL(url, window.location.origin); const { pathname } = urlObj; for (const pattern of channelPatterns) { const result = tryMatchChannelPattern(pathname, pattern); if (result) return result; } return null; } catch (error) { console.error('[URLUtils] Error extracting channel identifier:', error); return null; } }; const extractPlaylistId = (url = window.location.href) => { if (!url || typeof url !== 'string') { return null; } try { const urlObj = new URL(url, window.location.origin); const listParam = urlObj.searchParams.get('list'); if (listParam && isValidPlaylistId(listParam)) { return listParam; } return null; } catch (error) { console.error('[URLUtils] Error extracting playlist ID:', error); return null; } }; const extractShortsId = (url = window.location.href) => { if (!url || typeof url !== 'string') { return null; } try { const match = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/); return match && match[1] ? match[1] : null; } catch (error) { console.error('[URLUtils] Error extracting shorts ID:', error); return null; } }; const isValidVideoId = id => { return typeof id === 'string' && /^[a-zA-Z0-9_-]{11}$/.test(id); }; const isValidPlaylistId = id => { return typeof id === 'string' && /^(PL|FL|UU|LL|RD|OL)[a-zA-Z0-9_-]+$/.test(id); }; const isValidChannelId = id => { return typeof id === 'string' && /^UC[a-zA-Z0-9_-]{22}$/.test(id); }; const isWatchPage = (url = window.location.href) => { try { const urlObj = new URL(url, window.location.origin); return urlObj.pathname === '/watch' && urlObj.searchParams.has('v'); } catch { return false; } }; const isChannelPage = (url = window.location.href) => { try { const urlObj = new URL(url, window.location.origin); const { pathname } = urlObj; return ( pathname.startsWith('/channel/') || pathname.startsWith('/@') || pathname.startsWith('/c/') || pathname.startsWith('/user/') ); } catch { return false; } }; const isShortsPage = (url = window.location.href) => { try { const urlObj = new URL(url, window.location.origin); return urlObj.pathname.startsWith('/shorts/'); } catch { return false; } }; const isStudioPage = (url = window.location.href) => { try { const urlObj = new URL(url, window.location.origin); return urlObj.hostname === 'studio.youtube.com'; } catch { return false; } }; const buildWatchUrl = (videoId, params = {}) => { if (!isValidVideoId(videoId)) { throw new Error('Invalid video ID'); } const url = new URL('https://www.youtube.com/watch'); url.searchParams.set('v', videoId); for (const [key, value] of Object.entries(params)) { if (value !== null && value !== undefined) { url.searchParams.set(key, String(value)); } } return url.toString(); }; const buildChannelUrl = (identifier, type = 'channel') => { if (!identifier || typeof identifier !== 'string') { throw new Error('Invalid channel identifier'); } const baseUrl = 'https://www.youtube.com'; switch (type) { case 'channel': return `${baseUrl}/channel/${identifier}`; case 'handle': return `${baseUrl}/@${identifier.replace(/^@/, '')}`; case 'custom': return `${baseUrl}/c/${identifier}`; case 'user': return `${baseUrl}/user/${identifier}`; default: throw new Error(`Unknown channel URL type: ${type}`); } }; const buildThumbnailUrl = (videoId, quality = 'hqdefault') => { if (!isValidVideoId(videoId)) { throw new Error('Invalid video ID'); } const validQualities = ['maxresdefault', 'sddefault', 'hqdefault', 'mqdefault', 'default']; const q = validQualities.includes(quality) ? quality : 'hqdefault'; return `https://i.ytimg.com/vi/${videoId}/${q}.jpg`; }; const parseUrlParams = (url = window.location.href) => { try { const urlObj = new URL(url, window.location.origin); const params = {}; for (const [key, value] of urlObj.searchParams.entries()) { params[key] = value; } return params; } catch (error) { console.error('[URLUtils] Error parsing URL params:', error); return {}; } }; const getPageType = () => { const url = window.location.href; if (isStudioPage(url)) return 'studio'; if (isWatchPage(url)) return 'watch'; if (isShortsPage(url)) return 'shorts'; if (isChannelPage(url)) return 'channel'; if (window.location.pathname === '/results') return 'search'; if (window.location.pathname === '/' || window.location.pathname === '') return 'home'; return 'unknown'; }; const sanitizeUrl = (url, removeParams = ['feature', 'si', 'kw', 'pp']) => { try { const urlObj = new URL(url, window.location.origin); for (const param of removeParams) { urlObj.searchParams.delete(param); } return urlObj.toString(); } catch (error) { console.error('[URLUtils] Error sanitizing URL:', error); return url; } }; const URLUtils = { extractVideoId, extractChannelIdentifier, extractPlaylistId, extractShortsId, isValidVideoId, isValidPlaylistId, isValidChannelId, isWatchPage, isChannelPage, isShortsPage, isStudioPage, buildWatchUrl, buildChannelUrl, buildThumbnailUrl, parseUrlParams, getPageType, sanitizeUrl, }; if (typeof window !== 'undefined') { window.YouTubePlusURLUtils = URLUtils; } if (typeof module !== 'undefined' && module.exports) { module.exports = URLUtils; } })(); (function () { 'use strict'; const DEFAULT_TIMEOUT = 10000; const DEFAULT_MAX_RETRIES = 3; const DEFAULT_BASE_DELAY = 1000; const fetchWithTimeout = async (url, options = {}, timeoutMs = DEFAULT_TIMEOUT) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal, }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error(`Request timeout after ${timeoutMs}ms`); } throw error; } }; const fetchWithRetry = async ( url, options = {}, maxRetries = DEFAULT_MAX_RETRIES, baseDelay = DEFAULT_BASE_DELAY, timeoutMs = DEFAULT_TIMEOUT ) => { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetchWithTimeout(url, options, timeoutMs); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response; } catch (error) { lastError = error; if (attempt === maxRetries) { break; } const delay = baseDelay * Math.pow(2, attempt); const jitter = Math.random() * 0.3 * delay; const totalDelay = delay + jitter; await new Promise(resolve => setTimeout(resolve, totalDelay)); } } throw new Error( `Failed after ${maxRetries + 1} attempts: ${lastError?.message || 'Unknown error'}` ); }; const fetchJSON = async (url, options = {}, timeoutMs = DEFAULT_TIMEOUT) => { const response = await fetchWithTimeout(url, options, timeoutMs); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new Error('Response is not JSON'); } return response.json(); }; const fetchText = async (url, options = {}, timeoutMs = DEFAULT_TIMEOUT) => { const response = await fetchWithTimeout(url, options, timeoutMs); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.text(); }; const fetchHTML = async (url, options = {}, timeoutMs = DEFAULT_TIMEOUT) => { const html = await fetchText(url, options, timeoutMs); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); if (!doc) { throw new Error('Failed to parse HTML'); } return doc; }; const gmFetch = (url, options = {}) => { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest === 'undefined' && typeof GM?.xmlHttpRequest === 'undefined') { reject(new Error('GM_xmlhttpRequest not available')); return; } const gmRequest = typeof GM_xmlhttpRequest === 'undefined' ? GM.xmlHttpRequest : GM_xmlhttpRequest; const { method = 'GET', headers = {}, body = null, timeout = DEFAULT_TIMEOUT } = options; gmRequest({ method, url, headers, data: body, timeout, onload: response => { resolve({ ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText, headers: response.responseHeaders, text: () => Promise.resolve(response.responseText), json: () => Promise.resolve(JSON.parse(response.responseText)), }); }, onerror: error => { reject(new Error(`GM fetch failed: ${error}`)); }, ontimeout: () => { reject(new Error(`GM fetch timeout after ${timeout}ms`)); }, }); }); }; const batchFetch = async (urls, options = {}, concurrency = 5, onProgress = null) => { const results = new Array(urls.length); let completed = 0; let index = 0; const fetchNext = async () => { const currentIndex = index++; if (currentIndex >= urls.length) return; try { const response = await fetchWithTimeout(urls[currentIndex], options); results[currentIndex] = { success: true, data: response }; } catch (error) { results[currentIndex] = { success: false, error }; } completed++; if (onProgress) { onProgress(completed, urls.length); } await fetchNext(); }; const workers = Array.from({ length: Math.min(concurrency, urls.length) }, () => fetchNext()); await Promise.all(workers); return results; }; const isUrlReachable = async (url, timeoutMs = 5000) => { try { const response = await fetchWithTimeout(url, { method: 'HEAD' }, timeoutMs); return response.ok; } catch { return false; } }; const downloadBlob = async (url, options = {}, timeoutMs = DEFAULT_TIMEOUT) => { const response = await fetchWithTimeout(url, options, timeoutMs); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.blob(); }; const FetchUtils = { fetchWithTimeout, fetchWithRetry, fetchJSON, fetchText, fetchHTML, gmFetch, batchFetch, isUrlReachable, downloadBlob, DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY, }; if (typeof window !== 'undefined') { window.YouTubePlusFetchUtils = FetchUtils; } if (typeof module !== 'undefined' && module.exports) { module.exports = FetchUtils; } })(); (function () { 'use strict'; const loadSettings = (storageKey, schema, defaults = {}) => { try { const saved = localStorage.getItem(storageKey); if (!saved) { return { ...defaults }; } const parsed = JSON.parse(saved); if (typeof parsed !== 'object' || parsed === null) { console.warn('[SettingsUtils] Invalid settings format, using defaults'); return { ...defaults }; } const validated = { ...defaults }; for (const [key, fieldSchema] of Object.entries(schema)) { if (!(key in parsed)) { continue; } const value = parsed[key]; const validatedValue = validateField(value, fieldSchema, defaults[key]); if (validatedValue !== undefined) { validated[key] = validatedValue; } } return validated; } catch (error) { console.error('[SettingsUtils] Error loading settings:', error); return { ...defaults }; } }; const isValidType = (value, expectedType) => { if (!expectedType) return true; const valueType = Array.isArray(value) ? 'array' : typeof value; return valueType === expectedType; }; const isValidNumber = (value, schema) => { if (typeof schema.min === 'number' && value < schema.min) return false; if (typeof schema.max === 'number' && value > schema.max) return false; return true; }; const isValidString = (value, schema) => { if (typeof schema.minLength === 'number' && value.length < schema.minLength) return false; if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) return false; return true; }; const isValidEnum = (value, enumValues) => { if (!enumValues || !Array.isArray(enumValues)) return true; return enumValues.includes(value); }; const validateField = (value, schema, defaultValue) => { if (!isValidType(value, schema.type)) { return defaultValue; } if (schema.validator && typeof schema.validator === 'function') { if (!schema.validator(value)) { return defaultValue; } } if (schema.type === 'number' && !isValidNumber(value, schema)) { return defaultValue; } if (schema.type === 'string' && !isValidString(value, schema)) { return defaultValue; } if (!isValidEnum(value, schema.enum)) { return defaultValue; } return value; }; const validateSettingsAgainstSchema = (settings, schema) => { const validated = {}; for (const [key, value] of Object.entries(settings)) { if (!schema[key]) { validated[key] = value; continue; } const validatedValue = validateField(value, schema[key], value); if (validatedValue === undefined) { console.warn(`[SettingsUtils] Invalid value for ${key}, skipping`); } else { validated[key] = validatedValue; } } return validated; }; const saveSettings = (storageKey, settings, schema = null) => { try { if (typeof settings !== 'object' || settings === null) { throw new Error('Settings must be an object'); } if (schema) { const validatedSettings = validateSettingsAgainstSchema(settings, schema); Object.assign(settings, validatedSettings); } localStorage.setItem(storageKey, JSON.stringify(settings)); return true; } catch (error) { console.error('[SettingsUtils] Error saving settings:', error); return false; } }; const updateSetting = (storageKey, key, value, schema = null) => { try { const settings = loadSettings(storageKey, schema || {}); if (schema && schema[key]) { const validated = validateField(value, schema[key], settings[key]); if (validated === undefined) { console.warn(`[SettingsUtils] Invalid value for ${key}`); return false; } settings[key] = validated; } else { settings[key] = value; } return saveSettings(storageKey, settings, schema); } catch (error) { console.error('[SettingsUtils] Error updating setting:', error); return false; } }; const getSetting = (storageKey, key, defaultValue = null) => { try { const settings = loadSettings(storageKey, {}); return key in settings ? settings[key] : defaultValue; } catch (error) { console.error('[SettingsUtils] Error getting setting:', error); return defaultValue; } }; const deleteSetting = (storageKey, key) => { try { const settings = loadSettings(storageKey, {}); delete settings[key]; return saveSettings(storageKey, settings); } catch (error) { console.error('[SettingsUtils] Error deleting setting:', error); return false; } }; const resetSettings = (storageKey, defaults = {}) => { try { return saveSettings(storageKey, defaults); } catch (error) { console.error('[SettingsUtils] Error resetting settings:', error); return false; } }; const hasSettings = storageKey => { try { return localStorage.getItem(storageKey) !== null; } catch { return false; } }; const migrateSettings = (oldKey, newKey, transformer = null) => { try { const oldSettings = localStorage.getItem(oldKey); if (!oldSettings) { return false; } let settings = JSON.parse(oldSettings); if (transformer && typeof transformer === 'function') { settings = transformer(settings); } localStorage.setItem(newKey, JSON.stringify(settings)); localStorage.removeItem(oldKey); return true; } catch (error) { console.error('[SettingsUtils] Error migrating settings:', error); return false; } }; const exportSettings = (storageKey, pretty = false) => { try { const settings = loadSettings(storageKey, {}); return JSON.stringify(settings, null, pretty ? 2 : 0); } catch (error) { console.error('[SettingsUtils] Error exporting settings:', error); return null; } }; const importSettings = (storageKey, jsonString, schema = null) => { try { const settings = JSON.parse(jsonString); if (typeof settings !== 'object' || settings === null) { throw new Error('Invalid settings format'); } return saveSettings(storageKey, settings, schema); } catch (error) { console.error('[SettingsUtils] Error importing settings:', error); return false; } }; const watchSettings = (storageKey, callback, interval = 1000) => { let lastValue = localStorage.getItem(storageKey); let stopped = false; const poll = () => { if (stopped) return; const currentValue = localStorage.getItem(storageKey); if (currentValue !== lastValue) { try { const oldSettings = lastValue ? JSON.parse(lastValue) : {}; const newSettings = currentValue ? JSON.parse(currentValue) : {}; callback(newSettings, oldSettings); lastValue = currentValue; } catch (error) { console.error('[SettingsUtils] Error in settings watcher:', error); } } setTimeout(poll, interval); }; poll(); return () => { stopped = true; }; }; const createSchemaBuilder = () => { return { boolean: (defaultValue = false) => ({ type: 'boolean', default: defaultValue, }), string: (defaultValue = '', options = {}) => ({ type: 'string', default: defaultValue, ...options, }), number: (defaultValue = 0, options = {}) => ({ type: 'number', default: defaultValue, ...options, }), enum: (allowedValues, defaultValue) => ({ type: typeof defaultValue, enum: allowedValues, default: defaultValue, }), object: (defaultValue = {}) => ({ type: 'object', default: defaultValue, }), array: (defaultValue = []) => ({ type: 'array', default: defaultValue, }), }; }; const SettingsUtils = { loadSettings, saveSettings, validateField, updateSetting, getSetting, deleteSetting, resetSettings, hasSettings, migrateSettings, exportSettings, importSettings, watchSettings, createSchemaBuilder, }; if (typeof window !== 'undefined') { window.YouTubePlusSettingsUtils = SettingsUtils; } if (typeof module !== 'undefined' && module.exports) { module.exports = SettingsUtils; } })(); window.YouTubePlusErrorRecovery = (() => { 'use strict'; const RECOVERY_STRATEGIES = { RELOAD: 'reload', CLEAR_STORAGE: 'clear_storage', RESET_STATE: 'reset_state', NOTIFY_USER: 'notify_user', }; function determineStrategy(error, context = {}) { if (!error) return RECOVERY_STRATEGIES.NOTIFY_USER; const errorMsg = error.message || ''; const errorType = context.type || ''; if (errorMsg.includes('localStorage') || errorMsg.includes('storage')) { return RECOVERY_STRATEGIES.CLEAR_STORAGE; } if (errorMsg.includes('querySelector') || errorMsg.includes('DOM')) { return RECOVERY_STRATEGIES.RESET_STATE; } if (errorType === 'unhandledRejection' || errorMsg.includes('fatal')) { return RECOVERY_STRATEGIES.RELOAD; } return RECOVERY_STRATEGIES.NOTIFY_USER; } function clearStorage() { try { const keysToKeep = ['youtube-plus-settings', 'youtube-plus-user-prefs']; const allKeys = Object.keys(localStorage); allKeys.forEach(key => { if (!keysToKeep.includes(key) && key.startsWith('youtube-plus-')) { localStorage.removeItem(key); } }); return true; } catch (err) { console.error('[YouTube+][Recovery] Failed to clear storage:', err); return false; } } function resetState() { try { if (window.YouTubePlusDOMUtils?.clearSelectorCache) { window.YouTubePlusDOMUtils.clearSelectorCache(); } if (window.YouTubeUtils) { window.YouTubeUtils.resetFlags?.(); } return true; } catch (err) { console.error('[YouTube+][Recovery] Failed to reset state:', err); return false; } } function notifyUser(error, context = {}) { const message = `An error occurred: ${error.message || 'Unknown error'}`; console.error('[YouTube+][Error]', message, context); if (window.YouTubeUtils?.showNotification) { window.YouTubeUtils.showNotification(message, 'error'); } } function reloadPage(delay = 1000) { console.log('[YouTube+][Recovery] Reloading page in', delay, 'ms'); setTimeout(() => { window.location.reload(); }, delay); } function executeStrategy(strategy, error, context = {}) { console.log('[YouTube+][Recovery] Executing strategy:', strategy); switch (strategy) { case RECOVERY_STRATEGIES.CLEAR_STORAGE: if (clearStorage()) { notifyUser(new Error('Storage cleared due to error'), context); return true; } return false; case RECOVERY_STRATEGIES.RESET_STATE: if (resetState()) { notifyUser(new Error('Application state reset'), context); return true; } return false; case RECOVERY_STRATEGIES.RELOAD: notifyUser(new Error('Reloading page to recover from error'), context); reloadPage(); return true; case RECOVERY_STRATEGIES.NOTIFY_USER: default: notifyUser(error, context); return false; } } function attemptRecovery(error, context = {}) { const strategy = determineStrategy(error, context); return executeStrategy(strategy, error, context); } function isRecoverable(error) { if (!error) return false; const fatalErrors = ['SecurityError', 'CSP', 'TrustedTypes']; const errorMsg = error.message || ''; return !fatalErrors.some(fatal => errorMsg.includes(fatal)); } return { RECOVERY_STRATEGIES, determineStrategy, clearStorage, resetState, notifyUser, reloadPage, executeStrategy, attemptRecovery, isRecoverable, }; })(); // Embedded translations for offline/immediate use window.YouTubePlusEmbeddedTranslations = {"en":{"settingsTitle":"Settings","basicTab":"Basic","advancedTab":"Advanced","experimentalTab":"Experimental","reportTab":"Report","aboutTab":"About","closeButton":"Close","saveChanges":"Save Changes","settingsSaved":"Settings saved","speedControl":"Speed Control","speedControlDesc":"Add speed control buttons to video player","screenshotButton":"Screenshot Button","screenshotButtonDesc":"Add screenshot capture button to video player","downloadButton":"Download Button","downloadButtonDesc":"Add download button with multiple site options to video player","customDownloader":"Use custom downloader","customDownload":"Use my downloader","directDownload":"Direct Download","directDownloadDesc":"Enable the built-in direct download modal","directDownloadModuleNotAvailable":"Direct download module not available","videoTab":"VIDEO (.mp4/.webm)","audioTab":"AUDIO (.mp3)","subtitleTab":"SUBTITLES (.srt/.txt)","embedThumbnail":"Embed thumbnail","startingDownload":"Starting download...","starting":"Starting...","completed":"Completed","downloadCompleted":"Download completed!","downloadFailed":"Download failed:","subtitleDownloading":"Downloading subtitle...","subtitleDownloaded":"Subtitle downloaded!","subtitleDownloadFailed":"Subtitle download failed:","loading":"Loading...","noSubtitles":"No subtitles available","subtitleLoadError":"Error loading subtitles","autoTranslateSuffix":" (Auto-translate)","vp9Label":"VP9 (Higher Quality)","noSubtitleSelected":"No subtitle selected","zeroBytesError":"Downloaded file is 0 bytes","unknownSize":"—","alwaysEnabled":"Always enabled - GitHub repository","siteName":"Site name","urlTemplate":"URL template (use {videoId} or {videoUrl})","saveButton":"Save","resetButton":"Reset","y2mateSettingsSaved":"Y2Mate settings saved","y2mateReset":"Y2Mate reset to defaults","youtubeSettings":"YouTube + Settings","takeScreenshot":"Take screenshot","downloadOptions":"Download options","byYTDL":"by YTDL","copiedToClipboard":"Video URL copied to clipboard!","endscreenHideLabel":"Hide End Screens & Cards/Logo","endscreenHideDesc":"Remove end screen suggestions and info cards/channel logo","removedSuffix":" ({n} removed)","scrollToTop":"Scroll to top","resumePlayback":"Resume playback?","resume":"Resume","startOver":"Start over","stats":"Stats","channelStats":"Channel Stats","videoStats":"Video Stats","liveStats":"Live Stats","shortsStats":"Shorts Stats","close":"Close","channel":"Channel","live":"Live","shorts":"Shorts","statisticsButton":"Statistics Button","statisticsButtonDescription":"Show statistics button in video player","displayOptions":"Display Options","channelStatsTitle":"Channel Stats","channelStatsDescription":"Show live subscriber/views/videos overlay on channel banner","subscribers":"Subscribers","views":"Views","videos":"Videos","fontFamily":"Font Family","fontSize":"Font Size","updateInterval":"Update Interval","backgroundOpacity":"Background Opacity","overlayAriaLabel":"YouTube Channel Statistics Overlay","videoStatistics":"Video Statistics","channelStatistics":"Channel Statistics","loadingStats":"Loading statistics...","failedToLoadStats":"Failed to load statistics","likes":"Likes","dislikes":"Dislikes","comments":"Comments","liveViewers":"Live Viewers","totalViews":"Total Views","totalVideos":"Total Videos","settingsAriaLabel":"Open settings menu","settingsMenuAriaLabel":"Statistics display settings","unknown":"Unknown","privacy":"Privacy","monetization":"Monetization","country":"Country","duration":"Duration","yes":"Yes","no":"No","paidPromotion":"Paid Promotion","updateAvailableTitle":"YouTube + Update Available","version":"Version","newFeatures":"New features and improvements","installUpdate":"Install Update","later":"Later","enhancedExperience":"Enhanced YouTube experience with powerful features","currentVersion":"Current Version","lastChecked":"Last checked","latestAvailable":"Latest available","updateAvailable":"Update Available","installing":"Installing...","checkingForUpdates":"Checking for updates...","upToDate":"Up to date","checkForUpdates":"Check for Updates","autoCheckUpdates":"Automatically check for updates","updatePageFallback":"Opening update page...","never":"Never","justNow":"Just now","daysAgo":"{n}d ago","hoursAgo":"{n}h ago","minutesAgo":"{n}m ago","updateAvailableMsg":"Update {version} available!","upToDateMsg":"You're using the latest version ({version})","updateCheckFailed":"Update check failed: {msg}","noUpdateInfo":"No update information available","dismiss":"Dismiss","adBlocker":"Ad Blocker","adBlockerDescription":"Skip ads and remove ad elements automatically","seekBackward":"Seek backward","seekForward":"Seek forward","volumeUp":"Volume up","volumeDown":"Volume down","muteUnmute":"Mute/Unmute","showHideHelp":"Show/Hide help","keyboardShortcuts":"Keyboard Shortcuts","resetAll":"Reset All","resetAllConfirm":"Reset all shortcuts?","shortcutsReset":"Shortcuts reset","editShortcut":"Edit","pressAnyKey":"Press any key to set as new shortcut","current":"Current","cancel":"Cancel","keyAlreadyUsed":"Key \"{key}\" already used","shortcutUpdated":"Shortcut updated","toggleCaptions":"Toggle subtitles","captionsOn":"Subtitles: On","captionsOff":"Subtitles: Off","captionsUnavailable":"Subtitles unavailable","pipTitle":"Picture-in-Picture","pipDescription":"Add Picture-in-Picture functionality with keyboard shortcut","pipShortcutTitle":"PiP Keyboard Shortcut","pipShortcutDescription":"Customize keyboard combination to toggle PiP mode","none":"None","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"Search in {playlist}...","shortTitle":"Short title (one line)","emailOptional":"Your email (optional)","descriptionPlaceholder":"Describe the issue, steps to reproduce, expected vs actual","includeDebug":"Include debug info (version, URL, settings)","openGitHub":"Open GitHub Issue","copyReport":"Copy Report","prepareEmail":"Prepare Email","typeBug":"Bug / Error","typeFeature":"Feature Request","typeOther":"Other","titleRequired":"Title is required","titleMin":"Title must be at least 5 characters","descRequired":"Description is required","descMin":"Description must be at least 10 characters","invalidEmail":"Invalid email format","fixErrorsPrefix":"Please fix the following errors:\\n• ","opening":"Opening...","copying":"Copying...","copied":"Copied!","openingGithubNotification":"Opening GitHub in a new tab","failedOpenGithub":"Failed to open GitHub issue","reportCopied":"Report copied to clipboard","copyFailed":"Copy failed — please copy manually","thumbnailPreview":"Thumbnail Preview","clickToOpen":"Click to open in new tab","download":"Download","thumbnailLoadFailed":"Failed to load image","commentManager":"Comment Manager","deleteSelected":"Delete Selected","selectAll":"Select All","clearAll":"Clear All","selectComment":"Select comment","togglePanel":"Toggle panel","commentManagerControls":"Comment manager controls","commentManagement":"Comment Management","enableCommentManager":"Enable comment manager","bulkDeleteDescription":"Add checkboxes and bulk delete functionality to your comments","timecodes":"Timecodes","noTimecodesFound":"No timecodes found","clickToAdd":"Click + to add current time","reload":"Reload timecodes","add":"+ Add","export":"Export","tracking":"Tracking","track":"Track","timePlaceholder":"Time (e.g., 1:30)","labelPlaceholder":"Label (optional)","enableTimecode":"Enable Timecode Panel","keyboardShortcut":"Keyboard Shortcut","enableDescription":"Enable video timecode/chapter panel with quick navigation","shortcutDescription":"Customize keyboard combination to toggle Timecode Panel","foundTimecodes":"Found timecodes: {count}","cannotDeleteChapter":"Cannot delete YouTube chapters","invalidTimeFormat":"Invalid time format","timecodeDeleted":"Timecode deleted","timecodeUpdated":"Timecode updated","timecodeAdded":"Timecode added","noTimecodesToExport":"No timecodes to export","timecodesCopied":"Timecodes copied to clipboard","edit":"Edit","delete":"Delete","confirmDelete":"Delete timecode \"{label}\"?","reloadError":"Error reloading timecodes","cannotEditChapter":"Cannot edit YouTube chapters"},"ru":{"settingsTitle":"Настройки","basicTab":"Основные","advancedTab":"Расширенные","experimentalTab":"Экспериментальные","reportTab":"Отчет","aboutTab":"О программе","closeButton":"Закрыть","saveChanges":"Сохранить изменения","settingsSaved":"Настройки сохранены","speedControl":"Управление скоростью","speedControlDesc":"Добавить кнопки управления скоростью в видеоплеер","screenshotButton":"Кнопка скриншота","screenshotButtonDesc":"Добавить кнопку создания скриншота в видеоплеер","downloadButton":"Кнопка загрузки","downloadButtonDesc":"Добавить кнопку загрузки с несколькими вариантами сайтов в видеоплеер","customDownloader":"Использовать свой загрузчик","customDownload":"Использовать мой загрузчик","directDownload":"Прямая загрузка","directDownloadDesc":"Включить встроенный модуль прямой загрузки","directDownloadModuleNotAvailable":"Модуль прямой загрузки недоступен","videoTab":"ВИДЕО (.mp4/.webm)","audioTab":"АУДИО (.mp3)","subtitleTab":"СУБТИТРЫ (.srt/.txt)","embedThumbnail":"Встроить миниатюру","startingDownload":"Запуск загрузки...","starting":"Запуск...","completed":"Выполнено","downloadCompleted":"Загрузка завершена!","downloadFailed":"Ошибка загрузки:","subtitleDownloading":"Скачивание субтитров...","subtitleDownloaded":"Субтитры загружены!","subtitleDownloadFailed":"Ошибка загрузки субтитров:","loading":"Загрузка...","noSubtitles":"Субтитры недоступны","subtitleLoadError":"Ошибка загрузки субтитров","autoTranslateSuffix":" (Автоперевод)","vp9Label":"VP9 (Более высокое качество)","noSubtitleSelected":"Субтитры не выбраны","zeroBytesError":"Загруженный файл имеет размер 0 байт","unknownSize":"—","alwaysEnabled":"Всегда включено - репозиторий GitHub","siteName":"Название сайта","urlTemplate":"Шаблон URL (используйте {videoId} или {videoUrl})","saveButton":"Сохранить","resetButton":"Сбросить","y2mateSettingsSaved":"Настройки Y2Mate сохранены","y2mateReset":"Y2Mate сброшен к значениям по умолчанию","youtubeSettings":"Настройки YouTube +","takeScreenshot":"Сделать скриншот","downloadOptions":"Варианты загрузки","byYTDL":"by YTDL","copiedToClipboard":"URL видео скопирован в буфер обмена!","endscreenHideLabel":"Скрыть конечные экраны и карточки/логотип","endscreenHideDesc":"Удалить предложения конечного экрана и информационные карточки/логотип канала","removedSuffix":" ({n} удалено)","scrollToTop":"Прокрутить вверх","resumePlayback":"Продолжить воспроизведение?","resume":"Продолжить","startOver":"Начать сначала","stats":"Статистика","channelStats":"Статистика канала","videoStats":"Статистика видео","liveStats":"Статистика трансляции","shortsStats":"Статистика Shorts","close":"Закрыть","channel":"Канал","live":"Эфир","shorts":"Shorts","statisticsButton":"Кнопка статистики","displayOptions":"Параметры отображения","channelStatsTitle":"Статистика канала","subscribers":"Подписчики","views":"Просмотры","videos":"Видео","fontFamily":"Шрифт","fontSize":"Размер шрифта","updateInterval":"Интервал обновления","backgroundOpacity":"Прозрачность фона","overlayAriaLabel":"Наложение статистики канала YouTube","settingsAriaLabel":"Открыть меню настроек","settingsMenuAriaLabel":"Настройки отображения статистики","unknown":"Неизвестно","videoStatistics":"Статистика видео","channelStatistics":"Статистика канала","loadingStats":"Загрузка статистики...","failedToLoadStats":"Не удалось загрузить статистику","likes":"Лайки","dislikes":"Дизлайки","comments":"Комментарии","liveViewers":"Зрителей онлайн","totalViews":"Всего просмотров","totalVideos":"Всего видео","monetization":"Монетизация","country":"Страна","duration":"Длительность","yes":"Да","no":"Нет","paidPromotion":"Платная реклама","updateAvailableTitle":"Доступно обновление YouTube +","version":"Версия","newFeatures":"Новые функции и улучшения","installUpdate":"Установить обновление","later":"Позже","enhancedExperience":"Расширенные возможности YouTube с мощными функциями","currentVersion":"Текущая версия","lastChecked":"Последняя проверка","latestAvailable":"Доступна версия","updateAvailable":"Доступно обновление","installing":"Установка...","checkingForUpdates":"Проверка обновлений...","upToDate":"Актуальная версия","checkForUpdates":"Проверить обновления","autoCheckUpdates":"Автоматически проверять обновления","updatePageFallback":"Открываю страницу обновления...","never":"Никогда","justNow":"Только что","daysAgo":"{n}д","hoursAgo":"{n}ч","minutesAgo":"{n}м","updateAvailableMsg":"Доступно обновление {version}!","upToDateMsg":"Вы используете последнюю версию ({version})","updateCheckFailed":"Проверка обновлений не удалась: {msg}","noUpdateInfo":"Информация об обновлении недоступна","dismiss":"Закрыть","privacy":"Конфиденциальность","statisticsButtonDescription":"Показать кнопку статистики в видеоплеере","channelStatsDescription":"Показать наложение с подписчиками/просмотрами/видео на баннере канала","adBlocker":"Блокировщик рекламы","adBlockerDescription":"Автоматически пропускать рекламу и удалять рекламные элементы","seekBackward":"Перемотка назад","seekForward":"Перемотка вперёд","volumeUp":"Громче","volumeDown":"Тише","muteUnmute":"Вкл/Выкл звук","showHideHelp":"Показать/Скрыть помощь","keyboardShortcuts":"Клавиатурные сочетания","resetAll":"Сбросить всё","resetAllConfirm":"Сбросить все сочетания клавиш?","shortcutsReset":"Сочетания клавиш сброшены","editShortcut":"Изменить","pressAnyKey":"Нажмите любую клавишу для установки нового сочетания","current":"Текущее","cancel":"Отмена","keyAlreadyUsed":"Клавиша \"{key}\" уже используется","shortcutUpdated":"Сочетание клавиш обновлено","toggleCaptions":"Переключить субтитры","captionsOn":"Субтитры: Вкл","captionsOff":"Субтитры: Выкл","captionsUnavailable":"Субтитры недоступны","pipTitle":"Картинка в картинке","pipDescription":"Добавляет функцию «Картинка в картинке» с клавишной комбинацией","pipShortcutTitle":"Клавишная комбинация PiP","pipShortcutDescription":"Настройка клавиатурной комбинации для переключения режима PiP","none":"Нет","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"Поиск в плейлисте \"{playlist}\"...","shortTitle":"Краткий заголовок (в одну строку)","emailOptional":"Ваш email (необязательно)","descriptionPlaceholder":"Опишите проблему, шаги для воспроизведения, ожидаемый и фактический результат","includeDebug":"Включить отладочную информацию (версия, URL, настройки)","openGitHub":"Открыть заявку на GitHub","copyReport":"Копировать отчет","prepareEmail":"Подготовить письмо","typeBug":"Ошибка","typeFeature":"Запрос функции","typeOther":"Другое","titleRequired":"Требуется заголовок","titleMin":"Заголовок должен быть не менее 5 символов","descRequired":"Требуется описание","descMin":"Описание должно быть не менее 10 символов","invalidEmail":"Неправильный формат email","fixErrorsPrefix":"Пожалуйста, исправьте следующие ошибки:\\n• ","opening":"Открываю...","copying":"Копирую...","copied":"Скопировано!","openingGithubNotification":"Открываю GitHub в новой вкладке","failedOpenGithub":"Не удалось открыть заявку на GitHub","reportCopied":"Отчет скопирован в буфер обмена","copyFailed":"Копирование не удалось — пожалуйста, скопируйте вручную","thumbnailPreview":"Предпросмотр миниатюры","clickToOpen":"Нажмите, чтобы открыть в новой вкладке","download":"Скачать","thumbnailLoadFailed":"Не удалось загрузить изображение","commentManager":"Менеджер комментариев","deleteSelected":"Удалить выбранные","selectAll":"Выбрать все","clearAll":"Очистить все","selectComment":"Выбрать комментарий","togglePanel":"Переключить панель","commentManagerControls":"Управление менеджером комментариев","commentManagement":"Управление комментариями","enableCommentManager":"Включить менеджер комментариев","bulkDeleteDescription":"Добавить чекбоксы и функцию массового удаления к вашим комментариям","timecodes":"Таймкоды","noTimecodesFound":"Таймкоды не найдены","clickToAdd":"Нажмите + чтобы добавить текущее время","reload":"Обновить таймкоды","add":"+ Добавить","export":"Экспорт","tracking":"Отслеживание","track":"Отслеживать","timePlaceholder":"Время (например, 1:30)","labelPlaceholder":"Метка (необязательно)","enableTimecode":"Включить панель таймкодов","keyboardShortcut":"Горячая клавиша","enableDescription":"Включить панель таймкодов/глав с быстрым переходом","shortcutDescription":"Настройте комбинацию клавиш для переключения панели таймкодов","foundTimecodes":"Найдено таймкодов: {count}","cannotDeleteChapter":"Нельзя удалить главы YouTube","invalidTimeFormat":"Неверный формат времени","timecodeDeleted":"Таймкод удалён","timecodeUpdated":"Таймкод обновлён","timecodeAdded":"Таймкод добавлен","noTimecodesToExport":"Нет таймкодов для экспорта","timecodesCopied":"Таймкоды скопированы в буфер обмена","edit":"Редактировать","delete":"Удалить","confirmDelete":"Удалить таймкод \"{label}\"?","reloadError":"Ошибка при обновлении таймкодов","cannotEditChapter":"Нельзя редактировать главы YouTube"},"kr":{"settingsTitle":"설정","basicTab":"기본","advancedTab":"고급","experimentalTab":"실험적","reportTab":"보고서","aboutTab":"정보","closeButton":"닫기","saveChanges":"변경사항 저장","settingsSaved":"설정 저장됨","speedControl":"속도 조절","speedControlDesc":"비디오 플레이어에 속도 조절 버튼 추가","screenshotButton":"스크린샷 버튼","screenshotButtonDesc":"비디오 플레이어에 스크린샷 캡처 버튼 추가","downloadButton":"다운로드 버튼","downloadButtonDesc":"여러 사이트 옵션이 있는 다운로드 버튼을 비디오 플레이어에 추가","customDownloader":"사용자 정의 다운로더 사용","customDownload":"내 다운로더 사용","directDownload":"직접 다운로드","directDownloadDesc":"내장 직접 다운로드 모달 활성화","directDownloadModuleNotAvailable":"직접 다운로드 모듈을 사용할 수 없습니다","videoTab":"비디오 (.mp4/.webm)","audioTab":"오디오 (.mp3)","subtitleTab":"자막 (.srt/.txt)","embedThumbnail":"썸네일 포함","startingDownload":"다운로드 시작 중...","starting":"시작 중...","completed":"완료됨","downloadCompleted":"다운로드 완료!","downloadFailed":"다운로드 실패:","subtitleDownloading":"자막 다운로드 중...","subtitleDownloaded":"자막 다운로드됨!","subtitleDownloadFailed":"자막 다운로드 실패:","loading":"로딩 중...","noSubtitles":"사용 가능한 자막 없음","subtitleLoadError":"자막 로딩 오류","autoTranslateSuffix":" (자동 번역)","vp9Label":"VP9 (더 높은 품질)","noSubtitleSelected":"자막이 선택되지 않음","zeroBytesError":"다운로드된 파일이 0바이트입니다","unknownSize":"—","alwaysEnabled":"항상 활성화 - GitHub 저장소","siteName":"사이트 이름","urlTemplate":"URL 템플릿 ({videoId} 또는 {videoUrl} 사용)","saveButton":"저장","resetButton":"초기화","y2mateSettingsSaved":"Y2Mate 설정 저장됨","y2mateReset":"Y2Mate가 기본값으로 초기화됨","youtubeSettings":"YouTube + 설정","takeScreenshot":"스크린샷 찍기","downloadOptions":"다운로드 옵션","byYTDL":"by YTDL","copiedToClipboard":"비디오 URL이 클립보드에 복사되었습니다!","endscreenHideLabel":"엔드 스크린 및 카드/로고 숨기기","endscreenHideDesc":"엔드 스크린 제안 및 정보 카드/채널 로고 제거","removedSuffix":" ({n}개 제거됨)","scrollToTop":"맨 위로 스크롤","resumePlayback":"재생을 계속하시겠습니까?","resume":"계속","startOver":"처음부터","stats":"통계","channelStats":"채널 통계","videoStats":"비디오 통계","liveStats":"라이브 통계","shortsStats":"Shorts 통계","close":"닫기","channel":"채널","live":"라이브","shorts":"Shorts","statisticsButton":"통계 버튼","statisticsButtonDescription":"빠른 접근을 위해 비디오 및 채널 메뉴에 통계 버튼 표시","displayOptions":"표시 옵션","channelStatsTitle":"채널 통계","channelStatsDescription":"채널 배너에 실시간 구독자/조회수/비디오 오버레이 표시","subscribers":"구독자","views":"조회수","videos":"비디오","fontFamily":"글꼴","fontSize":"글꼴 크기","updateInterval":"업데이트 간격","backgroundOpacity":"배경 투명도","overlayAriaLabel":"YouTube 채널 통계 오버레이","videoStatistics":"비디오 통계","channelStatistics":"채널 통계","loadingStats":"통계 로딩 중...","failedToLoadStats":"통계 로드 실패","likes":"좋아요","dislikes":"싫어요","comments":"댓글","liveViewers":"실시간 시청자","totalViews":"총 조회수","totalVideos":"총 비디오 수","settingsAriaLabel":"설정 메뉴 열기","settingsMenuAriaLabel":"통계 표시 설정","unknown":"알 수 없음","privacy":"개인정보 보호","monetization":"수익 창출","country":"국가","duration":"재생 시간","yes":"예","no":"아니오","paidPromotion":"유료 프로모션","updateAvailableTitle":"YouTube + 업데이트 사용 가능","version":"버전","newFeatures":"새로운 기능 및 개선사항","installUpdate":"업데이트 설치","later":"나중에","enhancedExperience":"강력한 기능으로 향상된 YouTube 경험","currentVersion":"현재 버전","lastChecked":"마지막 확인","latestAvailable":"사용 가능한 최신 버전","updateAvailable":"업데이트 사용 가능","installing":"설치 중...","checkingForUpdates":"업데이트 확인 중...","upToDate":"최신 버전","checkForUpdates":"업데이트 확인","autoCheckUpdates":"자동으로 업데이트 확인","updatePageFallback":"업데이트 페이지 열기...","never":"안 함","justNow":"방금","daysAgo":"{n}일 전","hoursAgo":"{n}시간 전","minutesAgo":"{n}분 전","updateAvailableMsg":"업데이트 {version} 사용 가능!","upToDateMsg":"최신 버전({version})을 사용 중입니다","updateCheckFailed":"업데이트 확인 실패: {msg}","noUpdateInfo":"업데이트 정보가 없습니다","dismiss":"닫기","adBlocker":"광고 차단","adBlockerDescription":"자동으로 광고 건너뛰기 및 광고 요소 제거","seekBackward":"뒤로 탐색","seekForward":"앞으로 탐색","volumeUp":"볼륨 높이기","volumeDown":"볼륨 낮추기","muteUnmute":"음소거/음소거 해제","showHideHelp":"도움말 표시/숨기기","keyboardShortcuts":"키보드 단축키","resetAll":"모두 초기화","resetAllConfirm":"모든 단축키를 초기화하시겠습니까?","shortcutsReset":"단축키 초기화됨","editShortcut":"편집","pressAnyKey":"새 단축키로 설정할 키를 누르세요","current":"현재","cancel":"취소","keyAlreadyUsed":"키 \"{key}\"는 이미 사용 중입니다","shortcutUpdated":"단축키 업데이트됨","toggleCaptions":"자막 토글","captionsOn":"자막: 켜짐","captionsOff":"자막: 꺼짐","captionsUnavailable":"자막 사용 불가","pipTitle":"PIP (화면 속 화면)","pipDescription":"키보드 단축키로 PIP 기능 추가","pipShortcutTitle":"PiP 키보드 단축키","pipShortcutDescription":"PiP 모드를 토글하기 위한 키보드 조합 사용자 정의","none":"없음","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"{playlist}에서 검색...","shortTitle":"짧은 제목 (한 줄)","emailOptional":"이메일 (선택사항)","descriptionPlaceholder":"문제, 재현 단계, 예상 결과와 실제 결과 설명","includeDebug":"디버그 정보 포함 (버전, URL, 설정)","openGitHub":"GitHub 이슈 열기","copyReport":"보고서 복사","prepareEmail":"이메일 준비","typeBug":"버그 / 오류","typeFeature":"기능 요청","typeOther":"기타","titleRequired":"제목이 필요합니다","titleMin":"제목은 최소 5자 이상이어야 합니다","descRequired":"설명이 필요합니다","descMin":"설명은 최소 10자 이상이어야 합니다","invalidEmail":"잘못된 이메일 형식","fixErrorsPrefix":"다음 오류를 수정하세요:\\n• ","opening":"열기 중...","copying":"복사 중...","copied":"복사됨!","openingGithubNotification":"새 탭에서 GitHub 열기","failedOpenGithub":"GitHub 이슈를 열 수 없습니다","reportCopied":"보고서가 클립보드에 복사됨","copyFailed":"복사 실패 — 수동으로 복사하세요","thumbnailPreview":"썸네일 미리보기","clickToOpen":"클릭하여 새 탭에서 열기","download":"다운로드","thumbnailLoadFailed":"이미지 로드 실패","commentManager":"댓글 관리자","deleteSelected":"선택 항목 삭제","selectAll":"모두 선택","clearAll":"모두 지우기","selectComment":"댓글 선택","togglePanel":"패널 토글","commentManagerControls":"댓글 관리자 컨트롤","commentManagement":"댓글 관리","enableCommentManager":"댓글 관리자 활성화","bulkDeleteDescription":"댓글에 체크박스 및 일괄 삭제 기능 추가","timecodes":"타임코드","noTimecodesFound":"타임코드를 찾을 수 없습니다","clickToAdd":"현재 시간을 추가하려면 +를 클릭하세요","reload":"타임코드 새로고침","add":"+ 추가","export":"내보내기","tracking":"추적 중","track":"추적","timePlaceholder":"시간 (예: 1:30)","labelPlaceholder":"레이블 (선택사항)","enableTimecode":"타임코드 패널 활성화","keyboardShortcut":"키보드 단축키","enableDescription":"빠른 탐색을 위한 비디오 타임코드/챕터 패널 활성화","shortcutDescription":"타임코드 패널을 토글하기 위한 키보드 조합 사용자 정의","foundTimecodes":"타임코드 발견: {count}개","cannotDeleteChapter":"YouTube 챕터는 삭제할 수 없습니다","invalidTimeFormat":"잘못된 시간 형식","timecodeDeleted":"타임코드 삭제됨","timecodeUpdated":"타임코드 업데이트됨","timecodeAdded":"타임코드 추가됨","noTimecodesToExport":"내보낼 타임코드가 없습니다","timecodesCopied":"타임코드가 클립보드에 복사됨","edit":"편집","delete":"삭제","confirmDelete":"타임코드 \"{label}\"를 삭제하시겠습니까?","reloadError":"타임코드 새로고침 오류","cannotEditChapter":"YouTube 챕터는 편집할 수 없습니다","save":"저장"},"fr":{"settingsTitle":"Paramètres","basicTab":"Base","advancedTab":"Avancé","experimentalTab":"Expérimental","reportTab":"Rapport","aboutTab":"À propos","closeButton":"Fermer","saveChanges":"Enregistrer les modifications","settingsSaved":"Paramètres enregistrés","speedControl":"Contrôle de vitesse","speedControlDesc":"Ajouter des boutons de contrôle de vitesse au lecteur vidéo","screenshotButton":"Bouton de capture","screenshotButtonDesc":"Ajouter un bouton de capture d'écran au lecteur vidéo","downloadButton":"Bouton de téléchargement","customDownloader":"Utiliser un téléchargeur personnalisé","customDownload":"Utiliser mon téléchargeur","directDownload":"Téléchargement direct","directDownloadDesc":"Activer la modale de téléchargement direct intégrée","directDownloadModuleNotAvailable":"Module de téléchargement direct non disponible","videoTab":"VIDÉO (.mp4/.webm)","audioTab":"AUDIO (.mp3)","subtitleTab":"SOUS-TITRES (.srt/.txt)","embedThumbnail":"Intégrer la miniature","startingDownload":"Démarrage du téléchargement...","starting":"Démarrage...","completed":"Terminé","downloadCompleted":"Téléchargement terminé!","downloadFailed":"Échec du téléchargement:","subtitleDownloading":"Téléchargement des sous-titres...","subtitleDownloaded":"Sous-titres téléchargés!","subtitleDownloadFailed":"Échec du téléchargement des sous-titres:","loading":"Chargement...","noSubtitles":"Aucun sous-titre disponible","subtitleLoadError":"Erreur lors du chargement des sous-titres","autoTranslateSuffix":" (Traduction automatique)","vp9Label":"VP9 (Qualité supérieure)","noSubtitleSelected":"Aucun sous-titre sélectionné","zeroBytesError":"Le fichier téléchargé fait 0 octets","unknownSize":"—","alwaysEnabled":"Toujours activé - Dépôt GitHub","siteName":"Nom du site","urlTemplate":"Modèle d'URL (utiliser {videoId} ou {videoUrl})","saveButton":"Enregistrer","resetButton":"Réinitialiser","y2mateSettingsSaved":"Paramètres Y2Mate enregistrés","y2mateReset":"Y2Mate réinitialisé aux valeurs par défaut","youtubeSettings":"Paramètres YouTube +","takeScreenshot":"Prendre une capture d'écran","downloadOptions":"Options de téléchargement","byYTDL":"by YTDL","copiedToClipboard":"URL de la vidéo copiée dans le presse-papiers!","endscreenHideLabel":"Masquer les écrans de fin et cartes/logo","endscreenHideDesc":"Supprimer les suggestions d'écran de fin et les cartes d'information/logo de chaîne","removedSuffix":" ({n} supprimé(s))","scrollToTop":"Faire défiler vers le haut","resumePlayback":"Reprendre la lecture?","resume":"Reprendre","startOver":"Recommencer","stats":"Statistiques","channelStats":"Statistiques de la chaîne","videoStats":"Statistiques de la vidéo","liveStats":"Statistiques en direct","shortsStats":"Statistiques Shorts","close":"Fermer","channel":"Chaîne","live":"Direct","shorts":"Shorts","statisticsButton":"Bouton de statistiques","statisticsButtonDescription":"Afficher le bouton de statistiques dans le lecteur vidéo","displayOptions":"Options d'affichage","channelStatsTitle":"Statistiques de la chaîne","channelStatsDescription":"Afficher la superposition en direct des abonnés/vues/vidéos sur la bannière de la chaîne","subscribers":"Abonnés","views":"Vues","videos":"Vidéos","fontFamily":"Police","fontSize":"Taille de police","updateInterval":"Intervalle de mise à jour","backgroundOpacity":"Opacité de l'arrière-plan","overlayAriaLabel":"Superposition de statistiques de chaîne YouTube","videoStatistics":"Statistiques de la vidéo","channelStatistics":"Statistiques de la chaîne","loadingStats":"Chargement des statistiques...","failedToLoadStats":"Échec du chargement des statistiques","likes":"J'aime","dislikes":"Je n'aime pas","comments":"Commentaires","liveViewers":"Spectateurs en direct","totalViews":"Vues totales","totalVideos":"Vidéos totales","settingsAriaLabel":"Ouvrir le menu des paramètres","settingsMenuAriaLabel":"Paramètres d'affichage des statistiques","unknown":"Inconnu","privacy":"Confidentialité","monetization":"Monétisation","country":"Pays","duration":"Durée","yes":"Oui","no":"Non","paidPromotion":"Promotion payante","updateAvailableTitle":"Mise à jour YouTube + disponible","version":"Version","newFeatures":"Nouvelles fonctionnalités et améliorations","installUpdate":"Installer la mise à jour","later":"Plus tard","enhancedExperience":"Expérience YouTube améliorée avec des fonctionnalités puissantes","currentVersion":"Version actuelle","lastChecked":"Dernière vérification","latestAvailable":"Dernière disponible","updateAvailable":"Mise à jour disponible","installing":"Installation...","checkingForUpdates":"Vérification des mises à jour...","upToDate":"À jour","checkForUpdates":"Vérifier les mises à jour","autoCheckUpdates":"Vérifier automatiquement les mises à jour","updatePageFallback":"Ouverture de la page de mise à jour...","never":"Jamais","justNow":"À l'instant","daysAgo":"il y a {n}j","hoursAgo":"il y a {n}h","minutesAgo":"il y a {n}min","updateAvailableMsg":"Mise à jour {version} disponible!","upToDateMsg":"Vous utilisez la dernière version ({version})","updateCheckFailed":"Échec de la vérification des mises à jour: {msg}","noUpdateInfo":"Aucune information de mise à jour disponible","dismiss":"Ignorer","adBlocker":"Bloqueur de publicités","adBlockerDescription":"Ignorer automatiquement les publicités et supprimer les éléments publicitaires","seekBackward":"Reculer","seekForward":"Avancer","volumeUp":"Augmenter le volume","volumeDown":"Baisser le volume","muteUnmute":"Activer/désactiver le son","showHideHelp":"Afficher/masquer l'aide","keyboardShortcuts":"Raccourcis clavier","resetAll":"Tout réinitialiser","resetAllConfirm":"Réinitialiser tous les raccourcis?","shortcutsReset":"Raccourcis réinitialisés","editShortcut":"Modifier","pressAnyKey":"Appuyez sur n'importe quelle touche pour définir un nouveau raccourci","current":"Actuel","cancel":"Annuler","keyAlreadyUsed":"La touche \"{key}\" est déjà utilisée","shortcutUpdated":"Raccourci mis à jour","toggleCaptions":"Basculer les sous-titres","captionsOn":"Sous-titres: Activés","captionsOff":"Sous-titres: Désactivés","captionsUnavailable":"Sous-titres non disponibles","pipTitle":"Image dans l'image","pipDescription":"Ajouter la fonctionnalité Image dans l'image avec raccourci clavier","pipShortcutTitle":"Raccourci clavier PiP","pipShortcutDescription":"Personnaliser la combinaison de touches pour basculer le mode PiP","none":"Aucun","ctrl":"Ctrl","alt":"Alt","shift":"Maj","searchPlaceholder":"Rechercher dans {playlist}...","shortTitle":"Titre court (une ligne)","emailOptional":"Votre email (facultatif)","descriptionPlaceholder":"Décrivez le problème, les étapes de reproduction, résultat attendu vs réel","includeDebug":"Inclure les informations de débogage (version, URL, paramètres)","openGitHub":"Ouvrir un problème GitHub","copyReport":"Copier le rapport","prepareEmail":"Préparer l'email","typeBug":"Bug / Erreur","typeFeature":"Demande de fonctionnalité","typeOther":"Autre","titleRequired":"Le titre est requis","titleMin":"Le titre doit contenir au moins 5 caractères","descRequired":"La description est requise","descMin":"La description doit contenir au moins 10 caractères","invalidEmail":"Format d'email invalide","fixErrorsPrefix":"Veuillez corriger les erreurs suivantes:\\n• ","opening":"Ouverture...","copying":"Copie...","copied":"Copié!","openingGithubNotification":"Ouverture de GitHub dans un nouvel onglet","failedOpenGithub":"Impossible d'ouvrir le problème GitHub","reportCopied":"Rapport copié dans le presse-papiers","copyFailed":"Échec de la copie — veuillez copier manuellement","thumbnailPreview":"Aperçu de la miniature","clickToOpen":"Cliquer pour ouvrir dans un nouvel onglet","download":"Télécharger","thumbnailLoadFailed":"Échec du chargement de l'image","commentManager":"Gestionnaire de commentaires","deleteSelected":"Supprimer la sélection","selectAll":"Tout sélectionner","clearAll":"Tout effacer","selectComment":"Sélectionner le commentaire","togglePanel":"Basculer le panneau","commentManagerControls":"Contrôles du gestionnaire de commentaires","commentManagement":"Gestion des commentaires","enableCommentManager":"Activer le gestionnaire de commentaires","timecodes":"Timecodes","noTimecodesFound":"Aucun timecode trouvé","clickToAdd":"Cliquez sur + pour ajouter l'heure actuelle","reload":"Recharger les timecodes","add":"+ Ajouter","export":"Exporter","tracking":"Suivi","track":"Suivre","timePlaceholder":"Temps (ex: 1:30)","labelPlaceholder":"Libellé (facultatif)","enableTimecode":"Activer le panneau de timecode","keyboardShortcut":"Raccourci clavier","enableDescription":"Activer le panneau de timecode/chapitre vidéo avec navigation rapide","foundTimecodes":"Timecodes trouvés: {count}","cannotDeleteChapter":"Impossible de supprimer les chapitres YouTube","invalidTimeFormat":"Format de temps invalide","timecodeDeleted":"Timecode supprimé","timecodeUpdated":"Timecode mis à jour","timecodeAdded":"Timecode ajouté","noTimecodesToExport":"Aucun timecode à exporter","timecodesCopied":"Timecodes copiés dans le presse-papiers","edit":"Modifier","delete":"Supprimer","confirmDelete":"Supprimer le timecode \"{label}\"?","reloadError":"Erreur lors du rechargement des timecodes","cannotEditChapter":"Impossible de modifier les chapitres YouTube","save":"Enregistrer"},"du":{"settingsTitle":"Instellingen","basicTab":"Basis","advancedTab":"Geavanceerd","experimentalTab":"Experimenteel","reportTab":"Rapport","aboutTab":"Over","closeButton":"Sluiten","saveChanges":"Wijzigingen opslaan","settingsSaved":"Instellingen opgeslagen","speedControl":"Snelheidsregeling","speedControlDesc":"Voeg snelheidsregelingsknoppen toe aan de videospeler","screenshotButton":"Schermafbeeldingsknop","screenshotButtonDesc":"Voeg een schermafbeeldingsknop toe aan de videospeler","downloadButton":"Downloadknop","downloadButtonDesc":"Voeg een downloadknop met meerdere site-opties toe aan de videospeler","customDownloader":"Aangepaste downloader gebruiken","customDownload":"Mijn downloader gebruiken","directDownload":"Directe download","directDownloadDesc":"Schakel de ingebouwde directe downloadmodal in","directDownloadModuleNotAvailable":"Directe downloadmodule niet beschikbaar","videoTab":"VIDEO (.mp4/.webm)","audioTab":"AUDIO (.mp3)","subtitleTab":"ONDERTITELS (.srt/.txt)","embedThumbnail":"Miniatuur insluiten","startingDownload":"Download starten...","starting":"Starten...","completed":"Voltooid","downloadCompleted":"Download voltooid!","downloadFailed":"Download mislukt:","subtitleDownloading":"Ondertitels downloaden...","subtitleDownloaded":"Ondertitels gedownload!","subtitleDownloadFailed":"Ondertitels downloaden mislukt:","loading":"Laden...","noSubtitles":"Geen ondertitels beschikbaar","subtitleLoadError":"Fout bij laden ondertitels","autoTranslateSuffix":" (Automatische vertaling)","vp9Label":"VP9 (Hogere kwaliteit)","noSubtitleSelected":"Geen ondertitel geselecteerd","zeroBytesError":"Gedownload bestand is 0 bytes","unknownSize":"—","alwaysEnabled":"Altijd ingeschakeld - GitHub repository","siteName":"Sitenaam","urlTemplate":"URL-sjabloon (gebruik {videoId} of {videoUrl})","saveButton":"Opslaan","resetButton":"Resetten","y2mateSettingsSaved":"Y2Mate-instellingen opgeslagen","y2mateReset":"Y2Mate gereset naar standaardinstellingen","youtubeSettings":"YouTube + Instellingen","takeScreenshot":"Schermafbeelding maken","downloadOptions":"Downloadopties","byYTDL":"by YTDL","copiedToClipboard":"Video-URL gekopieerd naar klembord!","endscreenHideLabel":"Eindschermen en kaarten/logo verbergen","endscreenHideDesc":"Verwijder eindschermsuggesties en infokaarten/kanaallogo","removedSuffix":" ({n} verwijderd)","scrollToTop":"Naar boven scrollen","resumePlayback":"Afspelen hervatten?","resume":"Hervatten","startOver":"Opnieuw beginnen","stats":"Statistieken","channelStats":"Kanaalstatistieken","videoStats":"Videostatistieken","liveStats":"Live statistieken","shortsStats":"Shorts statistieken","close":"Sluiten","channel":"Kanaal","live":"Live","shorts":"Shorts","statisticsButton":"Statistiekenknop","displayOptions":"Weergaveopties","channelStatsTitle":"Kanaalstatistieken","channelStatsDescription":"Toon live abonnees/weergaven/video's overlay op kanaalbanner","subscribers":"Abonnees","views":"Weergaven","videos":"Video's","fontFamily":"Lettertype","fontSize":"Lettergrootte","updateInterval":"Update-interval","backgroundOpacity":"Achtergrondtransparantie","overlayAriaLabel":"YouTube Kanaalstatistieken Overlay","videoStatistics":"Videostatistieken","channelStatistics":"Kanaalstatistieken","loadingStats":"Statistieken laden...","failedToLoadStats":"Statistieken laden mislukt","likes":"Vind-ik-leuks","dislikes":"Vind-ik-niet-leuks","comments":"Reacties","liveViewers":"Live kijkers","totalViews":"Totale weergaven","totalVideos":"Totale video's","settingsAriaLabel":"Instellingenmenu openen","settingsMenuAriaLabel":"Statistiekweergave-instellingen","unknown":"Onbekend","monetization":"Monetisatie","country":"Land","duration":"Duur","yes":"Ja","no":"Nee","paidPromotion":"Betaalde promotie","updateAvailableTitle":"YouTube + Update beschikbaar","version":"Versie","newFeatures":"Nieuwe functies en verbeteringen","installUpdate":"Update installeren","later":"Later","enhancedExperience":"Verbeterde YouTube-ervaring met krachtige functies","currentVersion":"Huidige versie","lastChecked":"Laatst gecontroleerd","latestAvailable":"Laatste beschikbaar","updateAvailable":"Update beschikbaar","installing":"Installeren...","checkingForUpdates":"Controleren op updates...","upToDate":"Up-to-date","checkForUpdates":"Controleren op updates","autoCheckUpdates":"Automatisch controleren op updates","updatePageFallback":"Updatepagina openen...","never":"Nooit","justNow":"Zojuist","daysAgo":"{n}d geleden","hoursAgo":"{n}u geleden","minutesAgo":"{n}m geleden","updateAvailableMsg":"Update {version} beschikbaar!","upToDateMsg":"Je gebruikt de laatste versie ({version})","updateCheckFailed":"Updatecontrole mislukt: {msg}","noUpdateInfo":"Geen update-informatie beschikbaar","dismiss":"Sluiten","privacy":"Privacy","statisticsButtonDescription":"Toon statistiekenknop in videospeler","adBlocker":"Advertentieblokkeerder","adBlockerDescription":"Sla advertenties over en verwijder advertentie-elementen automatisch","seekBackward":"Achteruit zoeken","seekForward":"Vooruit zoeken","volumeUp":"Volume omhoog","volumeDown":"Volume omlaag","muteUnmute":"Dempen/Geluid aan","showHideHelp":"Help tonen/verbergen","keyboardShortcuts":"Sneltoetsen","resetAll":"Alles resetten","resetAllConfirm":"Alle sneltoetsen resetten?","shortcutsReset":"Sneltoetsen gereset","editShortcut":"Bewerken","pressAnyKey":"Druk op een toets om als nieuwe sneltoets in te stellen","current":"Huidig","cancel":"Annuleren","keyAlreadyUsed":"Toets \"{key}\" wordt al gebruikt","shortcutUpdated":"Sneltoets bijgewerkt","toggleCaptions":"Ondertitels in-/uitschakelen","captionsOn":"Ondertitels: Aan","captionsOff":"Ondertitels: Uit","captionsUnavailable":"Ondertitels niet beschikbaar","pipTitle":"Beeld-in-beeld","pipDescription":"Voeg Beeld-in-beeld functionaliteit toe met sneltoets","pipShortcutTitle":"PiP Sneltoets","pipShortcutDescription":"Pas toetsencombinatie aan om PiP-modus te schakelen","none":"Geen","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"Zoeken in {playlist}...","shortTitle":"Korte titel (één regel)","emailOptional":"Je e-mailadres (optioneel)","descriptionPlaceholder":"Beschrijf het probleem, stappen om te reproduceren, verwacht vs werkelijk","includeDebug":"Debug-info opnemen (versie, URL, instellingen)","openGitHub":"GitHub Issue openen","copyReport":"Rapport kopiëren","prepareEmail":"E-mail voorbereiden","typeBug":"Bug / Fout","typeFeature":"Functieverzoek","typeOther":"Overig","titleRequired":"Titel is verplicht","titleMin":"Titel moet minstens 5 tekens zijn","descRequired":"Beschrijving is verplicht","descMin":"Beschrijving moet minstens 10 tekens zijn","invalidEmail":"Ongeldig e-mailformaat","fixErrorsPrefix":"Los de volgende fouten op:\\n• ","opening":"Openen...","copying":"Kopiëren...","copied":"Gekopieerd!","openingGithubNotification":"GitHub openen in nieuw tabblad","failedOpenGithub":"Kan GitHub issue niet openen","reportCopied":"Rapport gekopieerd naar klembord","copyFailed":"Kopiëren mislukt — kopieer handmatig","thumbnailPreview":"Miniatuurvoorbeeld","clickToOpen":"Klik om in nieuw tabblad te openen","download":"Downloaden","thumbnailLoadFailed":"Afbeelding laden mislukt","commentManager":"Commentaarbeheerder","deleteSelected":"Geselecteerde verwijderen","selectAll":"Alles selecteren","clearAll":"Alles wissen","selectComment":"Commentaar selecteren","togglePanel":"Paneel schakelen","commentManagerControls":"Commentaarbeheerder besturingselementen","commentManagement":"Commentaarbeheer","enableCommentManager":"Commentaarbeheerder inschakelen","bulkDeleteDescription":"Voeg selectievakjes en bulkverwijdering toe aan je commentaren","timecodes":"Timecodes","noTimecodesFound":"Geen timecodes gevonden","clickToAdd":"Klik + om huidige tijd toe te voegen","reload":"Timecodes opnieuw laden","add":"+ Toevoegen","export":"Exporteren","tracking":"Volgen","track":"Volgen","timePlaceholder":"Tijd (bijv. 1:30)","labelPlaceholder":"Label (optioneel)","enableTimecode":"Timecode-paneel inschakelen","keyboardShortcut":"Sneltoets","enableDescription":"Schakel video timecode/hoofdstuk paneel in met snelle navigatie","shortcutDescription":"Pas toetsencombinatie aan om Timecode-paneel te schakelen","foundTimecodes":"Timecodes gevonden: {count}","cannotDeleteChapter":"Kan YouTube-hoofdstukken niet verwijderen","invalidTimeFormat":"Ongeldig tijdformaat","timecodeDeleted":"Timecode verwijderd","timecodeUpdated":"Timecode bijgewerkt","timecodeAdded":"Timecode toegevoegd","noTimecodesToExport":"Geen timecodes om te exporteren","timecodesCopied":"Timecodes gekopieerd naar klembord","edit":"Bewerken","delete":"Verwijderen","confirmDelete":"Timecode \"{label}\" verwijderen?","reloadError":"Fout bij opnieuw laden timecodes","cannotEditChapter":"Kan YouTube-hoofdstukken niet bewerken","save":"Opslaan"},"cn":{"settingsTitle":"设置","basicTab":"基本","advancedTab":"高级","experimentalTab":"实验性","reportTab":"报告","aboutTab":"关于","closeButton":"关闭","saveChanges":"保存更改","settingsSaved":"设置已保存","speedControl":"速度控制","speedControlDesc":"在视频播放器中添加速度控制按钮","screenshotButton":"截图按钮","screenshotButtonDesc":"在视频播放器中添加截图捕获按钮","downloadButton":"下载按钮","downloadButtonDesc":"在视频播放器中添加带有多个网站选项的下载按钮","customDownloader":"使用自定义下载器","customDownload":"使用我的下载器","directDownload":"直接下载","directDownloadDesc":"启用内置直接下载模态框","directDownloadModuleNotAvailable":"直接下载模块不可用","videoTab":"视频 (.mp4/.webm)","audioTab":"音频 (.mp3)","subtitleTab":"字幕 (.srt/.txt)","embedThumbnail":"嵌入缩略图","startingDownload":"开始下载...","starting":"开始中...","completed":"已完成","downloadCompleted":"下载完成!","downloadFailed":"下载失败:","subtitleDownloading":"下载字幕中...","subtitleDownloaded":"字幕已下载!","subtitleDownloadFailed":"字幕下载失败:","loading":"加载中...","noSubtitles":"无可用字幕","subtitleLoadError":"加载字幕时出错","autoTranslateSuffix":" (自动翻译)","vp9Label":"VP9 (更高质量)","noSubtitleSelected":"未选择字幕","zeroBytesError":"下载的文件为 0 字节","unknownSize":"—","alwaysEnabled":"始终启用 - GitHub 仓库","siteName":"网站名称","urlTemplate":"URL 模板(使用 {videoId} 或 {videoUrl})","saveButton":"保存","resetButton":"重置","y2mateSettingsSaved":"Y2Mate 设置已保存","y2mateReset":"Y2Mate 已重置为默认值","youtubeSettings":"YouTube + 设置","takeScreenshot":"截取屏幕截图","downloadOptions":"下载选项","byYTDL":"by YTDL","copiedToClipboard":"视频 URL 已复制到剪贴板!","endscreenHideLabel":"隐藏片尾屏幕和卡片/徽标","endscreenHideDesc":"移除片尾屏幕建议和信息卡片/频道徽标","removedSuffix":"(已移除 {n} 个)","scrollToTop":"滚动到顶部","resumePlayback":"恢复播放?","resume":"恢复","startOver":"重新开始","stats":"统计","channelStats":"频道统计","videoStats":"视频统计","liveStats":"直播统计","shortsStats":"Shorts 统计","close":"关闭","channel":"频道","live":"直播","shorts":"Shorts","statisticsButton":"统计按钮","statisticsButtonDescription":"在视频和频道菜单上显示统计按钮以快速访问统计信息","displayOptions":"显示选项","channelStatsTitle":"频道统计","channelStatsDescription":"在频道横幅上显示实时订阅者/观看次数/视频数覆盖层","subscribers":"订阅者","views":"观看次数","videos":"视频","fontFamily":"字体","fontSize":"字体大小","updateInterval":"更新间隔","backgroundOpacity":"背景不透明度","overlayAriaLabel":"YouTube 频道统计覆盖层","videoStatistics":"视频统计","channelStatistics":"频道统计","loadingStats":"加载统计中...","failedToLoadStats":"加载统计失败","likes":"点赞","dislikes":"不喜欢","comments":"评论","liveViewers":"直播观众","totalViews":"总观看次数","totalVideos":"总视频数","settingsAriaLabel":"打开设置菜单","settingsMenuAriaLabel":"统计显示设置","unknown":"未知","monetization":"盈利","country":"国家","duration":"时长","yes":"是","no":"否","paidPromotion":"付费推广","updateAvailableTitle":"YouTube + 更新可用","version":"版本","newFeatures":"新功能和改进","installUpdate":"安装更新","later":"稍后","enhancedExperience":"具有强大功能的增强型 YouTube 体验","currentVersion":"当前版本","lastChecked":"上次检查","latestAvailable":"最新可用","updateAvailable":"更新可用","installing":"正在安装...","checkingForUpdates":"正在检查更新...","upToDate":"已是最新","checkForUpdates":"检查更新","autoCheckUpdates":"自动检查更新","manualInstallHint":"无法自动打开更新。请从 GreasyFork 手动下载。","updatePageFallback":"正在打开更新页面...","never":"从不","justNow":"刚刚","daysAgo":"{n}天前","hoursAgo":"{n}小时前","minutesAgo":"{n}分钟前","updateAvailableMsg":"更新 {version} 可用!","upToDateMsg":"您正在使用最新版本 ({version})","updateCheckFailed":"更新检查失败:{msg}","noUpdateInfo":"没有可用的更新信息","dismiss":"关闭","adBlocker":"广告拦截器","adBlockerDescription":"自动跳过广告并移除广告元素","seekBackward":"后退","seekForward":"前进","volumeUp":"音量增加","volumeDown":"音量降低","muteUnmute":"静音/取消静音","showHideHelp":"显示/隐藏帮助","keyboardShortcuts":"键盘快捷键","resetAll":"全部重置","resetAllConfirm":"重置所有快捷键?","shortcutsReset":"快捷键已重置","editShortcut":"编辑","pressAnyKey":"按任意键设置为新快捷键","current":"当前","cancel":"取消","keyAlreadyUsed":"键 \"{key}\" 已被使用","shortcutUpdated":"快捷键已更新","toggleCaptions":"切换字幕","captionsOn":"字幕:开启","captionsOff":"字幕:关闭","captionsUnavailable":"字幕不可用","pipTitle":"画中画","pipDescription":"添加带键盘快捷键的画中画功能","pipShortcutTitle":"PiP 键盘快捷键","pipShortcutDescription":"自定义键盘组合以切换 PiP 模式","none":"无","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"在 {playlist} 中搜索...","shortTitle":"简短标题(一行)","emailOptional":"您的电子邮件(可选)","descriptionPlaceholder":"描述问题、重现步骤、预期与实际结果","includeDebug":"包含调试信息(版本、URL、设置)","openGitHub":"打开 GitHub Issue","copyReport":"复制报告","prepareEmail":"准备电子邮件","privacy":"提交即表示您同意包含所提供的信息。不要包含密码或个人令牌。","typeBug":"错误 / Bug","typeFeature":"功能请求","typeOther":"其他","titleRequired":"需要标题","titleMin":"标题必须至少 5 个字符","descRequired":"需要描述","descMin":"描述必须至少 10 个字符","invalidEmail":"无效的电子邮件格式","fixErrorsPrefix":"请修复以下错误:\\n• ","opening":"正在打开...","copying":"正在复制...","copied":"已复制!","openingGithubNotification":"在新标签页中打开 GitHub","failedOpenGithub":"无法打开 GitHub issue","reportCopied":"报告已复制到剪贴板","copyFailed":"复制失败 — 请手动复制","thumbnailPreview":"缩略图预览","clickToOpen":"点击在新标签页中打开","download":"下载","thumbnailLoadFailed":"加载图片失败","commentManager":"评论管理器","deleteSelected":"删除选中","selectAll":"全选","clearAll":"清除全部","selectComment":"选择评论","togglePanel":"切换面板","commentManagerControls":"评论管理器控件","commentManagement":"评论管理","enableCommentManager":"启用评论管理器","bulkDeleteDescription":"为您的评论添加复选框和批量删除功能","timecodes":"时间码","noTimecodesFound":"未找到时间码","clickToAdd":"点击 + 添加当前时间","reload":"重新加载时间码","add":"+ 添加","export":"导出","tracking":"跟踪中","track":"跟踪","timePlaceholder":"时间(例如:1:30)","labelPlaceholder":"标签(可选)","enableTimecode":"启用时间码面板","keyboardShortcut":"键盘快捷键","enableDescription":"启用带有快速导航的视频时间码/章节面板","shortcutDescription":"自定义键盘组合以切换时间码面板","foundTimecodes":"找到时间码:{count}","cannotDeleteChapter":"无法删除 YouTube 章节","invalidTimeFormat":"无效的时间格式","timecodeDeleted":"时间码已删除","timecodeUpdated":"时间码已更新","timecodeAdded":"时间码已添加","noTimecodesToExport":"没有要导出的时间码","timecodesCopied":"时间码已复制到剪贴板","edit":"编辑","delete":"删除","confirmDelete":"删除时间码 \"{label}\"?","reloadError":"重新加载时间码时出错","cannotEditChapter":"无法编辑 YouTube 章节","save":"保存"},"tw":{"settingsTitle":"設定","basicTab":"基本","advancedTab":"進階","experimentalTab":"實驗性","reportTab":"報告","aboutTab":"關於","closeButton":"關閉","saveChanges":"儲存變更","settingsSaved":"設定已儲存","speedControl":"速度控制","speedControlDesc":"在影片播放器中新增速度控制按鈕","screenshotButton":"截圖按鈕","screenshotButtonDesc":"在影片播放器中新增截圖擷取按鈕","downloadButton":"下載按鈕","downloadButtonDesc":"在影片播放器中新增具有多個網站選項的下載按鈕","customDownloader":"使用自訂下載器","customDownload":"使用我的下載器","directDownload":"直接下載","directDownloadDesc":"啟用內建直接下載模態框","directDownloadModuleNotAvailable":"直接下載模組不可用","videoTab":"影片 (.mp4/.webm)","audioTab":"音訊 (.mp3)","subtitleTab":"字幕 (.srt/.txt)","embedThumbnail":"嵌入縮圖","startingDownload":"開始下載...","starting":"開始中...","completed":"已完成","downloadCompleted":"下載完成!","downloadFailed":"下載失敗:","subtitleDownloading":"下載字幕中...","subtitleDownloaded":"字幕已下載!","subtitleDownloadFailed":"字幕下載失敗:","loading":"載入中...","noSubtitles":"無可用字幕","subtitleLoadError":"載入字幕時發生錯誤","autoTranslateSuffix":" (自動翻譯)","vp9Label":"VP9 (更高品質)","noSubtitleSelected":"未選擇字幕","zeroBytesError":"下載的檔案為 0 位元組","unknownSize":"—","alwaysEnabled":"始終啟用 - GitHub 儲存庫","siteName":"網站名稱","urlTemplate":"URL 範本(使用 {videoId} 或 {videoUrl})","saveButton":"儲存","resetButton":"重設","y2mateSettingsSaved":"Y2Mate 設定已儲存","y2mateReset":"Y2Mate 已重設為預設值","youtubeSettings":"YouTube + 設定","takeScreenshot":"擷取螢幕截圖","downloadOptions":"下載選項","byYTDL":"by YTDL","copiedToClipboard":"影片 URL 已複製到剪貼簿!","endscreenHideLabel":"隱藏片尾畫面和卡片/標誌","endscreenHideDesc":"移除片尾畫面建議和資訊卡片/頻道標誌","removedSuffix":"(已移除 {n} 個)","scrollToTop":"捲動到頂部","resumePlayback":"繼續播放?","resume":"繼續","startOver":"重新開始","stats":"統計","channelStats":"頻道統計","videoStats":"影片統計","liveStats":"直播統計","shortsStats":"Shorts 統計","close":"關閉","channel":"頻道","live":"直播","shorts":"Shorts","statisticsButton":"統計按鈕","statisticsButtonDescription":"在影片和頻道選單上顯示統計按鈕以快速存取統計資訊","displayOptions":"顯示選項","channelStatsTitle":"頻道統計","channelStatsDescription":"在頻道橫幅上顯示即時訂閱者/觀看次數/影片數覆蓋層","subscribers":"訂閱者","views":"觀看次數","videos":"影片","fontFamily":"字型","fontSize":"字型大小","updateInterval":"更新間隔","backgroundOpacity":"背景不透明度","overlayAriaLabel":"YouTube 頻道統計覆蓋層","videoStatistics":"影片統計","channelStatistics":"頻道統計","loadingStats":"載入統計中...","failedToLoadStats":"載入統計失敗","likes":"讚","dislikes":"不喜歡","comments":"留言","liveViewers":"直播觀眾","totalViews":"總觀看次數","totalVideos":"總影片數","settingsAriaLabel":"開啟設定選單","settingsMenuAriaLabel":"統計顯示設定","unknown":"未知","monetization":"營利","country":"國家","duration":"時長","yes":"是","no":"否","paidPromotion":"付費推廣","updateAvailableTitle":"YouTube + 更新可用","version":"版本","newFeatures":"新功能和改進","installUpdate":"安裝更新","later":"稍後","enhancedExperience":"具有強大功能的增強型 YouTube 體驗","currentVersion":"目前版本","lastChecked":"上次檢查","latestAvailable":"最新可用","updateAvailable":"更新可用","installing":"正在安裝...","checkingForUpdates":"正在檢查更新...","upToDate":"已是最新","checkForUpdates":"檢查更新","autoCheckUpdates":"自動檢查更新","manualInstallHint":"無法自動開啟更新。請從 GreasyFork 手動下載。","updatePageFallback":"正在開啟更新頁面...","never":"從不","justNow":"剛剛","daysAgo":"{n}天前","hoursAgo":"{n}小時前","minutesAgo":"{n}分鐘前","updateAvailableMsg":"更新 {version} 可用!","upToDateMsg":"您正在使用最新版本 ({version})","updateCheckFailed":"更新檢查失敗:{msg}","noUpdateInfo":"沒有可用的更新資訊","dismiss":"關閉","adBlocker":"廣告攔截器","adBlockerDescription":"自動略過廣告並移除廣告元素","seekBackward":"後退","seekForward":"前進","volumeUp":"音量增加","volumeDown":"音量降低","muteUnmute":"靜音/取消靜音","showHideHelp":"顯示/隱藏說明","keyboardShortcuts":"鍵盤快速鍵","resetAll":"全部重設","resetAllConfirm":"重設所有快速鍵?","shortcutsReset":"快速鍵已重設","editShortcut":"編輯","pressAnyKey":"按任意鍵設定為新快速鍵","current":"目前","cancel":"取消","keyAlreadyUsed":"鍵 \"{key}\" 已被使用","shortcutUpdated":"快速鍵已更新","toggleCaptions":"切換字幕","captionsOn":"字幕:開啟","captionsOff":"字幕:關閉","captionsUnavailable":"字幕不可用","pipTitle":"子母畫面","pipDescription":"新增具有鍵盤快速鍵的子母畫面功能","pipShortcutTitle":"PiP 鍵盤快速鍵","pipShortcutDescription":"自訂鍵盤組合以切換 PiP 模式","none":"無","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"在 {playlist} 中搜尋...","shortTitle":"簡短標題(一行)","emailOptional":"您的電子郵件(選填)","descriptionPlaceholder":"描述問題、重現步驟、預期與實際結果","includeDebug":"包含偵錯資訊(版本、URL、設定)","openGitHub":"開啟 GitHub Issue","copyReport":"複製報告","prepareEmail":"準備電子郵件","privacy":"提交即表示您同意包含所提供的資訊。不要包含密碼或個人權杖。","typeBug":"錯誤 / Bug","typeFeature":"功能請求","typeOther":"其他","titleRequired":"需要標題","titleMin":"標題必須至少 5 個字元","descRequired":"需要描述","descMin":"描述必須至少 10 個字元","invalidEmail":"無效的電子郵件格式","fixErrorsPrefix":"請修正以下錯誤:\\n• ","opening":"正在開啟...","copying":"正在複製...","copied":"已複製!","openingGithubNotification":"在新分頁中開啟 GitHub","failedOpenGithub":"無法開啟 GitHub issue","reportCopied":"報告已複製到剪貼簿","copyFailed":"複製失敗 — 請手動複製","thumbnailPreview":"縮圖預覽","clickToOpen":"點擊在新分頁中開啟","download":"下載","thumbnailLoadFailed":"載入圖片失敗","commentManager":"評論管理器","deleteSelected":"刪除選取","selectAll":"全選","clearAll":"清除全部","selectComment":"選擇評論","togglePanel":"切換面板","commentManagerControls":"評論管理器控制項","commentManagement":"評論管理","enableCommentManager":"啟用評論管理器","bulkDeleteDescription":"為您的評論新增核取方塊和批次刪除功能","timecodes":"時間碼","noTimecodesFound":"未找到時間碼","clickToAdd":"點擊 + 新增目前時間","reload":"重新載入時間碼","add":"+ 新增","export":"匯出","tracking":"追蹤中","track":"追蹤","timePlaceholder":"時間(例如:1:30)","labelPlaceholder":"標籤(選填)","enableTimecode":"啟用時間碼面板","keyboardShortcut":"鍵盤快速鍵","enableDescription":"啟用具有快速導覽的影片時間碼/章節面板","shortcutDescription":"自訂鍵盤組合以切換時間碼面板","foundTimecodes":"找到時間碼:{count}","cannotDeleteChapter":"無法刪除 YouTube 章節","invalidTimeFormat":"無效的時間格式","timecodeDeleted":"時間碼已刪除","timecodeUpdated":"時間碼已更新","timecodeAdded":"時間碼已新增","noTimecodesToExport":"沒有要匯出的時間碼","timecodesCopied":"時間碼已複製到剪貼簿","edit":"編輯","delete":"刪除","confirmDelete":"刪除時間碼 \"{label}\"?","reloadError":"重新載入時間碼時出錯","cannotEditChapter":"無法編輯 YouTube 章節","save":"儲存"},"jp":{"settingsTitle":"設定","basicTab":"基本","advancedTab":"高度","experimentalTab":"実験的","reportTab":"レポート","aboutTab":"について","closeButton":"閉じる","saveChanges":"変更を保存","settingsSaved":"設定が保存されました","speedControl":"速度制御","speedControlDesc":"ビデオプレーヤーに速度制御ボタンを追加","screenshotButton":"スクリーンショットボタン","screenshotButtonDesc":"ビデオプレーヤーにスクリーンショットキャプチャボタンを追加","downloadButton":"ダウンロードボタン","downloadButtonDesc":"ビデオプレーヤーに複数のサイトオプション付きダウンロードボタンを追加","customDownloader":"カスタムダウンローダーを使用","customDownload":"私のダウンローダーを使用","directDownload":"直接ダウンロード","directDownloadDesc":"組み込みの直接ダウンロードモーダルを有効にする","directDownloadModuleNotAvailable":"直接ダウンロードモジュールは利用できません","videoTab":"ビデオ (.mp4/.webm)","audioTab":"オーディオ (.mp3)","subtitleTab":"字幕 (.srt/.txt)","embedThumbnail":"サムネイルを埋め込む","startingDownload":"ダウンロードを開始しています...","starting":"開始中...","completed":"完了","downloadCompleted":"ダウンロード完了!","downloadFailed":"ダウンロード失敗:","subtitleDownloading":"字幕をダウンロード中...","subtitleDownloaded":"字幕がダウンロードされました!","subtitleDownloadFailed":"字幕のダウンロードに失敗しました:","loading":"読み込み中...","noSubtitles":"字幕はありません","subtitleLoadError":"字幕の読み込みエラー","autoTranslateSuffix":" (自動翻訳)","vp9Label":"VP9 (より高品質)","noSubtitleSelected":"字幕が選択されていません","zeroBytesError":"ダウンロードされたファイルは0バイトです","unknownSize":"—","alwaysEnabled":"常に有効 - GitHub リポジトリ","siteName":"サイト名","urlTemplate":"URL テンプレート({videoId} または {videoUrl} を使用)","saveButton":"保存","resetButton":"リセット","y2mateSettingsSaved":"Y2Mate 設定が保存されました","y2mateReset":"Y2Mate がデフォルトにリセットされました","youtubeSettings":"YouTube + 設定","takeScreenshot":"スクリーンショットを撮る","downloadOptions":"ダウンロードオプション","byYTDL":"by YTDL","copiedToClipboard":"動画URLがクリップボードにコピーされました!","endscreenHideLabel":"エンドスクリーンとカード/ロゴを非表示","endscreenHideDesc":"エンドスクリーンの提案と情報カード/チャンネルロゴを削除","removedSuffix":"({n}件削除)","scrollToTop":"トップにスクロール","resumePlayback":"再生を再開しますか?","resume":"再開","startOver":"最初から","stats":"統計","channelStats":"チャンネル統計","videoStats":"動画統計","liveStats":"ライブ統計","shortsStats":"Shorts統計","close":"閉じる","channel":"チャンネル","live":"ライブ","shorts":"Shorts","statisticsButton":"統計ボタン","statisticsButtonDescription":"動画とチャンネルメニューに統計ボタンを表示して素早くアクセス","displayOptions":"表示オプション","channelStatsTitle":"チャンネル統計","channelStatsDescription":"チャンネルバナーにライブの登録者/再生回数/動画数のオーバーレイを表示","subscribers":"登録者","views":"再生回数","videos":"動画","fontFamily":"フォント","fontSize":"フォントサイズ","updateInterval":"更新間隔","backgroundOpacity":"背景の不透明度","overlayAriaLabel":"YouTube チャンネル統計オーバーレイ","videoStatistics":"動画統計","channelStatistics":"チャンネル統計","loadingStats":"統計を読み込み中...","failedToLoadStats":"統計の読み込みに失敗しました","likes":"高評価","dislikes":"低評価","comments":"コメント","liveViewers":"ライブ視聴者","totalViews":"総再生回数","totalVideos":"総動画数","settingsAriaLabel":"設定メニューを開く","settingsMenuAriaLabel":"統計表示設定","unknown":"不明","privacy":"プライバシー","monetization":"収益化","country":"国","duration":"再生時間","yes":"はい","no":"いいえ","paidPromotion":"有料プロモーション","updateAvailableTitle":"YouTube + アップデート利用可能","version":"バージョン","newFeatures":"新機能と改善","installUpdate":"アップデートをインストール","later":"後で","enhancedExperience":"強力な機能による強化された YouTube 体験","currentVersion":"現在のバージョン","lastChecked":"最終チェック","latestAvailable":"最新利用可能","updateAvailable":"アップデート利用可能","installing":"インストール中...","checkingForUpdates":"アップデートを確認中...","upToDate":"最新です","checkForUpdates":"アップデートを確認","autoCheckUpdates":"自動的にアップデートを確認","updatePageFallback":"アップデートページを開いています...","never":"なし","justNow":"たった今","daysAgo":"{n}日前","hoursAgo":"{n}時間前","minutesAgo":"{n}分前","updateAvailableMsg":"アップデート {version} が利用可能です!","upToDateMsg":"最新バージョン ({version}) を使用しています","updateCheckFailed":"アップデートチェックに失敗しました:{msg}","noUpdateInfo":"更新情報がありません","dismiss":"閉じる","adBlocker":"広告ブロッカー","adBlockerDescription":"広告を自動的にスキップし、広告要素を削除","seekBackward":"後退","seekForward":"前進","volumeUp":"音量を上げる","volumeDown":"音量を下げる","muteUnmute":"ミュート/ミュート解除","showHideHelp":"ヘルプを表示/非表示","keyboardShortcuts":"キーボードショートカット","resetAll":"すべてリセット","resetAllConfirm":"すべてのショートカットをリセットしますか?","shortcutsReset":"ショートカットがリセットされました","editShortcut":"編集","pressAnyKey":"新しいショートカットとして設定するキーを押してください","current":"現在","cancel":"キャンセル","keyAlreadyUsed":"キー \"{key}\" は既に使用されています","shortcutUpdated":"ショートカットが更新されました","toggleCaptions":"字幕の切り替え","captionsOn":"字幕:オン","captionsOff":"字幕:オフ","captionsUnavailable":"字幕利用不可","pipTitle":"ピクチャーインピクチャー","pipDescription":"キーボードショートカット付きピクチャーインピクチャー機能を追加","pipShortcutTitle":"PiP キーボードショートカット","pipShortcutDescription":"PiP モードを切り替えるキーボードの組み合わせをカスタマイズ","none":"なし","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"{playlist} で検索...","shortTitle":"短いタイトル(1行)","emailOptional":"メールアドレス(オプション)","descriptionPlaceholder":"問題、再現手順、期待される結果と実際の結果を説明","includeDebug":"デバッグ情報を含める(バージョン、URL、設定)","openGitHub":"GitHub Issueを開く","copyReport":"レポートをコピー","prepareEmail":"メールを準備","typeBug":"バグ / エラー","typeFeature":"機能リクエスト","typeOther":"その他","titleRequired":"タイトルが必要です","titleMin":"タイトルは少なくとも5文字必要です","descRequired":"説明が必要です","descMin":"説明は少なくとも10文字必要です","invalidEmail":"無効なメール形式","fixErrorsPrefix":"以下のエラーを修正してください:\\n• ","opening":"開いています...","copying":"コピー中...","copied":"コピーしました!","openingGithubNotification":"新しいタブでGitHubを開いています","failedOpenGithub":"GitHub issueを開けませんでした","reportCopied":"レポートがクリップボードにコピーされました","copyFailed":"コピーに失敗しました — 手動でコピーしてください","thumbnailPreview":"サムネイルプレビュー","clickToOpen":"クリックして新しいタブで開く","download":"ダウンロード","thumbnailLoadFailed":"画像の読み込みに失敗しました","commentManager":"コメントマネージャー","deleteSelected":"選択を削除","selectAll":"すべて選択","clearAll":"すべてクリア","selectComment":"コメントを選択","togglePanel":"パネルを切り替え","commentManagerControls":"コメントマネージャーコントロール","commentManagement":"コメント管理","enableCommentManager":"コメントマネージャーを有効にする","bulkDeleteDescription":"コメントにチェックボックスと一括削除機能を追加","timecodes":"タイムコード","noTimecodesFound":"タイムコードが見つかりません","clickToAdd":"+ をクリックして現在の時刻を追加","reload":"タイムコードを再読み込み","add":"+ 追加","export":"エクスポート","tracking":"トラッキング中","track":"トラック","timePlaceholder":"時間(例:1:30)","labelPlaceholder":"ラベル(オプション)","enableTimecode":"タイムコードパネルを有効にする","keyboardShortcut":"キーボードショートカット","enableDescription":"クイックナビゲーション付き動画タイムコード/チャプターパネルを有効にする","shortcutDescription":"タイムコードパネルを切り替えるキーボードの組み合わせをカスタマイズ","foundTimecodes":"タイムコードが見つかりました:{count}","cannotDeleteChapter":"YouTubeチャプターは削除できません","invalidTimeFormat":"無効な時刻形式","timecodeDeleted":"タイムコードが削除されました","timecodeUpdated":"タイムコードが更新されました","timecodeAdded":"タイムコードが追加されました","noTimecodesToExport":"エクスポートするタイムコードがありません","timecodesCopied":"タイムコードがクリップボードにコピーされました","edit":"編集","delete":"削除","confirmDelete":"タイムコード \"{label}\" を削除しますか?","reloadError":"タイムコードの再読み込み中にエラーが発生しました","cannotEditChapter":"YouTubeチャプターは編集できません","save":"保存"},"tr":{"settingsTitle":"Ayarlar","basicTab":"Temel","advancedTab":"Gelişmiş","experimentalTab":"Deneysel","reportTab":"Rapor","aboutTab":"Hakkında","closeButton":"Kapat","saveChanges":"Değişiklikleri Kaydet","settingsSaved":"Ayarlar kaydedildi","speedControl":"Hız Kontrolü","speedControlDesc":"Video oynatıcıya hız kontrolü butonları ekle","screenshotButton":"Ekran Görüntüsü Butonu","screenshotButtonDesc":"Video oynatıcıya ekran görüntüsü yakalama butonu ekle","downloadButton":"İndirme Butonu","downloadButtonDesc":"Video oynatıcıya birden fazla site seçeneği ile indirme butonu ekle","customDownloader":"Özel indirici kullan","customDownload":"Kendi indiriciyi kullan","directDownload":"Doğrudan İndirme","directDownloadDesc":"Yerleşik doğrudan indirme modalını etkinleştir","directDownloadModuleNotAvailable":"Doğrudan indirme modülü mevcut değil","videoTab":"VİDEO (.mp4/.webm)","audioTab":"SES (.mp3)","subtitleTab":"ALTYAZILAR (.srt/.txt)","embedThumbnail":"Küçük resim ekle","startingDownload":"İndirme başlatılıyor...","starting":"Başlatılıyor...","completed":"Tamamlandı","downloadCompleted":"İndirme tamamlandı!","downloadFailed":"İndirme başarısız:","subtitleDownloading":"Altyazı indiriliyor...","subtitleDownloaded":"Altyazı indirildi!","subtitleDownloadFailed":"Altyazı indirme başarısız:","loading":"Yükleniyor...","noSubtitles":"Altyazı mevcut değil","subtitleLoadError":"Altyazı yükleme hatası","autoTranslateSuffix":" (Otomatik çeviri)","vp9Label":"VP9 (Daha Yüksek Kalite)","noSubtitleSelected":"Altyazı seçilmedi","zeroBytesError":"İndirilen dosya 0 bayt","unknownSize":"—","alwaysEnabled":"Her zaman etkin - GitHub deposu","siteName":"Site adı","urlTemplate":"URL şablonu ({videoId} veya {videoUrl} kullanın)","saveButton":"Kaydet","resetButton":"Sıfırla","y2mateSettingsSaved":"Y2Mate ayarları kaydedildi","y2mateReset":"Y2Mate varsayılana sıfırlandı","youtubeSettings":"YouTube + Ayarları","takeScreenshot":"Ekran görüntüsü al","downloadOptions":"İndirme seçenekleri","byYTDL":"by YTDL","copiedToClipboard":"Video URL'si panoya kopyalandı!","endscreenHideLabel":"Bitiş Ekranları ve Kartları/Logoyu Gizle","endscreenHideDesc":"Bitiş ekranı önerilerini ve bilgi kartlarını/kanal logosunu kaldır","removedSuffix":" ({n} kaldırıldı)","scrollToTop":"Başa kaydır","resumePlayback":"Oynatmaya devam edilsin mi?","resume":"Devam et","startOver":"Baştan başla","stats":"İstatistikler","channelStats":"Kanal İstatistikleri","videoStats":"Video İstatistikleri","liveStats":"Canlı İstatistikleri","shortsStats":"Shorts İstatistikleri","close":"Kapat","channel":"Kanal","live":"Canlı","shorts":"Shorts","statisticsButton":"İstatistik Butonu","displayOptions":"Görüntüleme Seçenekleri","channelStatsTitle":"Kanal İstatistikleri","channelStatsDescription":"Kanal banner'ında canlı abone/görüntülenme/video sayısı kaplamasını göster","subscribers":"Aboneler","views":"Görüntülenmeler","videos":"Videolar","fontFamily":"Yazı Tipi","fontSize":"Yazı Boyutu","updateInterval":"Güncelleme Aralığı","backgroundOpacity":"Arka Plan Opaklığı","overlayAriaLabel":"YouTube Kanal İstatistikleri Kaplaması","videoStatistics":"Video İstatistikleri","channelStatistics":"Kanal İstatistikleri","loadingStats":"İstatistikler yükleniyor...","failedToLoadStats":"İstatistikler yüklenemedi","likes":"Beğeniler","dislikes":"Beğenmemeler","comments":"Yorumlar","liveViewers":"Canlı İzleyiciler","totalViews":"Toplam Görüntülenmeler","totalVideos":"Toplam Videolar","settingsAriaLabel":"Ayarlar menüsünü aç","settingsMenuAriaLabel":"İstatistik görüntüleme ayarları","unknown":"Bilinmiyor","monetization":"Para Kazanma","country":"Ülke","duration":"Süre","yes":"Evet","no":"Hayır","paidPromotion":"Ücretli Promosyon","updateAvailableTitle":"YouTube + Güncellemesi Mevcut","version":"Sürüm","newFeatures":"Yeni özellikler ve iyileştirmeler","installUpdate":"Güncellemeyi Yükle","later":"Daha sonra","enhancedExperience":"Güçlü özelliklerle geliştirilmiş YouTube deneyimi","currentVersion":"Mevcut Sürüm","lastChecked":"Son kontrol","latestAvailable":"En son mevcut","updateAvailable":"Güncelleme Mevcut","installing":"Yükleniyor...","checkingForUpdates":"Güncellemeler kontrol ediliyor...","upToDate":"Güncel","checkForUpdates":"Güncellemeleri Kontrol Et","autoCheckUpdates":"Güncellemeleri otomatik kontrol et","updatePageFallback":"Güncelleme sayfası açılıyor...","never":"Asla","justNow":"Az önce","daysAgo":"{n}g önce","hoursAgo":"{n}s önce","minutesAgo":"{n}d önce","updateAvailableMsg":"Güncelleme {version} mevcut!","upToDateMsg":"En son sürümü ({version}) kullanıyorsunuz","updateCheckFailed":"Güncelleme kontrolü başarısız: {msg}","noUpdateInfo":"Güncelleme bilgisi yok","dismiss":"Kapat","adBlocker":"Reklam Engelleyici","adBlockerDescription":"Reklamları otomatik olarak atla ve reklam öğelerini kaldır","statisticsButtonDescription":"Video oynatıcıda istatistik butonunu göster","privacy":"Gizlilik","seekBackward":"Geri sar","seekForward":"İleri sar","volumeUp":"Ses aç","volumeDown":"Ses kıs","muteUnmute":"Sessiz/Sesli","showHideHelp":"Yardımı göster/gizle","keyboardShortcuts":"Klavye Kısayolları","resetAll":"Tümünü Sıfırla","resetAllConfirm":"Tüm kısayolları sıfırla?","shortcutsReset":"Kısayollar sıfırlandı","editShortcut":"Düzenle","pressAnyKey":"Yeni kısayol olarak ayarlamak için herhangi bir tuşa basın","current":"Mevcut","cancel":"İptal","keyAlreadyUsed":"\"{key}\" tuşu zaten kullanılıyor","shortcutUpdated":"Kısayol güncellendi","toggleCaptions":"Altyazıları aç/kapat","captionsOn":"Altyazılar: Açık","captionsOff":"Altyazılar: Kapalı","captionsUnavailable":"Altyazılar mevcut değil","pipTitle":"Resim İçinde Resim","pipDescription":"Klavye kısayolu ile Resim İçinde Resim işlevselliği ekle","pipShortcutTitle":"PiP Klavye Kısayolu","pipShortcutDescription":"PiP modunu değiştirmek için klavye kombinasyonunu özelleştir","none":"Yok","ctrl":"Ctrl","alt":"Alt","shift":"Shift","searchPlaceholder":"{playlist} içinde ara...","shortTitle":"Kısa başlık (bir satır)","emailOptional":"E-postanız (isteğe bağlı)","descriptionPlaceholder":"Sorunu, yeniden üretme adımlarını, beklenen ve gerçek durumu açıklayın","includeDebug":"Hata ayıklama bilgilerini dahil et (sürüm, URL, ayarlar)","openGitHub":"GitHub Issue Aç","copyReport":"Raporu Kopyala","prepareEmail":"E-posta Hazırla","typeBug":"Hata / Bug","typeFeature":"Özellik İsteği","typeOther":"Diğer","titleRequired":"Başlık gerekli","titleMin":"Başlık en az 5 karakter olmalı","descRequired":"Açıklama gerekli","descMin":"Açıklama en az 10 karakter olmalı","invalidEmail":"Geçersiz e-posta formatı","fixErrorsPrefix":"Lütfen aşağıdaki hataları düzeltin:\\n• ","opening":"Açılıyor...","copying":"Kopyalanıyor...","copied":"Kopyalandı!","openingGithubNotification":"GitHub yeni sekmede açılıyor","failedOpenGithub":"GitHub issue açılamadı","reportCopied":"Rapor panoya kopyalandı","copyFailed":"Kopyalama başarısız — lütfen manuel olarak kopyalayın","thumbnailPreview":"Küçük Resim Önizlemesi","clickToOpen":"Yeni sekmede açmak için tıklayın","download":"İndir","thumbnailLoadFailed":"Resim yüklenemedi","commentManager":"Yorum Yöneticisi","deleteSelected":"Seçilenleri Sil","selectAll":"Tümünü Seç","clearAll":"Tümünü Temizle","selectComment":"Yorumu seç","togglePanel":"Paneli değiştir","commentManagerControls":"Yorum yöneticisi kontrolleri","commentManagement":"Yorum Yönetimi","enableCommentManager":"Yorum yöneticisini etkinleştir","bulkDeleteDescription":"Yorumlarınıza onay kutuları ve toplu silme işlevi ekle","timecodes":"Zaman Kodları","noTimecodesFound":"Zaman kodu bulunamadı","clickToAdd":"Geçerli zamanı eklemek için + tıklayın","reload":"Zaman kodlarını yenile","add":"+ Ekle","export":"Dışa Aktar","tracking":"İzleniyor","track":"İzle","timePlaceholder":"Zaman (örn: 1:30)","labelPlaceholder":"Etiket (isteğe bağlı)","enableTimecode":"Zaman Kodu Panelini Etkinleştir","keyboardShortcut":"Klavye Kısayolu","enableDescription":"Hızlı gezinme ile video zaman kodu/bölüm panelini etkinleştir","shortcutDescription":"Zaman Kodu Panelini değiştirmek için klavye kombinasyonunu özelleştir","foundTimecodes":"Bulunan zaman kodları: {count}","cannotDeleteChapter":"YouTube bölümleri silinemez","invalidTimeFormat":"Geçersiz zaman formatı","timecodeDeleted":"Zaman kodu silindi","timecodeUpdated":"Zaman kodu güncellendi","timecodeAdded":"Zaman kodu eklendi","noTimecodesToExport":"Dışa aktarılacak zaman kodu yok","timecodesCopied":"Zaman kodları panoya kopyalandı","edit":"Düzenle","delete":"Sil","confirmDelete":"\"{label}\" zaman kodunu sil?","reloadError":"Zaman kodları yenilenirken hata","cannotEditChapter":"YouTube bölümleri düzenlenemez","save":"Kaydet"}}; (function () { 'use strict'; const GITHUB_CONFIG = { owner: 'diorhc', repo: 'YTP', branch: 'main', basePath: 'locales', }; const CDN_URLS = { github: `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.basePath}`, jsdelivr: `https://cdn.jsdelivr.net/gh/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}@${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.basePath}`, }; const AVAILABLE_LANGUAGES = ['en', 'ru', 'kr', 'fr', 'du', 'cn', 'tw', 'jp', 'tr']; const LANGUAGE_NAMES = { en: 'English', ru: 'Русский', kr: '한국어', fr: 'Français', du: 'Nederlands', cn: '简体中文', tw: '繁體中文', jp: '日本語', tr: 'Türkçe', }; const translationsCache = new Map(); const loadingPromises = new Map(); async function fetchTranslation(lang) { try { if (typeof window !== 'undefined' && window.YouTubePlusEmbeddedTranslations) { const embedded = window.YouTubePlusEmbeddedTranslations[lang]; if (embedded) { console.log('[YouTube+][i18n-loader]', `Using embedded translations for ${lang}`); return embedded; } } } catch (e) { console.warn('[YouTube+][i18n-loader]', 'Error reading embedded translations', e); } try { const url = `${CDN_URLS.jsdelivr}/${lang}.json`; const response = await fetch(url, { cache: 'default', headers: { Accept: 'application/json' }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch { try { const url = `${CDN_URLS.github}/${lang}.json`; console.warn('[YouTube+][i18n-loader]', `Primary CDN failed, trying GitHub raw: ${url}`); const response = await fetch(url, { cache: 'default', headers: { Accept: 'application/json' }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (err) { console.error('[YouTube+][i18n-loader]', `Failed to fetch translations for ${lang}:`, err); throw err; } } } async function loadTranslations(lang) { const languageCode = AVAILABLE_LANGUAGES.includes(lang) ? lang : 'en'; if (translationsCache.has(languageCode)) return translationsCache.get(languageCode); if (loadingPromises.has(languageCode)) return loadingPromises.get(languageCode); const loadPromise = (async () => { try { const translations = await fetchTranslation(languageCode); translationsCache.set(languageCode, translations); loadingPromises.delete(languageCode); return translations; } catch (error) { loadingPromises.delete(languageCode); if (languageCode !== 'en') return loadTranslations('en'); throw error; } })(); loadingPromises.set(languageCode, loadPromise); return loadPromise; } const i18nLoaderAPI = { loadTranslations, AVAILABLE_LANGUAGES, LANGUAGE_NAMES, CDN_URLS }; if (typeof window !== 'undefined') window.YouTubePlusI18nLoader = i18nLoaderAPI; console.log('[YouTube+][i18n-loader] initialized'); })(); (function () { 'use strict'; let currentLanguage = 'en'; let translations = {}; const translationCache = new Map(); const languageChangeListeners = new Set(); let loadingPromise = null; const languageMap = { ko: 'kr', 'ko-kr': 'kr', fr: 'fr', 'fr-fr': 'fr', nl: 'du', 'nl-nl': 'du', 'nl-be': 'du', zh: 'cn', 'zh-cn': 'cn', 'zh-hans': 'cn', 'zh-tw': 'tw', 'zh-hk': 'tw', 'zh-hant': 'tw', ja: 'jp', 'ja-jp': 'jp', tr: 'tr', 'tr-tr': 'tr', }; function detectLanguage() { try { const ytLang = document.documentElement.lang || document.querySelector('html')?.getAttribute('lang'); if (ytLang) { const mapped = languageMap[ytLang.toLowerCase()] || ytLang.toLowerCase().substr(0, 2); if (window.YouTubePlusI18nLoader?.AVAILABLE_LANGUAGES.includes(mapped)) { return mapped; } } const browserLang = navigator.language || navigator.userLanguage || 'en'; const mapped = languageMap[browserLang.toLowerCase()] || browserLang.split('-')[0]; if (window.YouTubePlusI18nLoader?.AVAILABLE_LANGUAGES.includes(mapped)) { return mapped; } return 'en'; } catch (error) { console.error('[YouTube+][i18n]', 'Error detecting language:', error); return 'en'; } } async function loadTranslations() { if (!window.YouTubePlusI18nLoader) { console.error('[YouTube+][i18n]', 'i18n-loader not available'); return false; } if (loadingPromise) { await loadingPromise; return true; } loadingPromise = (async () => { try { console.log('[YouTube+][i18n]', `Loading translations for ${currentLanguage}...`); translations = await window.YouTubePlusI18nLoader.loadTranslations(currentLanguage); translationCache.clear(); console.log( '[YouTube+][i18n]', `✓ Loaded ${Object.keys(translations).length} translations for ${currentLanguage}` ); return true; } catch (error) { console.error('[YouTube+][i18n]', 'Failed to load translations:', error); if (currentLanguage !== 'en') { currentLanguage = 'en'; return loadTranslations(); } return false; } finally { loadingPromise = null; } })(); return loadingPromise; } function translate(key, params = {}) { const cacheKey = `${key}:${JSON.stringify(params)}`; if (translationCache.has(cacheKey)) { return translationCache.get(cacheKey); } let text = translations[key]; if (!text) { if (Object.keys(translations).length > 0) { console.warn('[YouTube+][i18n]', `Missing translation for key: ${key}`); } text = key; } if (Object.keys(params).length > 0) { Object.keys(params).forEach(param => { text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]); }); } translationCache.set(cacheKey, text); return text; } function getLanguage() { return currentLanguage; } async function setLanguage(lang) { if (lang === currentLanguage) { return true; } const oldLang = currentLanguage; currentLanguage = lang; try { const success = await loadTranslations(); if (success) { languageChangeListeners.forEach(listener => { try { listener(currentLanguage, oldLang); } catch (error) { console.error('[YouTube+][i18n]', 'Error in language change listener:', error); } }); } return success; } catch (error) { console.error('[YouTube+][i18n]', 'Failed to change language:', error); currentLanguage = oldLang; return false; } } function getAllTranslations() { return { ...translations }; } function getAvailableLanguages() { return window.YouTubePlusI18nLoader?.AVAILABLE_LANGUAGES || ['en']; } function hasTranslation(key) { return translations[key] !== undefined; } function addTranslation(key, value) { translations[key] = value; translationCache.clear(); } function addTranslations(newTranslations) { Object.assign(translations, newTranslations); translationCache.clear(); } function onLanguageChange(callback) { languageChangeListeners.add(callback); return () => languageChangeListeners.delete(callback); } function formatNumber(num, options = {}) { try { const lang = getLanguage(); const localeMap = { ru: 'ru-RU', kr: 'ko-KR', fr: 'fr-FR', du: 'nl-NL', cn: 'zh-CN', tw: 'zh-TW', jp: 'ja-JP', tr: 'tr-TR', }; const locale = localeMap[lang] || 'en-US'; return new Intl.NumberFormat(locale, options).format(num); } catch (error) { console.error('[YouTube+][i18n]', 'Error formatting number:', error); return String(num); } } function formatDate(date, options = {}) { try { const lang = getLanguage(); const localeMap = { ru: 'ru-RU', kr: 'ko-KR', fr: 'fr-FR', du: 'nl-NL', cn: 'zh-CN', tw: 'zh-TW', jp: 'ja-JP', tr: 'tr-TR', }; const locale = localeMap[lang] || 'en-US'; const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(locale, options).format(dateObj); } catch (error) { console.error('[YouTube+][i18n]', 'Error formatting date:', error); return String(date); } } function pluralize(count, singular, plural, few = null) { const lang = getLanguage(); if (lang === 'ru' && few) { const mod10 = count % 10; const mod100 = count % 100; if (mod10 === 1 && mod100 !== 11) { return singular; } if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) { return few; } return plural; } return count === 1 ? singular : plural; } function clearCache() { translationCache.clear(); } function getCacheStats() { return { size: translationCache.size, currentLanguage, availableLanguages: getAvailableLanguages(), translationsLoaded: Object.keys(translations).length, }; } async function initialize() { try { currentLanguage = detectLanguage(); let attempts = 0; while (!window.YouTubePlusI18nLoader && attempts < 50) { await new Promise(resolve => setTimeout(resolve, 100)); attempts++; } if (!window.YouTubePlusI18nLoader) { console.error('[YouTube+][i18n]', 'i18n-loader not available after waiting'); return; } const languageNames = window.YouTubePlusI18nLoader.LANGUAGE_NAMES; console.log( '[YouTube+][i18n]', `Detected language: ${currentLanguage} (${languageNames[currentLanguage] || currentLanguage})` ); await loadTranslations(); } catch (error) { console.error('[YouTube+][i18n]', 'Initialization error:', error); currentLanguage = 'en'; } } const i18nAPI = { t: translate, translate, getLanguage, setLanguage, detectLanguage, getAllTranslations, getAvailableLanguages, hasTranslation, addTranslation, addTranslations, onLanguageChange, formatNumber, formatDate, pluralize, clearCache, getCacheStats, loadTranslations, initialize, }; if (typeof window !== 'undefined') { window.YouTubePlusI18n = i18nAPI; if (window.YouTubeUtils) { window.YouTubeUtils.i18n = i18nAPI; window.YouTubeUtils.t = translate; window.YouTubeUtils.getLanguage = getLanguage; } } if (typeof module !== 'undefined' && module.exports) { module.exports = i18nAPI; } initialize().then(() => { console.log('[YouTube+][i18n]', 'i18n system initialized successfully'); }); })(); function createSettingsSidebar(t) { return `

${t('settingsTitle')}

${createNavItem('basic', t('basicTab'), createBasicIcon(), true)} ${createNavItem('advanced', t('advancedTab'), createAdvancedIcon())} ${createNavItem('experimental', t('experimentalTab'), createExperimentalIcon())} ${createNavItem('report', t('reportTab'), createReportIcon())} ${createNavItem('about', t('aboutTab'), createAboutIcon())}
`; } function createNavItem(section, label, icon, active = false) { const activeClass = active ? ' active' : ''; return `
${icon} ${label}
`; } function createBasicIcon() { return ` `; } function createAdvancedIcon() { return ` `; } function createExperimentalIcon() { return ` `; } function createReportIcon() { return ` `; } function createAboutIcon() { return ` `; } function createSettingsItem(label, description, setting, checked) { return `
${description}
`; } function createDownloadSiteOption(site, _t) { const { key, name, description, checked, hasControls, controls } = site; return `
${name}
${description}
${hasControls ? `
${controls}
` : ''}
`; } function createY2MateControls(customization, t) { const name = customization?.name || 'Y2Mate'; const url = customization?.url || 'https://www.y2mate.com/youtube/{videoId}'; return `
`; } function createYTDLControls() { return `
`; } function createDownloadSubmenu(settings, t) { const display = settings.enableDownload ? 'block' : 'none'; const sites = [ { key: 'y2mate', name: settings.downloadSiteCustomization?.y2mate?.name || 'Y2Mate', description: t('customDownloader'), checked: settings.downloadSites?.y2mate, hasControls: true, controls: createY2MateControls(settings.downloadSiteCustomization?.y2mate, t), }, { key: 'ytdl', name: t('byYTDL'), description: t('customDownload'), checked: settings.downloadSites?.ytdl, hasControls: true, controls: createYTDLControls(), }, { key: 'direct', name: t('directDownload'), description: t('directDownloadDesc'), checked: settings.downloadSites?.direct, hasControls: false, }, ]; return `
${sites.map(site => createDownloadSiteOption(site, t)).join('')}
`; } function createBasicSettingsSection(settings, t) { return `
${createSettingsItem(t('speedControl'), t('speedControlDesc'), 'enableSpeedControl', settings.enableSpeedControl)} ${createSettingsItem(t('screenshotButton'), t('screenshotButtonDesc'), 'enableScreenshot', settings.enableScreenshot)} ${createSettingsItem(t('downloadButton'), t('downloadButtonDesc'), 'enableDownload', settings.enableDownload)} ${createDownloadSubmenu(settings, t)}
`; } function createAboutSection() { return ` `; } function createMainContent(settings, t) { return `
${createBasicSettingsSection(settings, t)} ${createAboutSection()}
`; } if (typeof window !== 'undefined') { window.YouTubePlusSettingsHelpers = { createSettingsSidebar, createMainContent, createSettingsItem, createDownloadSiteOption, createBasicSettingsSection, }; } window.YouTubePlusModalUtils = (() => { 'use strict'; function createBackdrop() { const DOMUtils = window.YouTubePlusDOMUtils; const backdrop = DOMUtils && DOMUtils.createElement ? DOMUtils.createElement('div', { className: 'ytp-modal-backdrop' }) : document.createElement('div'); if (!DOMUtils) { backdrop.className = 'ytp-modal-backdrop'; } backdrop.addEventListener('click', e => { if (e.target === backdrop) { closeModal(backdrop.nextElementSibling); } }); return backdrop; } function createModalHeader(title, closeable = true) { const DOMUtils = window.YouTubePlusDOMUtils; const header = DOMUtils && DOMUtils.createElement ? DOMUtils.createElement('div', { className: 'ytp-modal-header' }) : document.createElement('div'); if (!DOMUtils) { header.className = 'ytp-modal-header'; } const titleEl = document.createElement('h3'); titleEl.textContent = title; header.appendChild(titleEl); if (closeable) { const closeBtn = DOMUtils && DOMUtils.createButton ? DOMUtils.createButton({ text: '×', className: 'ytp-modal-close', ariaLabel: 'Close', }) : document.createElement('button'); if (!DOMUtils) { closeBtn.className = 'ytp-modal-close'; closeBtn.textContent = '×'; closeBtn.setAttribute('aria-label', 'Close'); } closeBtn.addEventListener('click', () => { const modal = header.closest('.ytp-modal'); if (modal) closeModal(modal); }); header.appendChild(closeBtn); } return header; } function createModalBody(content) { const DOMUtils = window.YouTubePlusDOMUtils; const body = DOMUtils && DOMUtils.createElement ? DOMUtils.createElement('div', { className: 'ytp-modal-body' }) : document.createElement('div'); if (!DOMUtils) { body.className = 'ytp-modal-body'; } if (typeof content === 'string') { body.textContent = content; } else if (content instanceof HTMLElement) { body.appendChild(content); } return body; } function createModalFooter(buttons = []) { const DOMUtils = window.YouTubePlusDOMUtils; const footer = DOMUtils && DOMUtils.createElement ? DOMUtils.createElement('div', { className: 'ytp-modal-footer' }) : document.createElement('div'); if (!DOMUtils) { footer.className = 'ytp-modal-footer'; } buttons.forEach(btnConfig => { const btn = DOMUtils && DOMUtils.createButton ? DOMUtils.createButton({ text: btnConfig.text, className: btnConfig.className || 'ytp-btn', onClick: btnConfig.onClick, }) : document.createElement('button'); if (!DOMUtils) { btn.className = btnConfig.className || 'ytp-btn'; btn.textContent = btnConfig.text; if (btnConfig.onClick) { btn.addEventListener('click', btnConfig.onClick); } } footer.appendChild(btn); }); return footer; } function createModal({ title = '', content = '', buttons = [], className = '', closeable = true, } = {}) { const DOMUtils = window.YouTubePlusDOMUtils; const modal = DOMUtils && DOMUtils.createElement ? DOMUtils.createElement('div', { className: `ytp-modal ${className}` }) : document.createElement('div'); if (!DOMUtils) { modal.className = `ytp-modal ${className}`; } if (title) { modal.appendChild(createModalHeader(title, closeable)); } if (content) { modal.appendChild(createModalBody(content)); } if (buttons.length > 0) { modal.appendChild(createModalFooter(buttons)); } return modal; } function showModal(modal, withBackdrop = true) { if (!modal) return; if (withBackdrop) { const backdrop = createBackdrop(); document.body.appendChild(backdrop); } document.body.appendChild(modal); modal.classList.add('ytp-modal-visible'); const focusable = modal.querySelector('button, input, textarea, select'); if (focusable) { focusable.focus(); } const handleEscape = e => { if (e.key === 'Escape') { closeModal(modal); document.removeEventListener('keydown', handleEscape); } }; document.addEventListener('keydown', handleEscape); } function closeModal(modal) { if (!modal) return; modal.classList.remove('ytp-modal-visible'); setTimeout(() => { modal.remove(); const backdrop = document.querySelector('.ytp-modal-backdrop'); if (backdrop) backdrop.remove(); }, 300); } function isEmpty(value) { return !value || value === ''; } function validateByType(value, type) { const ValidationUtils = window.YouTubePlusValidationUtils; if (!ValidationUtils) return true; if (type === 'email') return ValidationUtils.isValidEmail(value); if (type === 'url') return ValidationUtils.isValidURL(value); return true; } function checkRequired(value, rule, field) { if (rule.required && isEmpty(value)) { return rule.message || `${field} is required`; } return null; } function checkType(value, rule) { if (rule.type && !validateByType(value, rule.type)) { return rule.message || `Invalid ${rule.type}`; } return null; } function checkLength(value, rule) { if (rule.min && value.length < rule.min) { return rule.message || `Minimum length is ${rule.min}`; } if (rule.max && value.length > rule.max) { return rule.message || `Maximum length is ${rule.max}`; } return null; } function checkPattern(value, rule) { if (rule.pattern && !rule.pattern.test(value)) { return rule.message || 'Invalid format'; } return null; } function validateField(field, value, rule) { return ( checkRequired(value, rule, field) || checkType(value, rule) || checkLength(value, rule) || checkPattern(value, rule) ); } function validateForm(data, rules) { const errors = {}; Object.keys(rules).forEach(field => { const rule = rules[field]; const value = data[field]; const error = validateField(field, value, rule); if (error) { errors[field] = error; } }); return { valid: Object.keys(errors).length === 0, errors, }; } return { createBackdrop, createModalHeader, createModalBody, createModalFooter, createModal, showModal, closeModal, isEmpty, validateByType, validateField, validateForm, }; })(); const initializeDownloadSites = settings => { if (!settings.downloadSites) { settings.downloadSites = { y2mate: true, ytdl: true, direct: true }; } }; const toggleDownloadSiteControls = checkbox => { try { const container = checkbox.closest('.download-site-option'); if (container) { const controls = container.querySelector('.download-site-controls'); if (controls) { controls.style.display = checkbox.checked ? 'block' : 'none'; } } } catch (err) { console.warn('[YouTube+] toggle download-site-controls failed:', err); } }; const safelySaveSettings = saveSettings => { try { saveSettings(); } catch (err) { console.warn('[YouTube+] autosave downloadSite toggle failed:', err); } }; const handleDownloadSiteToggle = (target, key, settings, markDirty, saveSettings) => { initializeDownloadSites(settings); const checkbox = (target); settings.downloadSites[key] = checkbox.checked; try { markDirty(); } catch {} toggleDownloadSiteControls(checkbox); rebuildDownloadDropdown(settings); safelySaveSettings(saveSettings); }; const handleDownloadButtonToggle = context => { const { settings, getElement, addDownloadButton } = context; const controls = getElement('.ytp-right-controls'); const existing = getElement('.ytp-download-button', false); if (settings.enableDownload) { if (controls && !existing) addDownloadButton(controls); } else { if (existing) existing.remove(); const dropdown = document.querySelector('.download-options'); if (dropdown) dropdown.remove(); } }; const handleSpeedControlToggle = context => { const { settings, getElement, addSpeedControlButton } = context; const controls = getElement('.ytp-right-controls'); const existing = getElement('.speed-control-btn', false); if (settings.enableSpeedControl) { if (controls && !existing) addSpeedControlButton(controls); } else { if (existing) existing.remove(); const speedOptions = document.querySelector('.speed-options'); if (speedOptions) speedOptions.remove(); } }; const updateGlobalSettings = settings => { if (typeof window !== 'undefined' && window.youtubePlus) { window.youtubePlus.settings = window.youtubePlus.settings || settings; } }; const applySettingLive = (setting, context) => { const { settings, refreshDownloadButton } = context; try { if (context.updatePageBasedOnSettings) { context.updatePageBasedOnSettings(); } if (setting === 'enableDownload') { handleDownloadButtonToggle(context); } else if (setting === 'enableSpeedControl') { handleSpeedControlToggle(context); } if (refreshDownloadButton) { refreshDownloadButton(); } } catch (innerErr) { console.warn('[YouTube+] live apply specific toggle failed:', innerErr); } updateGlobalSettings(settings); }; const handleSimpleSettingToggle = ( target, setting, settings, context, markDirty, saveSettings, modal ) => { settings[setting] = (target).checked; try { markDirty(); } catch {} try { applySettingLive(setting, context); } catch (err) { console.warn('[YouTube+] apply settings live failed:', err); } try { saveSettings(); } catch (err) { console.warn('[YouTube+] autosave simple setting failed:', err); } if (setting === 'enableDownload') { const submenu = modal.querySelector('.download-submenu'); if (submenu) { submenu.style.display = (target).checked ? 'block' : 'none'; } } }; const initializeDownloadCustomization = settings => { if (!settings.downloadSiteCustomization) { settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, }; } }; const initializeDownloadSite = (settings, site) => { if (!settings.downloadSiteCustomization[site]) { settings.downloadSiteCustomization[site] = { name: '', url: '' }; } }; const getDownloadSiteFallbackName = (site, t) => { if (site === 'y2mate') return 'Y2Mate'; if (site === 'ytdl') return t('byYTDL'); return t('directDownload'); }; const updateDownloadSiteName = (target, site, t) => { const nameDisplay = target.closest('.download-site-option')?.querySelector('.download-site-name'); if (nameDisplay) { const inputValue = (target).value; const fallbackName = getDownloadSiteFallbackName(site, t); nameDisplay.textContent = inputValue || fallbackName; } }; const rebuildDownloadDropdown = settings => { try { if ( typeof window !== 'undefined' && window.youtubePlus && typeof window.youtubePlus.rebuildDownloadDropdown === 'function' ) { window.youtubePlus.settings = window.youtubePlus.settings || settings; window.youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } }; const handleDownloadSiteInput = (target, site, field, settings, markDirty, t) => { initializeDownloadCustomization(settings); initializeDownloadSite(settings, site); settings.downloadSiteCustomization[site][field] = (target).value; try { markDirty(); } catch {} if (field === 'name') { updateDownloadSiteName(target, site, t); } rebuildDownloadDropdown(settings); }; const ensureY2MateStructure = settings => { if (!settings.downloadSiteCustomization) { settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, }; } if (!settings.downloadSiteCustomization.y2mate) { settings.downloadSiteCustomization.y2mate = { name: '', url: '' }; } }; const readY2MateInputs = (container, settings) => { const nameInput = container.querySelector( 'input.download-site-input[data-site="y2mate"][data-field="name"]' ); const urlInput = container.querySelector( 'input.download-site-input[data-site="y2mate"][data-field="url"]' ); if (nameInput) settings.downloadSiteCustomization.y2mate.name = nameInput.value; if (urlInput) settings.downloadSiteCustomization.y2mate.url = urlInput.value; }; const triggerRebuildDropdown = () => { try { if ( typeof window !== 'undefined' && window.youtubePlus && typeof window.youtubePlus.rebuildDownloadDropdown === 'function' ) { window.youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } }; const handleY2MateSave = (target, settings, saveSettings, showNotification, t) => { ensureY2MateStructure(settings); const container = target.closest('.download-site-option'); if (container) { readY2MateInputs(container, settings); } saveSettings(); if (window.youtubePlus) { window.youtubePlus.settings = window.youtubePlus.settings || settings; } triggerRebuildDropdown(); showNotification(t('y2mateSettingsSaved')); }; const resetY2MateToDefaults = settings => { ensureY2MateStructure(settings); settings.downloadSiteCustomization.y2mate = { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}', }; }; const updateY2MateModalInputs = (container, settings) => { const nameInput = container.querySelector( 'input.download-site-input[data-site="y2mate"][data-field="name"]' ); const urlInput = container.querySelector( 'input.download-site-input[data-site="y2mate"][data-field="url"]' ); const nameDisplay = container.querySelector('.download-site-name'); const y2mateSettings = settings.downloadSiteCustomization.y2mate; if (nameInput) nameInput.value = y2mateSettings.name; if (urlInput) urlInput.value = y2mateSettings.url; if (nameDisplay) nameDisplay.textContent = y2mateSettings.name; }; const handleY2MateReset = (modal, settings, saveSettings, showNotification, t) => { resetY2MateToDefaults(settings); const container = modal.querySelector('.download-site-option'); if (container) { updateY2MateModalInputs(container, settings); } saveSettings(); if (window.youtubePlus) { window.youtubePlus.settings = window.youtubePlus.settings || settings; } triggerRebuildDropdown(); showNotification(t('y2mateReset')); }; const handleSidebarNavigation = (navItem, modal) => { const { dataset } = navItem; const { section } = dataset; modal .querySelectorAll('.ytp-plus-settings-nav-item') .forEach(item => item.classList.remove('active')); modal.querySelectorAll('.ytp-plus-settings-section').forEach(s => s.classList.add('hidden')); navItem.classList.add('active'); const targetSection = modal.querySelector( `.ytp-plus-settings-section[data-section="${section}"]` ); if (targetSection) targetSection.classList.remove('hidden'); }; if (typeof window !== 'undefined') { window.YouTubePlusModalHandlers = { handleDownloadSiteToggle, handleSimpleSettingToggle, handleDownloadSiteInput, handleY2MateSave, handleY2MateReset, handleSidebarNavigation, applySettingLive, }; } const waitForDownloadAPI = timeout => new Promise(resolve => { const interval = 200; let waited = 0; if (typeof window.YouTubePlusDownload !== 'undefined') { return resolve(window.YouTubePlusDownload); } const id = setInterval(() => { waited += interval; if (typeof window.YouTubePlusDownload !== 'undefined') { clearInterval(id); return resolve(window.YouTubePlusDownload); } if (waited >= timeout) { clearInterval(id); return resolve(undefined); } }, interval); }); const fallbackCopyToClipboard = (text, t, NotificationManager) => { const input = document.createElement('input'); input.value = text; document.body.appendChild(input); input.select(); document.execCommand('copy'); document.body.removeChild(input); NotificationManager.show(t('copiedToClipboard'), { duration: 2000, type: 'success', }); }; const buildUrl = (template, videoId, videoUrl) => (template || '') .replace('{videoId}', videoId || '') .replace('{videoUrl}', encodeURIComponent(videoUrl || '')); const createButtonElement = t => { const button = document.createElement('div'); button.className = 'ytp-button ytp-download-button'; button.setAttribute('title', t('downloadOptions')); button.setAttribute('tabindex', '0'); button.setAttribute('role', 'button'); button.setAttribute('aria-haspopup', 'true'); button.setAttribute('aria-expanded', 'false'); button.innerHTML = ` `; return button; }; const positionDropdown = (button, dropdown) => { const rect = button.getBoundingClientRect(); const left = Math.max(8, rect.left + rect.width / 2 - 75); const bottom = Math.max(8, window.innerHeight - rect.top + 12); dropdown.style.left = `${left}px`; dropdown.style.bottom = `${bottom}px`; }; const createDownloadActions = (t, YouTubeUtils) => { const handleDirectDownload = async () => { const api = await waitForDownloadAPI(2000); if (!api) { console.error('[YouTube+] Direct download module not loaded'); YouTubeUtils.NotificationManager.show(t('directDownloadModuleNotAvailable'), { duration: 3000, type: 'error', }); return; } try { if (typeof api.openModal === 'function') { api.openModal(); return; } if (typeof api.downloadVideo === 'function') { await api.downloadVideo({ format: 'video', quality: '1080' }); return; } } catch (err) { console.error('[YouTube+] Direct download invocation failed:', err); } YouTubeUtils.NotificationManager.show(t('directDownloadModuleNotAvailable'), { duration: 3000, type: 'error', }); }; const handleYTDLDownload = url => { const videoId = new URLSearchParams(location.search).get('v'); const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href; navigator.clipboard .writeText(videoUrl) .then(() => { YouTubeUtils.NotificationManager.show(t('copiedToClipboard'), { duration: 2000, type: 'success', }); }) .catch(() => { fallbackCopyToClipboard(videoUrl, t, YouTubeUtils.NotificationManager); }); window.open(url, '_blank'); }; const openDownloadSite = (url, isYTDL, isDirect, dropdown, button) => { dropdown.classList.remove('visible'); button.setAttribute('aria-expanded', 'false'); if (isDirect) { handleDirectDownload(); return; } if (isYTDL) { handleYTDLDownload(url); return; } window.open(url, '_blank'); }; return { handleDirectDownload, handleYTDLDownload, openDownloadSite }; }; const createDownloadSitesBuilder = t => { return (customization, enabledSites, videoId, videoUrl) => { const baseSites = [ { key: 'y2mate', name: customization?.y2mate?.name || 'Y2Mate', url: buildUrl( customization?.y2mate?.url || `https://www.y2mate.com/youtube/{videoId}`, videoId, videoUrl ), isYTDL: false, isDirect: false, }, { key: 'ytdl', name: 'by YTDL', url: `http://localhost:5005`, isYTDL: true, isDirect: false, }, { key: 'direct', name: t('directDownload'), url: '#', isYTDL: false, isDirect: true, }, ]; const downloadSites = baseSites.filter(s => enabledSites[s.key] !== false); return { baseSites, downloadSites }; }; }; const createDropdownOptions = (downloadSites, button, openDownloadSite) => { const options = document.createElement('div'); options.className = 'download-options'; options.setAttribute('role', 'menu'); const list = document.createElement('div'); list.className = 'download-options-list'; downloadSites.forEach(site => { const opt = document.createElement('div'); opt.className = 'download-option-item'; opt.textContent = site.name; opt.setAttribute('role', 'menuitem'); opt.setAttribute('tabindex', '0'); opt.addEventListener('click', () => openDownloadSite(site.url, site.isYTDL, site.isDirect, options, button) ); opt.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { openDownloadSite(site.url, site.isYTDL, site.isDirect, options, button); } }); list.appendChild(opt); }); options.appendChild(list); return options; }; const setupDropdownHoverBehavior = (button, dropdown) => { let downloadHideTimer; const showDropdown = () => { clearTimeout(downloadHideTimer); positionDropdown(button, dropdown); dropdown.classList.add('visible'); button.setAttribute('aria-expanded', 'true'); }; const hideDropdown = () => { clearTimeout(downloadHideTimer); downloadHideTimer = setTimeout(() => { dropdown.classList.remove('visible'); button.setAttribute('aria-expanded', 'false'); }, 180); }; button.addEventListener('mouseenter', () => { clearTimeout(downloadHideTimer); showDropdown(); }); button.addEventListener('mouseleave', () => { clearTimeout(downloadHideTimer); downloadHideTimer = setTimeout(hideDropdown, 180); }); dropdown.addEventListener('mouseenter', () => { clearTimeout(downloadHideTimer); showDropdown(); }); dropdown.addEventListener('mouseleave', () => { clearTimeout(downloadHideTimer); downloadHideTimer = setTimeout(hideDropdown, 180); }); button.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { if (dropdown.classList.contains('visible')) { hideDropdown(); } else { showDropdown(); } } }); }; const createDownloadButtonManager = config => { const { settings, t, getElement, YouTubeUtils } = config; const actions = createDownloadActions(t, YouTubeUtils); const buildDownloadSites = createDownloadSitesBuilder(t); const addDownloadButton = controls => { if (!settings.enableDownload) return; try { const existingBtn = controls.querySelector('.ytp-download-button'); if (existingBtn) existingBtn.remove(); } catch { } const videoId = new URLSearchParams(location.search).get('v'); const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href; const customization = settings.downloadSiteCustomization || { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, }; const enabledSites = settings.downloadSites || { y2mate: true, ytdl: true, direct: true }; const { downloadSites } = buildDownloadSites(customization, enabledSites, videoId, videoUrl); const button = createButtonElement(t); if (downloadSites.length === 1) { const singleSite = downloadSites[0]; button.style.cursor = 'pointer'; const tempDropdown = document.createElement('div'); button.addEventListener('click', () => actions.openDownloadSite( singleSite.url, singleSite.isYTDL, singleSite.isDirect, tempDropdown, button ) ); controls.insertBefore(button, controls.firstChild); return; } const dropdown = createDropdownOptions(downloadSites, button, actions.openDownloadSite); const existingDownload = document.querySelector('.download-options'); if (existingDownload) existingDownload.remove(); try { document.body.appendChild(dropdown); } catch { button.appendChild(dropdown); } setupDropdownHoverBehavior(button, dropdown); try { if (typeof window !== 'undefined') { window.youtubePlus = window.youtubePlus || {}; window.youtubePlus.downloadButtonManager = window.youtubePlus.downloadButtonManager || {}; window.youtubePlus.downloadButtonManager.addDownloadButton = window.youtubePlus.downloadButtonManager.addDownloadButton || (() => {}); window.youtubePlus.downloadButtonManager.refreshDownloadButton = window.youtubePlus.downloadButtonManager.refreshDownloadButton || (() => {}); window.youtubePlus.downloadButtonManager.addDownloadButton = controlsArg => addDownloadButton(controlsArg); window.youtubePlus.downloadButtonManager.refreshDownloadButton = () => { try { const btn = document.querySelector('.ytp-download-button'); const dd = document.querySelector('.download-options'); if (settings.enableDownload) { if (btn) btn.style.display = ''; if (dd) dd.style.display = ''; } else { if (btn) btn.style.display = 'none'; if (dd) dd.style.display = 'none'; } } catch { } }; window.youtubePlus.rebuildDownloadDropdown = () => { try { const controlsEl = document.querySelector('.ytp-right-controls'); if (!controlsEl) return; window.youtubePlus.downloadButtonManager.addDownloadButton(controlsEl); window.youtubePlus.settings = window.youtubePlus.settings || settings; } catch (e) { console.warn('[YouTube+] rebuildDownloadDropdown failed:', e); } }; } } catch (e) { console.warn('[YouTube+] expose rebuildDownloadDropdown failed:', e); } controls.insertBefore(button, controls.firstChild); }; const refreshDownloadButton = () => { const button = getElement('.ytp-download-button'); const dropdown = document.querySelector('.download-options'); if (settings.enableDownload) { if (button) button.style.display = ''; if (dropdown) dropdown.style.display = ''; } else { if (button) button.style.display = 'none'; if (dropdown) dropdown.style.display = 'none'; } }; return { addDownloadButton, refreshDownloadButton, }; }; if (typeof window !== 'undefined') { window.YouTubePlusDownloadButton = { createDownloadButtonManager }; } const YouTubeUtils = (() => { 'use strict'; const Security = window.YouTubePlusSecurity || {}; const Storage = window.YouTubePlusStorage || {}; const Performance = window.YouTubePlusPerformance || {}; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') { return _globalI18n.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const logError = (module, message, error) => { console.error(`[YouTube+][${module}] ${message}:`, error); }; const safeExecute = Security.safeExecute || ((fn, context = 'Unknown') => { return function (...args) { try { return fn.call(this, ...args); } catch (error) { logError(context, 'Execution failed', error); return null; } }; }); const safeExecuteAsync = Security.safeExecuteAsync || ((fn, context = 'Unknown') => { return async function (...args) { try { return await fn.call(this, ...args); } catch (error) { logError(context, 'Async execution failed', error); return null; } }; }); const sanitizeHTML = Security.sanitizeHTML || (html => { if (typeof html !== 'string') return ''; return html.replace(/[<>&"'\/`=]/g, ''); }); const isValidURL = Security.isValidURL || (url => { if (typeof url !== 'string') return false; try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol); } catch { return false; } }); const storage = Storage || { get: (key, defaultValue = null) => { try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch { return defaultValue; } }, set: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; } }, remove: key => { try { localStorage.removeItem(key); return true; } catch { return false; } }, }; const debounce = Performance.debounce || ((func, wait, options = {}) => { let timeout = null; const debounced = function (...args) { if (timeout !== null) clearTimeout(timeout); if (options.leading && timeout === null) { func.call(this, ...args); } timeout = setTimeout(() => { if (!options.leading) func.call(this, ...args); timeout = null; }, wait); }; debounced.cancel = () => { if (timeout !== null) clearTimeout(timeout); timeout = null; }; return debounced; }); const throttle = Performance.throttle || ((func, limit) => { let inThrottle = false; return function (...args) { if (!inThrottle) { func.call(this, ...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; }); const createElement = (tag, props = {}, children = []) => { const validTags = /^[a-z][a-z0-9-]*$/i; if (!validTags.test(tag)) { logError('createElement', 'Invalid tag name', new Error(`Tag "${tag}" is not allowed`)); return document.createElement('div'); } const element = document.createElement(tag); Object.entries(props).forEach(([key, value]) => { if (key === 'className') { element.className = value; } else if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else if (key.startsWith('on') && typeof value === 'function') { element.addEventListener(key.substring(2).toLowerCase(), value); } else if (key === 'dataset' && typeof value === 'object') { Object.assign(element.dataset, value); } else if (key === 'innerHTML' || key === 'outerHTML') { logError( 'createElement', 'Direct HTML injection prevented', new Error('Use children array instead') ); } else { try { element.setAttribute(key, value); } catch (e) { logError('createElement', `Failed to set attribute ${key}`, e); } } }); children.forEach(child => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { element.appendChild(child); } }); return element; }; const selectorCache = new Map(); const CACHE_MAX_SIZE = 50; const CACHE_MAX_AGE = 5000; const querySelector = (selector, nocache = false) => { if (nocache) return document.querySelector(selector); const now = Date.now(); const cached = selectorCache.get(selector); if (cached?.element?.isConnected && now - cached.timestamp < CACHE_MAX_AGE) { return cached.element; } if (cached) { selectorCache.delete(selector); } const element = document.querySelector(selector); if (element) { if (selectorCache.size >= CACHE_MAX_SIZE) { const firstKey = selectorCache.keys().next().value; selectorCache.delete(firstKey); } selectorCache.set(selector, { element, timestamp: now }); } return element; }; const validateWaitParams = (selector, parent) => { if (!selector || typeof selector !== 'string') { return new Error('Selector must be a non-empty string'); } if (!parent || !(parent instanceof Element)) { return new Error('Parent must be a valid DOM element'); } return null; }; const tryQuerySelector = (parent, selector) => { try { const element = parent.querySelector(selector); return { element, error: null }; } catch { return { element: null, error: new Error(`Invalid selector: ${selector}`) }; } }; const cleanupWaitResources = (observer, timeoutId, controller) => { controller.abort(); if (observer) { try { observer.disconnect(); } catch (e) { logError('waitForElement', 'Observer disconnect failed', e); } } clearTimeout(timeoutId); }; const createWaitObserver = (parent, selector, resolve, timeoutId) => { return new MutationObserver(() => { try { const element = parent.querySelector(selector); if (element) { clearTimeout(timeoutId); resolve( ( (element))); } } catch (e) { logError('waitForElement', 'Observer callback error', e); } }); }; const startWaitObservation = (observer, parent) => { try { if (!(parent instanceof Element) && parent !== document) { throw new Error('Parent does not support observation'); } observer.observe(parent, { childList: true, subtree: true }); return null; } catch { try { observer.observe(parent, { childList: true, subtree: true }); return null; } catch { return new Error('Failed to observe DOM'); } } }; const waitForElement = (selector, timeout = 5000, parent = document.body) => { return new Promise((resolve, reject) => { const validationError = validateWaitParams(selector, parent); if (validationError) { reject(validationError); return; } const { element, error } = tryQuerySelector(parent, selector); if (error) { reject(error); return; } if (element) { resolve( ( (element))); return; } const controller = new AbortController(); let observer = null; const timeoutId = setTimeout(() => { cleanupWaitResources(observer, timeoutId, controller); reject(new Error(`Element ${selector} not found within ${timeout}ms`)); }, timeout); observer = createWaitObserver(parent, selector, resolve, timeoutId); const observeError = startWaitObservation(observer, parent); if (observeError) { clearTimeout(timeoutId); reject(observeError); } }); }; const cleanupManager = { observers: new Set(), listeners: new Map(), intervals: new Set(), timeouts: new Set(), animationFrames: new Set(), cleanupFunctions: new Set(), register: fn => { if (typeof fn === 'function') { cleanupManager.cleanupFunctions.add(fn); } return fn; }, unregister: fn => { cleanupManager.cleanupFunctions.delete(fn); }, registerObserver: observer => { cleanupManager.observers.add(observer); return observer; }, unregisterObserver: observer => { if (observer) { try { observer.disconnect(); } catch (e) { logError('Cleanup', 'Observer disconnect failed', e); } cleanupManager.observers.delete(observer); } }, registerListener: (element, event, handler, options) => { const key = Symbol('listener'); cleanupManager.listeners.set(key, { element, event, handler, options }); try { element.addEventListener(event, (handler), options); } catch { } return key; }, unregisterListener: key => { const listener = cleanupManager.listeners.get(key); if (listener) { const { element, event, handler, options } = listener; try { element.removeEventListener(event, handler, options); } catch (e) { logError('Cleanup', 'Listener removal failed', e); } cleanupManager.listeners.delete(key); } }, registerInterval: id => { cleanupManager.intervals.add(id); return id; }, unregisterInterval: id => { clearInterval(id); cleanupManager.intervals.delete(id); }, registerTimeout: id => { cleanupManager.timeouts.add(id); return id; }, unregisterTimeout: id => { clearTimeout(id); cleanupManager.timeouts.delete(id); }, registerAnimationFrame: id => { cleanupManager.animationFrames.add(id); return id; }, unregisterAnimationFrame: id => { cancelAnimationFrame(id); cleanupManager.animationFrames.delete(id); }, cleanup: () => { cleanupManager.cleanupFunctions.forEach(fn => { try { fn(); } catch (e) { logError('Cleanup', 'Cleanup function failed', e); } }); cleanupManager.cleanupFunctions.clear(); cleanupManager.observers.forEach(obs => { try { obs.disconnect(); } catch (e) { logError('Cleanup', 'Observer disconnect failed', e); } }); cleanupManager.observers.clear(); cleanupManager.listeners.forEach(({ element, event, handler, options }) => { try { element.removeEventListener(event, handler, options); } catch (e) { logError('Cleanup', 'Listener removal failed', e); } }); cleanupManager.listeners.clear(); cleanupManager.intervals.forEach(id => clearInterval(id)); cleanupManager.intervals.clear(); cleanupManager.timeouts.forEach(id => clearTimeout(id)); cleanupManager.timeouts.clear(); cleanupManager.animationFrames.forEach(id => cancelAnimationFrame(id)); cleanupManager.animationFrames.clear(); }, }; const SettingsManager = { storageKey: 'youtube_plus_all_settings_v2', defaults: { speedControl: { enabled: true, currentSpeed: 1 }, screenshot: { enabled: true }, download: { enabled: true }, updateChecker: { enabled: true }, adBlocker: { enabled: true }, pip: { enabled: true }, timecodes: { enabled: true }, }, load() { const saved = storage.get(this.storageKey); return saved ? { ...this.defaults, ...saved } : { ...this.defaults }; }, save(settings) { storage.set(this.storageKey, settings); window.dispatchEvent( new CustomEvent('youtube-plus-settings-changed', { detail: settings, }) ); }, get(path) { const settings = this.load(); return path.split('.').reduce((obj, key) => (obj)?.[key], settings); }, set(path, value) { const settings = this.load(); const keys = path.split('.'); const last = keys.pop(); const target = keys.reduce((obj, key) => { (obj)[key] = (obj)[key] || {}; return (obj)[key]; }, settings); (target)[ (last)] = value; this.save(settings); }, }; const StyleManager = { styles: new Map(), element: null, add(id, css) { if (typeof id !== 'string' || !id) { logError('StyleManager', 'Invalid style ID', new Error('ID must be a non-empty string')); return; } if (typeof css !== 'string') { logError('StyleManager', 'Invalid CSS', new Error('CSS must be a string')); return; } this.styles.set(id, css); this.update(); }, remove(id) { this.styles.delete(id); this.update(); }, update() { try { if (!this.element) { this.element = document.createElement('style'); this.element.id = 'youtube-plus-styles'; this.element.type = 'text/css'; (document.head || document.documentElement).appendChild(this.element); } this.element.textContent = Array.from(this.styles.values()).join('\n'); } catch (error) { logError('StyleManager', 'Failed to update styles', error); } }, clear() { this.styles.clear(); if (this.element) { try { this.element.remove(); } catch (e) { logError('StyleManager', 'Failed to remove style element', e); } this.element = null; } }, }; const NotificationManager = { queue: [], activeNotifications: new Set(), maxVisible: 3, defaultDuration: 3000, show(message, options = {}) { if (!message || typeof message !== 'string') { logError( 'NotificationManager', 'Invalid message', new Error('Message must be a non-empty string') ); return null; } const { duration = this.defaultDuration, position = null, action = null, } = options; this.activeNotifications.forEach(notif => { if (notif.dataset.message === message) { this.remove(notif); } }); const positions = { 'top-right': { top: '20px', right: '20px' }, 'top-left': { top: '20px', left: '20px' }, 'bottom-right': { bottom: '20px', right: '20px' }, 'bottom-left': { bottom: '20px', left: '20px' }, }; try { const notification = createElement('div', { className: 'youtube-enhancer-notification', dataset: { message }, style: { zIndex: '10001', width: 'auto', display: 'flex', alignItems: 'center', gap: '10px', ...(position && (positions)[position] ? (positions)[position] : {}), }, }); notification.setAttribute('role', 'status'); notification.setAttribute('aria-live', 'polite'); notification.setAttribute('aria-atomic', 'true'); const messageSpan = createElement( 'span', { style: { flex: '1' }, }, [message] ); notification.appendChild(messageSpan); if (action && action.text && typeof action.callback === 'function') { const actionBtn = createElement( 'button', { style: { background: 'rgba(255,255,255,0.2)', border: '1px solid rgba(255,255,255,0.3)', color: 'white', padding: '4px 12px', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', fontWeight: '600', transition: 'background 0.2s', }, onClick: () => { action.callback(); this.remove(notification); }, }, [action.text] ); notification.appendChild(actionBtn); } const _notifContainerId = 'youtube-enhancer-notification-container'; let _notifContainer = document.getElementById(_notifContainerId); if (!_notifContainer) { _notifContainer = createElement('div', { id: _notifContainerId, className: 'youtube-enhancer-notification-container', }); try { document.body.appendChild(_notifContainer); } catch { document.body.appendChild(notification); this.activeNotifications.add(notification); } } try { _notifContainer.insertBefore(notification, _notifContainer.firstChild); } catch { document.body.appendChild(notification); } try { notification.style.pointerEvents = 'auto'; } catch {} this.activeNotifications.add(notification); try { notification.style.animation = 'slideInFromBottom 0.38s ease-out forwards'; } catch {} if (duration > 0) { const timeoutId = setTimeout(() => this.remove(notification), duration); cleanupManager.registerTimeout(timeoutId); } if (this.activeNotifications.size > this.maxVisible) { const oldest = Array.from(this.activeNotifications)[0]; this.remove(oldest); } return notification; } catch (error) { logError('NotificationManager', 'Failed to show notification', error); return null; } }, remove(notification) { if (!notification || !notification.isConnected) return; try { try { notification.style.animation = 'slideOutToBottom 0.32s ease-in forwards'; const timeoutId = setTimeout(() => { try { notification.remove(); this.activeNotifications.delete(notification); } catch (e) { logError('NotificationManager', 'Failed to remove notification', e); } }, 340); cleanupManager.registerTimeout(timeoutId); } catch { try { notification.remove(); this.activeNotifications.delete(notification); } catch (e) { logError('NotificationManager', 'Failed to remove notification (fallback)', e); } } } catch (error) { logError('NotificationManager', 'Failed to animate notification removal', error); notification.remove(); this.activeNotifications.delete(notification); } }, clearAll() { this.activeNotifications.forEach(notif => { try { notif.remove(); } catch (e) { logError('NotificationManager', 'Failed to clear notification', e); } }); this.activeNotifications.clear(); }, }; StyleManager.add( 'notification-animations', ` @keyframes slideInFromBottom { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideOutToBottom { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } } ` ); window.addEventListener('beforeunload', () => { cleanupManager.cleanup(); selectorCache.clear(); StyleManager.clear(); NotificationManager.clearAll(); }); const cacheCleanupInterval = setInterval(() => { const now = Date.now(); for (const [key, value] of selectorCache.entries()) { if (!value.element?.isConnected || now - value.timestamp > CACHE_MAX_AGE) { selectorCache.delete(key); } } }, 30000); cleanupManager.registerInterval(cacheCleanupInterval); window.addEventListener('unhandledrejection', event => { logError('Global', 'Unhandled promise rejection', event.reason); event.preventDefault(); }); window.addEventListener('error', event => { if (event.filename && event.filename.includes('youtube')) { logError( 'Global', 'Uncaught error', new Error(`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`) ); } }); const measurePerformance = (label, fn) => { return function (...args) { const start = performance.now(); try { const result = fn.apply(this, args); const duration = performance.now() - start; if (duration > 100) { console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`); } return result; } catch (error) { logError('Performance', `${label} failed`, error); throw error; } }; }; const measurePerformanceAsync = (label, fn) => { return async function (...args) { const start = performance.now(); try { const result = await fn.apply(this, args); const duration = performance.now() - start; if (duration > 100) { console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`); } return result; } catch (error) { logError('Performance', `${label} failed`, error); throw error; } }; }; const isMobile = () => { return ( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768 ); }; const getViewport = () => ({ width: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0), height: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0), }); const retryAsync = async (fn, retries = 3, delay = 1000) => { for (let i = 0; i < retries; i++) { try { return await fn(); } catch (error) { if (i === retries - 1) throw error; await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); } } }; return { logError, safeExecute, safeExecuteAsync, sanitizeHTML, isValidURL, storage, debounce, throttle, createElement, querySelector, waitForElement, cleanupManager, SettingsManager, StyleManager, NotificationManager, clearCache: () => selectorCache.clear(), isMobile, getViewport, retryAsync, measurePerformance, measurePerformanceAsync, t, }; })(); if (typeof window !== 'undefined') { (window).YouTubeUtils = (window).YouTubeUtils || {}; const existing = (window).YouTubeUtils; try { for (const k of Object.keys(YouTubeUtils)) { if (existing[k] === undefined) existing[k] = YouTubeUtils[k]; } } catch {} console.log('[YouTube+ v2.2] Core utilities merged'); (window).YouTubePlusDebug = { version: '2.2', cacheSize: () => YouTubeUtils.cleanupManager.observers.size + YouTubeUtils.cleanupManager.listeners.size + YouTubeUtils.cleanupManager.intervals.size, clearAll: () => { YouTubeUtils.cleanupManager.cleanup(); YouTubeUtils.clearCache(); YouTubeUtils.StyleManager.clear(); YouTubeUtils.NotificationManager.clearAll(); console.log('[YouTube+] All resources cleared'); }, stats: () => ({ observers: YouTubeUtils.cleanupManager.observers.size, listeners: YouTubeUtils.cleanupManager.listeners.size, intervals: YouTubeUtils.cleanupManager.intervals.size, timeouts: YouTubeUtils.cleanupManager.timeouts.size, animationFrames: YouTubeUtils.cleanupManager.animationFrames.size, styles: YouTubeUtils.StyleManager.styles.size, notifications: YouTubeUtils.NotificationManager.activeNotifications.size, }), }; if (!sessionStorage.getItem('youtube_plus_started')) { sessionStorage.setItem('youtube_plus_started', 'true'); setTimeout(() => { if (YouTubeUtils.NotificationManager) { YouTubeUtils.NotificationManager.show('YouTube+ v2.2 loaded', { type: 'success', duration: 2000, position: 'bottom-right', }); } }, 1000); } } (function () { 'use strict'; const { t } = YouTubeUtils; const YouTubeEnhancer = { speedControl: { currentSpeed: 1, activeAnimationId: null, storageKey: 'youtube_playback_speed', }, _initialized: false, settings: { enableSpeedControl: true, enableScreenshot: true, enableDownload: true, downloadSites: { direct: true, y2mate: true, ytdl: true, }, downloadSiteCustomization: { y2mate: typeof window !== 'undefined' && window.YouTubePlusConstants ? window.YouTubePlusConstants.DOWNLOAD_SITES.Y2MATE : { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, }, storageKey: 'youtube_plus_settings', hideSideGuide: false, }, _cache: new Map(), getElement(selector, useCache = true) { if (useCache && this._cache.has(selector)) { const element = this._cache.get(selector); if (element?.isConnected) return element; this._cache.delete(selector); } const element = document.querySelector(selector); if (element && useCache) this._cache.set(selector, element); return element; }, loadSettings() { try { const saved = localStorage.getItem(this.settings.storageKey); if (saved) { Object.assign(this.settings, JSON.parse(saved)); return; } try { if ( typeof window !== 'undefined' && window.YouTubeUtils && YouTubeUtils.SettingsManager ) { const globalSettings = YouTubeUtils.SettingsManager.load(); if (!globalSettings) return; const sc = globalSettings.speedControl; if (sc && typeof sc.enabled === 'boolean') { this.settings.enableSpeedControl = sc.enabled; } const ss = globalSettings.screenshot; if (ss && typeof ss.enabled === 'boolean') this.settings.enableScreenshot = ss.enabled; const dl = globalSettings.download; if (dl && typeof dl.enabled === 'boolean') this.settings.enableDownload = dl.enabled; if (globalSettings.downloadSites && typeof globalSettings.downloadSites === 'object') { this.settings.downloadSites = { ...(this.settings.downloadSites || {}), ...globalSettings.downloadSites, }; } } } catch { } } catch (e) { console.error('Error loading settings:', e); } }, init() { if (this._initialized) { return; } this._initialized = true; try { this.loadSettings(); } catch (error) { console.warn('[YouTube+][Basic]', 'Failed to load settings during init:', error); } this.insertStyles(); this.addSettingsButtonToHeader(); this.setupNavigationObserver(); if (location.href.includes('watch?v=')) { this.setupCurrentPage(); } document.addEventListener('visibilitychange', () => { if (!document.hidden && location.href.includes('watch?v=')) { this.setupCurrentPage(); } }); }, saveSettings() { localStorage.setItem(this.settings.storageKey, JSON.stringify(this.settings)); this.updatePageBasedOnSettings(); this.refreshDownloadButton(); }, updatePageBasedOnSettings() { const settingsMap = { 'ytp-screenshot-button': 'enableScreenshot', 'ytp-download-button': 'enableDownload', 'speed-control-btn': 'enableSpeedControl', }; Object.entries(settingsMap).forEach(([className, setting]) => { const button = this.getElement(`.${className}`, false); if (button) button.style.display = this.settings[setting] ? '' : 'none'; }); const speedOptions = document.querySelector('.speed-options'); if (speedOptions) { speedOptions.style.display = this.settings.enableSpeedControl ? '' : 'none'; } }, refreshDownloadButton() { if (typeof window !== 'undefined' && window.YouTubePlusDownloadButton) { const manager = window.YouTubePlusDownloadButton.createDownloadButtonManager({ settings: this.settings, t, getElement: this.getElement.bind(this), YouTubeUtils, }); manager.refreshDownloadButton(); } }, setupCurrentPage() { this.waitForElement('#player-container-outer .html5-video-player, .ytp-right-controls', 5000) .then(() => { this.addCustomButtons(); this.setupVideoObserver(); this.applyCurrentSpeed(); this.updatePageBasedOnSettings(); this.refreshDownloadButton(); }) .catch(() => {}); }, insertStyles() { const styles = `:root{--yt-accent:#ff0000;--yt-accent-hover:#cc0000;--yt-radius-sm:6px;--yt-radius-md:10px;--yt-radius-lg:16px;--yt-transition:all .2s ease;--yt-space-xs:4px;--yt-space-sm:8px; --yt-space-md:16px;--yt-space-lg:24px;--yt-glass-blur:blur(18px) saturate(180%);--yt-glass-blur-light:blur(12px) saturate(160%);--yt-glass-blur-heavy:blur(24px) saturate(200%);} html[dark],html:not([dark]):not([light]){--yt-bg-primary:rgba(15,15,15,.85);--yt-bg-secondary:rgba(28,28,28,.85);--yt-bg-tertiary:rgba(34,34,34,.85);--yt-text-primary:#fff;--yt-text-secondary:#aaa;--yt-border-color:rgba(255,255,255,.2);--yt-hover-bg:rgba(255,255,255,.1);--yt-shadow:0 4px 12px rgba(0,0,0,.25);--yt-glass-bg:rgba(255,255,255,.1);--yt-glass-border:rgba(255,255,255,.2);--yt-glass-shadow:0 8px 32px rgba(0,0,0,.2);--yt-modal-bg:rgba(0,0,0,.75);--yt-notification-bg:rgba(28,28,28,.9);--yt-panel-bg:rgba(34,34,34,.3);--yt-header-bg:rgba(20,20,20,.6);--yt-input-bg:rgba(255,255,255,.1);--yt-button-bg:rgba(255,255,255,.2);--yt-text-stroke:white;} html[light]{--yt-bg-primary:rgba(255,255,255,.85);--yt-bg-secondary:rgba(248,248,248,.85);--yt-bg-tertiary:rgba(240,240,240,.85);--yt-text-primary:#030303;--yt-text-secondary:#606060;--yt-border-color:rgba(0,0,0,.2);--yt-hover-bg:rgba(0,0,0,.05);--yt-shadow:0 4px 12px rgba(0,0,0,.15);--yt-glass-bg:rgba(255,255,255,.7);--yt-glass-border:rgba(0,0,0,.1);--yt-glass-shadow:0 8px 32px rgba(0,0,0,.1);--yt-modal-bg:rgba(0,0,0,.5);--yt-notification-bg:rgba(255,255,255,.95);--yt-panel-bg:rgba(255,255,255,.7);--yt-header-bg:rgba(248,248,248,.8);--yt-input-bg:rgba(0,0,0,.05);--yt-button-bg:rgba(0,0,0,.1);--yt-text-stroke:#030303;} .ytp-screenshot-button,.ytp-cobalt-button,.ytp-pip-button{position:relative;width:44px;height:100%;display:inline-flex;align-items:center;justify-content:center;vertical-align:top;transition:opacity .15s,transform .15s;} .ytp-screenshot-button:hover,.ytp-cobalt-button:hover,.ytp-pip-button:hover{transform:scale(1.1);} .speed-control-btn{width:4em!important;position:relative!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;height:100%!important;vertical-align:top!important;text-align:center!important;border-radius:var(--yt-radius-sm);font-size:13px;color:var(--yt-text-primary);cursor:pointer;user-select:none;font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:color .2s;} .speed-control-btn:hover{color:var(--yt-accent);font-weight:bold;} .speed-options{position:fixed!important;background:var(--yt-glass-bg)!important;color:var(--yt-text-primary)!important;border-radius:var(--yt-radius-md)!important;display:flex!important;flex-direction:column!important;align-items:stretch!important;gap:0!important;transform:translate(-50%,12px)!important;width:92px!important;z-index:2147483647!important;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);overflow:hidden;backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);opacity:0;pointer-events:none!important;transition:opacity .18s ease,transform .18s ease;box-sizing:border-box;} .speed-options.visible{opacity:1;pointer-events:auto!important;transform:translate(-50%,0)!important;} .speed-option-item{cursor:pointer!important;height:28px!important;line-height:28px!important;font-size:12px!important;text-align:center!important;transition:background-color .15s,color .15s;} .speed-option-active,.speed-option-item:hover{color:var(--yt-accent)!important;font-weight:bold!important;background:var(--yt-hover-bg)!important;} #speed-indicator{position:absolute!important;margin:auto!important;top:0!important;right:0!important;bottom:0!important;left:0!important;border-radius:24px!important;font-size:30px!important;background:var(--yt-glass-bg)!important;color:var(--yt-text-primary)!important;z-index:99999!important;width:80px!important;height:80px!important;line-height:80px!important;text-align:center!important;display:none;box-shadow:var(--yt-glass-shadow);backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);border:1px solid var(--yt-glass-border);} .youtube-enhancer-notification-container{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:10px;z-index:2147483647;pointer-events:none;max-width:calc(100% - 32px);width:100%;box-sizing:border-box;padding:0 16px;} .youtube-enhancer-notification{position:relative;max-width:700px;width:auto;background:var(--yt-glass-bg);color:var(--yt-text-primary);padding:8px 14px;font-size:13px;border-radius:var(--yt-radius-md);z-index:inherit;transition:opacity .35s,transform .32s;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);backdrop-filter:var(--yt-glass-blur); -webkit-backdrop-filter:var(--yt-glass-blur);font-weight:500;box-sizing:border-box;display:flex;align-items:center;gap:10px;pointer-events:auto;} .ytp-plus-settings-button{background:transparent;border:none;color:var(--yt-text-secondary);cursor:pointer;padding:var(--yt-space-sm);margin-right:var(--yt-space-sm);border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color .2s,transform .2s;} .ytp-plus-settings-button svg{width:24px;height:24px;} .ytp-plus-settings-button:hover{background:var(--yt-hover-bg);transform:rotate(30deg);color:var(--yt-text-secondary);} .ytp-plus-settings-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:100000;backdrop-filter:blur(8px) saturate(140%);-webkit-backdrop-filter:blur(8px) saturate(140%);animation:ytEnhanceFadeIn .25s ease-out;} .ytp-plus-settings-panel{background:var(--yt-glass-bg);color:var(--yt-text-primary);border-radius:20px;width:760px;max-width:94%;max-height:60vh;overflow:hidden;box-shadow:0 12px 40px rgba(0,0,0,0.45);animation:ytEnhanceScaleIn .28s cubic-bezier(.4,0,.2,1);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%);border:1.5px solid var(--yt-glass-border);will-change:transform,opacity;display:flex;flex-direction:row} .ytp-plus-settings-sidebar{width:240px;background:var(--yt-header-bg);border-right:1px solid var(--yt-glass-border);display:flex;flex-direction:column;backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);} .ytp-plus-settings-sidebar-header{padding:var(--yt-space-md) var(--yt-space-lg);border-bottom:1px solid var(--yt-glass-border);display:flex;justify-content:space-between;align-items:center;} .ytp-plus-settings-title{font-size:18px;font-weight:500;margin:0;color:var(--yt-text-primary);} .ytp-plus-settings-sidebar-close{padding:var(--yt-space-md) var(--yt-space-lg);display:flex;justify-content:flex-end;background:transparent;} .ytp-plus-settings-close{background:none;border:none;cursor:pointer;padding:var(--yt-space-sm);margin:-8px;color:var(--yt-text-primary);transition:color .2s,transform .2s;} .ytp-plus-settings-close:hover{color:var(--yt-accent);transform:scale(1.25) rotate(90deg);} .ytp-plus-settings-nav{flex:1;padding:var(--yt-space-md) 0;} .ytp-plus-settings-nav-item{display:flex;align-items:center;padding:12px var(--yt-space-lg);cursor:pointer;transition:all .2s cubic-bezier(.4,0,.2,1);font-size:14px;border-left:3px solid transparent;color:var(--yt-text-primary);} .ytp-plus-settings-nav-item:hover{background:var(--yt-hover-bg);} .ytp-plus-settings-nav-item.active{background:rgba(255,0,0,.1);border-left-color:var(--yt-accent);color:var(--yt-accent);font-weight:500;} .ytp-plus-settings-nav-item svg{width:18px;height:18px;margin-right:12px;opacity:.8;transition:opacity .2s,transform .2s;} .ytp-plus-settings-nav-item.active svg{opacity:1;transform:scale(1.1);} .ytp-plus-settings-nav-item:hover svg{transform:scale(1.05);} .ytp-plus-settings-main{flex:1;display:flex;flex-direction:column;overflow-y:auto;} .ytp-plus-settings-header{padding:var(--yt-space-md) var(--yt-space-lg);border-bottom:1px solid var(--yt-glass-border);background:var(--yt-header-bg);backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);} .ytp-plus-settings-content{flex:1;padding:var(--yt-space-md) var(--yt-space-lg);overflow-y:auto;} .ytp-plus-settings-section{margin-bottom:var(--yt-space-lg);} .ytp-plus-settings-section-title{font-size:16px;font-weight:500;margin-bottom:var(--yt-space-md);color:var(--yt-text-primary);} .ytp-plus-settings-section.hidden{display:none;} .ytp-plus-settings-item{display:flex;align-items:center;margin-bottom:var(--yt-space-md);padding:14px 18px;background:transparent;transition:all .25s cubic-bezier(.4,0,.2,1);border-radius:var(--yt-radius-md);} .ytp-plus-settings-item:hover{background:var(--yt-hover-bg);transform:translateX(6px);box-shadow:0 2px 8px rgba(0,0,0,.1);} .ytp-plus-settings-item-label{flex:1;font-size:14px;color:var(--yt-text-primary);} .ytp-plus-settings-item-description{font-size:12px;color:var(--yt-text-secondary);margin-top:4px;} .ytp-plus-settings-checkbox{appearance:none;-webkit-appearance:none;-moz-appearance:none;width:15px;height:15px;margin-left:auto;border:1px solid var(--yt-glass-border);border-radius:50%;background:transparent;display:inline-flex;align-items:center;justify-content:center;transition:all 250ms cubic-bezier(.4,0,.23,1);cursor:pointer;position:relative;flex-shrink:0;color:#fff;} html:not([dark]) .ytp-plus-settings-checkbox{border-color:rgba(0,0,0,.25);color:#222;} .ytp-plus-settings-checkbox:focus-visible{outline:2px solid var(--yt-accent);outline-offset:2px;} .ytp-plus-settings-checkbox:hover{background:var(--yt-hover-bg);transform:scale(1.1);} .ytp-plus-settings-checkbox::before{content:"";width:4px;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(45deg);top:4px;left:3px;transition:width 100ms ease 50ms,opacity 50ms;transform-origin:0% 0%;opacity:0;} .ytp-plus-settings-checkbox::after{content:"";width:0;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(305deg);top:9px;left:6px;transition:width 100ms ease,opacity 50ms;transform-origin:0% 0%;opacity:0;} .ytp-plus-settings-checkbox:checked{transform:rotate(0deg) scale(1.2);} .ytp-plus-settings-checkbox:checked::before{width:8px;opacity:1;background:#fff;transition:width 150ms ease 100ms,opacity 150ms ease 100ms;} .ytp-plus-settings-checkbox:checked::after{width:15px;opacity:1;background:#fff;transition:width 150ms ease 250ms,opacity 150ms ease 250ms;} .ytp-plus-footer{padding:var(--yt-space-md) var(--yt-space-lg);border-top:1px solid var(--yt-glass-border);display:flex;justify-content:flex-end;background:transparent;} .ytp-plus-button{padding:var(--yt-space-sm) var(--yt-space-md);border-radius:18px;border:none;font-size:14px;font-weight:500;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1);} .ytp-plus-button-primary{background:transparent;border:1px solid var(--yt-glass-border);color:var(--yt-text-primary);} .ytp-plus-button-primary:hover{background:var(--yt-accent);color:#fff;box-shadow:0 6px 16px rgba(255,0,0,.35);transform:translateY(-2px);} .app-icon{fill:var(--yt-text-primary);stroke:var(--yt-text-primary);transition:all .3s;} @keyframes ytEnhanceFadeIn{from{opacity:0;}to{opacity:1;}} @keyframes ytEnhanceScaleIn{from{opacity:0;transform:scale(.92) translateY(10px);}to{opacity:1;transform:scale(1) translateY(0);}} @media(max-width:768px){.ytp-plus-settings-panel{width:95%;max-height:80vh;flex-direction:column;} .ytp-plus-settings-sidebar{width:100%;max-height:120px;flex-direction:row;overflow-x:auto;} .ytp-plus-settings-nav{display:flex;flex-direction:row;padding:0;} .ytp-plus-settings-nav-item{white-space:nowrap;border-left:none;border-bottom:3px solid transparent;} .ytp-plus-settings-nav-item.active{border-left:none;border-bottom-color:var(--yt-accent);} .ytp-plus-settings-item{padding:10px 12px;}} .ytp-plus-settings-section h1{margin:-95px 90px 8px;font-family:'Montserrat',sans-serif;font-size:52px;font-weight:600;color:transparent;-webkit-text-stroke-width:1px;-webkit-text-stroke-color:var(--yt-text-stroke);cursor:pointer;transition:color .2s;} .ytp-plus-settings-section h1:hover{color:var(--yt-accent);-webkit-text-stroke-width:1px;-webkit-text-stroke-color:transparent;} .download-options{position:fixed;background:var(--yt-glass-bg);color:var(--yt-text-primary);border-radius:var(--yt-radius-md);width:150px;z-index:2147483647;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);overflow:hidden;backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease;transform:translateY(8px);box-sizing:border-box;} .download-options.visible{opacity:1;pointer-events:auto;transform:translateY(0);} .download-options-list{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;} .download-option-item{cursor:pointer;padding:12px;text-align:center;transition:background .2s,color .2s;width:100%;} .download-option-item:hover{background:var(--yt-hover-bg);color:var(--yt-accent);} .ytp-download-button{position:relative!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;height:100%!important;vertical-align:top!important;padding:0 10px!important;cursor:pointer!important;} .glass-panel{background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);box-shadow:var(--yt-glass-shadow);} .glass-card{background:var(--yt-panel-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);padding:var(--yt-space-md);backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);box-shadow:var(--yt-shadow);} .glass-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--yt-modal-bg);display:flex;align-items:center;justify-content:center;z-index:99999;backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);} .glass-button{background:var(--yt-button-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);padding:var(--yt-space-sm) var(--yt-space-md);color:var(--yt-text-primary);cursor:pointer;transition:all .2s ease;backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);} .glass-button:hover{background:var(--yt-hover-bg);transform:translateY(-1px);box-shadow:var(--yt-shadow);} .download-site-option{display:flex;flex-direction:column;align-items:stretch;gap:8px;} .download-site-header{display:flex;flex-direction:row;align-items:center;justify-content:space-between;width:100%;gap:8px;} .download-site-controls{width:100%;margin-top:6px;} .download-site-cta{display:flex;flex-direction:row;gap:8px;margin-top:6px;} .download-site-cta .glass-button{width:100%;} .download-site-option .ytp-plus-settings-checkbox{margin:0;} .download-site-name{font-weight:600;color:var(--yt-text-primary);} .download-site-desc{font-size:12px;color:var(--yt-text-secondary);margin-top:2px;} .ytSearchboxComponentInputBox { background: transparent !important; } .ytp-plus-settings-panel select, .ytp-plus-settings-panel select option {background: var(--yt-panel-bg) !important; color: var(--yt-text-primary) !important;} .ytp-plus-settings-panel select {-webkit-appearance: menulist !important; appearance: menulist !important; padding: 6px 8px !important; border-radius: 6px !important; border: 1px solid var(--yt-glass-border) !important;} `; if (!document.getElementById('yt-enhancer-styles')) { YouTubeUtils.StyleManager.add('yt-enhancer-main', styles); } }, addSettingsButtonToHeader() { this.waitForElement('ytd-masthead #end', 5000) .then(headerEnd => { if (!this.getElement('.ytp-plus-settings-button')) { const settingsButton = document.createElement('div'); settingsButton.className = 'ytp-plus-settings-button'; settingsButton.setAttribute('title', t('youtubeSettings')); settingsButton.innerHTML = ` `; settingsButton.addEventListener('click', this.openSettingsModal.bind(this)); const avatarButton = headerEnd.querySelector('ytd-topbar-menu-button-renderer'); if (avatarButton) { headerEnd.insertBefore(settingsButton, avatarButton); } else { headerEnd.appendChild(settingsButton); } } }) .catch(() => {}); }, handleModalClickActions(target, modal, handlers, markDirty, context, translate) { const navItem = ( target.classList && target.classList.contains('ytp-plus-settings-nav-item') ? target : target.closest && target.closest('.ytp-plus-settings-nav-item') ); if (navItem) { handlers.handleSidebarNavigation(navItem, modal); return; } if (target.classList.contains('ytp-plus-settings-checkbox')) { const { dataset } = (target); const { setting } = dataset; if (!setting) return; if (setting.startsWith('downloadSite_')) { const key = setting.replace('downloadSite_', ''); handlers.handleDownloadSiteToggle( target, key, this.settings, markDirty, this.saveSettings.bind(this) ); return; } handlers.handleSimpleSettingToggle( target, setting, this.settings, context, markDirty, this.saveSettings.bind(this), modal ); return; } if (target.classList.contains('download-site-input')) { const { dataset } = (target); const { site, field } = dataset; if (!site || !field) return; handlers.handleDownloadSiteInput(target, site, field, this.settings, markDirty, translate); return; } if (target.id === 'ytp-plus-save-settings' || target.id === 'ytp-plus-save-settings-icon') { this.saveSettings(); modal.remove(); this.showNotification(translate('settingsSaved')); return; } if (target.id === 'download-y2mate-save') { handlers.handleY2MateSave( target, this.settings, this.saveSettings.bind(this), this.showNotification.bind(this), translate ); return; } if (target.id === 'download-y2mate-reset') { handlers.handleY2MateReset( modal, this.settings, this.saveSettings.bind(this), this.showNotification.bind(this), translate ); } }, createSettingsModal() { const modal = document.createElement('div'); modal.className = 'ytp-plus-settings-modal'; const helpers = window.YouTubePlusSettingsHelpers; const handlers = window.YouTubePlusModalHandlers; modal.innerHTML = `
${helpers.createSettingsSidebar(t)}${helpers.createMainContent(this.settings, t)}
`; let dirty = false; const saveIconBtn = modal.querySelector('#ytp-plus-save-settings-icon'); if (saveIconBtn) saveIconBtn.style.display = 'none'; const markDirty = () => { if (dirty) return; dirty = true; if (saveIconBtn) saveIconBtn.style.display = ''; }; const context = { settings: this.settings, getElement: this.getElement.bind(this), addDownloadButton: this.addDownloadButton.bind(this), addSpeedControlButton: this.addSpeedControlButton.bind(this), refreshDownloadButton: this.refreshDownloadButton.bind(this), updatePageBasedOnSettings: this.updatePageBasedOnSettings.bind(this), }; const handleModalClick = e => { const { target } = (e); if (target === modal) { modal.remove(); return; } if ( target.id === 'ytp-plus-close-settings' || target.id === 'ytp-plus-close-settings-icon' || target.classList.contains('ytp-plus-settings-close') || target.closest('.ytp-plus-settings-close') || target.closest('#ytp-plus-close-settings') || target.closest('#ytp-plus-close-settings-icon') ) { modal.remove(); return; } if (target.id === 'open-ytdl-github' || target.closest('#open-ytdl-github')) { window.open('https://github.com/diorhc/YTDL', '_blank'); return; } this.handleModalClickActions(target, modal, handlers, markDirty, context, t); }; modal.addEventListener('click', handleModalClick); modal.addEventListener('input', e => { const { target } = (e); if (target.classList.contains('download-site-input')) { const { dataset } = (target); const { site, field } = dataset; if (!site || !field) return; handlers.handleDownloadSiteInput(target, site, field, this.settings, markDirty, t); } }); try { if ( typeof window !== 'undefined' && (window).youtubePlusReport && typeof ( (window).youtubePlusReport.render) === 'function' ) { try { (window).youtubePlusReport.render(modal); } catch (e) { YouTubeUtils.logError('Report', 'report.render failed', e); } } } catch (e) { YouTubeUtils.logError('Report', 'Failed to initialize report section', e); } return modal; }, openSettingsModal() { const existingModal = this.getElement('.ytp-plus-settings-modal', false); if (existingModal) existingModal.remove(); document.body.appendChild(this.createSettingsModal()); }, waitForElement(selector, timeout = 5000) { return YouTubeUtils.waitForElement(selector, timeout); }, addCustomButtons() { const controls = this.getElement('.ytp-right-controls'); if (!controls) return; if (!this.getElement('.ytp-screenshot-button')) this.addScreenshotButton(controls); if (!this.getElement('.ytp-download-button')) this.addDownloadButton(controls); if (!this.getElement('.speed-control-btn')) this.addSpeedControlButton(controls); if (!document.getElementById('speed-indicator')) { const indicator = document.createElement('div'); indicator.id = 'speed-indicator'; const player = document.getElementById('movie_player'); if (player) player.appendChild(indicator); } this.handleFullscreenChange(); }, addScreenshotButton(controls) { const button = document.createElement('button'); button.className = 'ytp-button ytp-screenshot-button'; button.setAttribute('title', t('takeScreenshot')); button.innerHTML = ` `; button.addEventListener('click', this.captureFrame.bind(this)); controls.insertBefore(button, controls.firstChild); }, addDownloadButton(controls) { if (typeof window !== 'undefined' && window.YouTubePlusDownloadButton) { const manager = window.YouTubePlusDownloadButton.createDownloadButtonManager({ settings: this.settings, t, getElement: this.getElement.bind(this), YouTubeUtils, }); manager.addDownloadButton(controls); } else { console.warn('[YouTube+] Download button module not loaded'); } }, addSpeedControlButton(controls) { if (!this.settings.enableSpeedControl) return; const speedBtn = document.createElement('button'); speedBtn.type = 'button'; speedBtn.className = 'ytp-button speed-control-btn'; speedBtn.setAttribute('aria-label', t('speedControl')); speedBtn.setAttribute('aria-haspopup', 'true'); speedBtn.setAttribute('aria-expanded', 'false'); speedBtn.innerHTML = `${this.speedControl.currentSpeed}×`; const speedOptions = document.createElement('div'); speedOptions.className = 'speed-options'; speedOptions.setAttribute('role', 'menu'); const selectSpeed = speed => { this.changeSpeed(speed); hideDropdown(); }; [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0].forEach(speed => { const option = document.createElement('div'); option.className = `speed-option-item${Number(speed) === this.speedControl.currentSpeed ? ' speed-option-active' : ''}`; option.textContent = `${speed}x`; option.dataset.speed = String(speed); option.setAttribute('role', 'menuitem'); option.tabIndex = 0; option.addEventListener('click', () => selectSpeed(speed)); option.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectSpeed(speed); } }); speedOptions.appendChild(option); }); speedBtn.appendChild(speedOptions); const existingSpeed = document.querySelector('.speed-options'); if (existingSpeed) existingSpeed.remove(); try { document.body.appendChild(speedOptions); } catch { } const positionDropdown = () => { const rect = speedBtn.getBoundingClientRect(); speedOptions.style.left = `${rect.left + rect.width / 2}px`; speedOptions.style.bottom = `${window.innerHeight - rect.top + 8}px`; }; const hideDropdown = () => { speedOptions.classList.remove('visible'); speedBtn.setAttribute('aria-expanded', 'false'); }; const showDropdown = () => { positionDropdown(); speedOptions.classList.add('visible'); speedBtn.setAttribute('aria-expanded', 'true'); }; const toggleDropdown = () => { if (speedOptions.classList.contains('visible')) { hideDropdown(); } else { showDropdown(); } }; let documentClickKey; const documentClickHandler = event => { if (!speedBtn.isConnected) { if (documentClickKey) { YouTubeUtils.cleanupManager.unregisterListener(documentClickKey); documentClickKey = undefined; } return; } if (!speedOptions.classList.contains('visible')) return; if ( speedBtn.contains( (event.target)) || speedOptions.contains( (event.target)) ) { return; } hideDropdown(); }; const documentKeydownHandler = event => { if (event.key === 'Escape' && speedOptions.classList.contains('visible')) { hideDropdown(); speedBtn.focus(); } }; documentClickKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', documentClickHandler, true ); YouTubeUtils.cleanupManager.registerListener( document, 'keydown', documentKeydownHandler, true ); YouTubeUtils.cleanupManager.registerListener(window, 'resize', () => { if (speedOptions.classList.contains('visible')) { positionDropdown(); } }); YouTubeUtils.cleanupManager.registerListener( window, 'scroll', () => { if (speedOptions.classList.contains('visible')) { positionDropdown(); } }, true ); let speedHideTimer; speedBtn.addEventListener('mouseenter', () => { clearTimeout(speedHideTimer); showDropdown(); }); speedBtn.addEventListener('mouseleave', () => { clearTimeout(speedHideTimer); speedHideTimer = setTimeout(hideDropdown, 200); }); speedOptions.addEventListener('mouseenter', () => { clearTimeout(speedHideTimer); showDropdown(); }); speedOptions.addEventListener('mouseleave', () => { clearTimeout(speedHideTimer); speedHideTimer = setTimeout(hideDropdown, 200); }); speedBtn.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleDropdown(); } else if (event.key === 'Escape') { hideDropdown(); } }); controls.insertBefore(speedBtn, controls.firstChild); }, applyGuideVisibility() { try { const enabled = Boolean(YouTubeUtils.storage.get('ytplus.hideGuide', false)); document.documentElement.classList.toggle('ytp-hide-guide', enabled); const btn = document.getElementById('ytplus-guide-toggle-btn'); if (btn) { btn.setAttribute('aria-pressed', String(enabled)); btn.title = enabled ? 'Show side guide' : 'Hide side guide'; } } catch (e) { console.warn('[YouTube+] applyGuideVisibility failed:', e); } }, toggleSideGuide() { try { const current = Boolean(YouTubeUtils.storage.get('ytplus.hideGuide', false)); const next = !current; YouTubeUtils.storage.set('ytplus.hideGuide', next); this.applyGuideVisibility(); } catch (e) { console.warn('[YouTube+] toggleSideGuide failed:', e); } }, createGuideToggleButton() { try { if (document.getElementById('ytplus-guide-toggle-btn')) return; const btn = document.createElement('button'); btn.id = 'ytplus-guide-toggle-btn'; btn.type = 'button'; btn.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:100000;background:var(--yt-spec-call-to-action);color:#fff;border:none;border-radius:8px;padding:8px 10px;box-shadow:0 6px 18px rgba(0,0,0,0.3);cursor:pointer;opacity:0.95;font-size:13px;'; btn.setAttribute('aria-pressed', 'false'); btn.title = 'Hide side guide'; btn.textContent = 'Toggle Guide'; btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); this.toggleSideGuide(); }); btn.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.toggleSideGuide(); } }); document.body.appendChild(btn); this.applyGuideVisibility(); } catch (e) { console.warn('[YouTube+] createGuideToggleButton failed:', e); } }, captureFrame() { const video = this.getElement('video', false); if (!video) return; const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const videoTitle = document.title.replace(/\s-\sYouTube$/, '').trim(); const link = document.createElement('a'); link.href = canvas.toDataURL('image/png'); link.download = `${videoTitle}.png`; link.click(); }, showNotification(message, duration = 2000) { YouTubeUtils.NotificationManager.show(message, { duration, type: 'info' }); }, handleFullscreenChange() { const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement; document.querySelectorAll('.ytp-screenshot-button, .ytp-cobalt-button').forEach(button => { button.style.bottom = isFullscreen ? '0px' : '0px'; }); }, changeSpeed(speed) { const numericSpeed = Number(speed); this.speedControl.currentSpeed = numericSpeed; localStorage.setItem(this.speedControl.storageKey, String(numericSpeed)); const speedBtn = this.getElement('.speed-control-btn span', false); if (speedBtn) speedBtn.textContent = `${numericSpeed}×`; document.querySelectorAll('.speed-option-item').forEach(option => { option.classList.toggle( 'speed-option-active', parseFloat(option.dataset.speed) === numericSpeed ); }); this.applyCurrentSpeed(); this.showSpeedIndicator(numericSpeed); }, applyCurrentSpeed() { document.querySelectorAll('video').forEach(video => { if (video && video.playbackRate !== this.speedControl.currentSpeed) { video.playbackRate = this.speedControl.currentSpeed; } }); }, setupVideoObserver() { if (this._speedInterval) clearInterval(this._speedInterval); this._speedInterval = setInterval(() => this.applyCurrentSpeed(), 1000); YouTubeUtils.cleanupManager.registerInterval(this._speedInterval); }, setupNavigationObserver() { let lastUrl = location.href; document.addEventListener('fullscreenchange', this.handleFullscreenChange.bind(this)); document.addEventListener('yt-navigate-finish', () => { if (location.href.includes('watch?v=')) this.setupCurrentPage(); this.addSettingsButtonToHeader(); }); const observer = new MutationObserver(() => { if (lastUrl !== location.href) { lastUrl = location.href; if (location.href.includes('watch?v=')) { setTimeout(() => this.setupCurrentPage(), 500); } this.addSettingsButtonToHeader(); } }); YouTubeUtils.cleanupManager.registerObserver(observer); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); } }, showSpeedIndicator(speed) { const indicator = document.getElementById('speed-indicator'); if (!indicator) return; if (this.speedControl.activeAnimationId) { cancelAnimationFrame(this.speedControl.activeAnimationId); YouTubeUtils.cleanupManager.unregisterAnimationFrame(this.speedControl.activeAnimationId); this.speedControl.activeAnimationId = null; } indicator.textContent = `${speed}×`; indicator.style.display = 'block'; indicator.style.opacity = '0.8'; const startTime = performance.now(); const fadeOut = timestamp => { const elapsed = timestamp - startTime; const progress = Math.min(elapsed / 1500, 1); indicator.style.opacity = String(0.8 * (1 - progress)); if (progress < 1) { this.speedControl.activeAnimationId = YouTubeUtils.cleanupManager.registerAnimationFrame( requestAnimationFrame(fadeOut) ); } else { indicator.style.display = 'none'; this.speedControl.activeAnimationId = null; } }; this.speedControl.activeAnimationId = YouTubeUtils.cleanupManager.registerAnimationFrame( requestAnimationFrame(fadeOut) ); }, }; const initFunction = YouTubeEnhancer.init.bind(YouTubeEnhancer); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFunction); } else { initFunction(); } })(); (function () { 'use strict'; if (typeof YouTubeUtils === 'undefined') { logger.error('YouTubeUtils not found!'); return; } function createSubtitleSelect() { const subtitleSelect = document.createElement('div'); subtitleSelect.setAttribute('role', 'listbox'); Object.assign(subtitleSelect.style, { position: 'relative', width: '100%', marginBottom: '8px', fontSize: '14px', color: '#fff', cursor: 'pointer', }); const _ssDisplay = document.createElement('div'); Object.assign(_ssDisplay.style, { padding: '10px 12px', borderRadius: '10px', background: 'linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))', border: '1px solid rgba(255,255,255,0.06)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', backdropFilter: 'blur(6px)', boxShadow: '0 4px 18px rgba(0,0,0,0.35) inset', }); const _ssLabel = document.createElement('div'); _ssLabel.style.flex = '1'; _ssLabel.style.overflow = 'hidden'; _ssLabel.style.textOverflow = 'ellipsis'; _ssLabel.style.whiteSpace = 'nowrap'; _ssLabel.textContent = t('loading'); const _ssChevron = document.createElement('div'); _ssChevron.textContent = '▾'; _ssChevron.style.opacity = '0.8'; _ssDisplay.appendChild(_ssLabel); _ssDisplay.appendChild(_ssChevron); const _ssList = document.createElement('div'); Object.assign(_ssList.style, { position: 'absolute', top: 'calc(100% + 8px)', left: '0', right: '0', maxHeight: '220px', overflowY: 'auto', borderRadius: '10px', background: 'linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.02))', border: '1px solid rgba(255,255,255,0.06)', boxShadow: '0 8px 30px rgba(0,0,0,0.6)', backdropFilter: 'blur(8px)', zIndex: '9999', display: 'none', }); subtitleSelect.appendChild(_ssDisplay); subtitleSelect.appendChild(_ssList); subtitleSelect._options = []; subtitleSelect._value = ''; subtitleSelect._disabled = false; subtitleSelect.setPlaceholder = text => { _ssLabel.textContent = text || ''; subtitleSelect._options = []; _ssList.innerHTML = ''; subtitleSelect._value = ''; }; subtitleSelect.setOptions = options => { subtitleSelect._options = options || []; _ssList.innerHTML = ''; subtitleSelect._options.forEach(opt => { const item = document.createElement('div'); item.textContent = opt.text; item.dataset.value = String(opt.value); Object.assign(item.style, { padding: '10px 12px', cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.02)', color: '#fff', }); item.addEventListener('click', () => { subtitleSelect.value = item.dataset.value; _ssList.style.display = 'none'; }); item.addEventListener('mouseenter', () => { item.style.background = 'rgba(255,255,255,0.02)'; }); item.addEventListener('mouseleave', () => { item.style.background = 'transparent'; }); _ssList.appendChild(item); }); if (subtitleSelect._options.length > 0) { subtitleSelect.value = String(subtitleSelect._options[0].value); } else { subtitleSelect._value = ''; _ssLabel.textContent = t('noSubtitles'); } }; Object.defineProperty(subtitleSelect, 'value', { get() { return subtitleSelect._value; }, set(v) { subtitleSelect._value = String(v); const found = subtitleSelect._options.find(o => String(o.value) === subtitleSelect._value); _ssLabel.textContent = found ? found.text : ''; }, }); Object.defineProperty(subtitleSelect, 'disabled', { get() { return subtitleSelect._disabled; }, set(v) { subtitleSelect._disabled = !!v; _ssDisplay.style.opacity = subtitleSelect._disabled ? '0.5' : '1'; subtitleSelect.style.pointerEvents = subtitleSelect._disabled ? 'none' : 'auto'; }, }); _ssDisplay.addEventListener('click', () => { if (subtitleSelect._disabled) return; _ssList.style.display = _ssList.style.display === 'none' ? '' : 'none'; }); document.addEventListener('click', e => { if (!subtitleSelect.contains(e.target)) _ssList.style.display = 'none'; }); return subtitleSelect; } const { NotificationManager } = YouTubeUtils; const t = typeof YouTubeUtils !== 'undefined' && typeof YouTubeUtils.t === 'function' ? YouTubeUtils.t : key => key; const logger = typeof YouTubePlusLogger !== 'undefined' && YouTubePlusLogger ? YouTubePlusLogger.createLogger('Download') : { debug: () => {}, info: () => {}, warn: console.warn.bind(console), error: console.error.bind(console), }; const DownloadConfig = { API: { KEY_URL: 'https://cnv.cx/v2/sanity/key', CONVERT_URL: 'https://cnv.cx/v2/converter', }, HEADERS: { 'Content-Type': 'application/json', Origin: 'https://mp3yt.is', Accept: '*/*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', }, VIDEO_QUALITIES: ['144', '240', '360', '480', '720', '1080', '1440', '2160'], AUDIO_BITRATES: ['64', '128', '192', '256', '320'], DEFAULTS: { format: 'video', videoQuality: '1080', audioBitrate: '320', embedThumbnail: true, }, }; function getVideoId() { const params = new URLSearchParams(window.location.search); return params.get('v') || null; } function getVideoUrl() { const videoId = getVideoId(); return videoId ? `https://www.youtube.com/watch?v=${videoId}` : window.location.href; } function getVideoTitle() { try { const titleElement = document.querySelector('h1.ytd-video-primary-info-renderer yt-formatted-string') || document.querySelector('h1.title yt-formatted-string') || document.querySelector('ytd-watch-metadata h1'); return titleElement ? titleElement.textContent.trim() : 'video'; } catch { return 'video'; } } function sanitizeFilename(filename) { return filename .replace(/[<>:"/\\|?*]/g, '') .replace(/\s+/g, ' ') .trim() .substring(0, 200); } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } function createGmRequestOptions(options, resolve, reject) { return { ...options, onload: response => { if (options.onload) options.onload(response); resolve(response); }, onerror: error => { if (options.onerror) options.onerror(error); reject(error); }, ontimeout: () => { if (options.ontimeout) options.ontimeout(); reject(new Error('Request timeout')); }, }; } function buildResponseObject(resp) { return { status: resp.status, statusText: resp.statusText, finalUrl: resp.url, headers: {}, responseText: null, response: null, }; } async function extractResponseText(resp, responseLike) { try { responseLike.responseText = await resp.text(); } catch { responseLike.responseText = null; } } async function extractResponseBlob(resp, responseLike, responseType) { if (responseType === 'blob') { try { responseLike.response = await resp.blob(); } catch { responseLike.response = null; } } } async function executeFetchFallback(options) { const fetchOpts = { method: options.method || 'GET', headers: options.headers || {}, body: options.data || options.body || undefined, }; const resp = await fetch(options.url, fetchOpts); const responseLike = buildResponseObject(resp); await extractResponseText(resp, responseLike); await extractResponseBlob(resp, responseLike, options.responseType); if (options.onload) options.onload(responseLike); return responseLike; } function gmXmlHttpRequest(options) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest(createGmRequestOptions(options, resolve, reject)); return; } (async () => { try { const responseLike = await executeFetchFallback(options); resolve(responseLike); } catch (err) { if (options.onerror) options.onerror(err); reject(err); } })(); }); } async function createSquareAlbumArt(thumbnailUrl) { return new Promise((resolve, reject) => { const img = document.createElement('img'); img.crossOrigin = 'anonymous'; img.onload = () => { const canvas = document.createElement('canvas'); const size = Math.min(img.width, img.height); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('Failed to get canvas context')); return; } const sx = (img.width - size) / 2; const sy = (img.height - size) / 2; ctx.drawImage(img, sx, sy, size, size, 0, 0, size, size); canvas.toBlob( blob => { if (blob) resolve(blob); else reject(new Error('Failed to create blob')); }, 'image/jpeg', 0.95 ); }; img.onerror = () => reject(new Error('Failed to load thumbnail')); img.src = thumbnailUrl; }); } async function embedAlbumArtToMP3(mp3Blob, albumArtBlob, metadata) { try { if (typeof window.ID3Writer === 'undefined') { logger.warn('ID3Writer not available, skipping album art embedding'); return mp3Blob; } const arrayBuffer = await mp3Blob.arrayBuffer(); const writer = new window.ID3Writer(arrayBuffer); if (metadata.title) { writer.setFrame('TIT2', metadata.title); } if (metadata.artist) { writer.setFrame('TPE1', [metadata.artist]); } if (metadata.album) { writer.setFrame('TALB', metadata.album); } if (albumArtBlob) { const coverArrayBuffer = await albumArtBlob.arrayBuffer(); writer.setFrame('APIC', { type: 3, data: coverArrayBuffer, description: 'Cover', }); } writer.addTag(); return new Blob([writer.arrayBuffer], { type: 'audio/mpeg' }); } catch (error) { logger.error('Error embedding album art:', error); return mp3Blob; } } async function fetchPlayerData(videoId) { const response = await gmXmlHttpRequest({ method: 'POST', url: 'https://www.youtube.com/youtubei/v1/player', headers: { 'Content-Type': 'application/json', 'User-Agent': DownloadConfig.HEADERS['User-Agent'], }, data: JSON.stringify({ context: { client: { clientName: 'WEB', clientVersion: '2.20240304.00.00', }, }, videoId, }), }); if (response.status !== 200) { throw new Error(`Failed to get player data: ${response.status}`); } return JSON.parse(response.responseText); } function buildSubtitleUrl(baseUrl) { if (!baseUrl.includes('fmt=')) { return `${baseUrl}&fmt=srv1`; } return baseUrl; } function parseCaptionTracks(captionTracks) { return captionTracks.map(track => ({ name: track.name?.simpleText || track.languageCode, languageCode: track.languageCode, url: buildSubtitleUrl(track.baseUrl), isAutoGenerated: track.kind === 'asr', })); } function parseTranslationLanguages(translationLanguages, baseUrl) { return translationLanguages.map(lang => ({ name: lang.languageName?.simpleText || lang.languageCode, languageCode: lang.languageCode, baseUrl: baseUrl || '', isAutoGenerated: true, })); } function createEmptySubtitleResult(videoId, videoTitle) { return { videoId, videoTitle, subtitles: [], autoTransSubtitles: [], }; } async function getSubtitles(videoId) { try { const data = await fetchPlayerData(videoId); const videoTitle = data.videoDetails?.title || 'video'; const captions = data.captions?.playerCaptionsTracklistRenderer; if (!captions) { return createEmptySubtitleResult(videoId, videoTitle); } const captionTracks = captions.captionTracks || []; const translationLanguages = captions.translationLanguages || []; const baseUrl = captionTracks[0]?.baseUrl || ''; return { videoId, videoTitle, subtitles: parseCaptionTracks(captionTracks), autoTransSubtitles: parseTranslationLanguages(translationLanguages, baseUrl), }; } catch (error) { logger.error('Error getting subtitles:', error); return null; } } function parseSubtitleXML(xml) { const cues = []; const textTagRegex = /]*>([\s\S]*?)<\/text>/gi; let match; while ((match = textTagRegex.exec(xml)) !== null) { const start = parseFloat(match[1] || '0'); const duration = parseFloat(match[2] || '0'); let text = match[3] || ''; text = text.replace(//g, '$1'); text = decodeHTMLEntities(text.trim()); cues.push({ start, duration, text }); } return cues; } function decodeHTMLEntities(text) { const entities = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ''': "'", ' ': ' ', }; let decoded = text; for (const [entity, char] of Object.entries(entities)) { decoded = decoded.replace(new RegExp(entity, 'g'), char); } decoded = decoded.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10))); decoded = decoded.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)) ); return decoded; } function convertToSRT(cues) { let srt = ''; cues.forEach((cue, index) => { const startTime = formatSRTTime(cue.start); const endTime = formatSRTTime(cue.start + cue.duration); const text = cue.text.replace(/\n/g, ' ').trim(); srt += `${index + 1}\n`; srt += `${startTime} --> ${endTime}\n`; srt += `${text}\n\n`; }); return srt; } function formatSRTTime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); const milliseconds = Math.floor((seconds % 1) * 1000); return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')},${String(milliseconds).padStart(3, '0')}`; } function convertToTXT(cues) { return cues.map(cue => cue.text.trim()).join('\n'); } async function downloadSubtitle(options = {}) { const { videoId, url: baseUrl, languageCode, languageName, format = 'srt', translateTo = null, } = options; if (!videoId || !baseUrl) { throw new Error('Video ID and URL are required'); } const title = getVideoTitle(); let subtitleUrl = baseUrl; if (!subtitleUrl.includes('fmt=')) { subtitleUrl += '&fmt=srv1'; } if (translateTo) { subtitleUrl += `&tlang=${translateTo}`; } NotificationManager.show(t('subtitleDownloading'), { duration: 2000, type: 'info', }); try { const response = await gmXmlHttpRequest({ method: 'GET', url: subtitleUrl, headers: { 'User-Agent': DownloadConfig.HEADERS['User-Agent'], Referer: 'https://www.youtube.com/', }, }); if (response.status !== 200) { throw new Error(`Failed to download subtitle: ${response.status}`); } const xmlText = response.responseText; if (!xmlText || xmlText.length === 0) { throw new Error('Empty subtitle response'); } let content; let extension; if (format === 'xml') { content = xmlText; extension = 'xml'; } else { const cues = parseSubtitleXML(xmlText); if (cues.length === 0) { throw new Error('No subtitle cues found'); } if (format === 'srt') { content = convertToSRT(cues); extension = 'srt'; } else if (format === 'txt') { content = convertToTXT(cues); extension = 'txt'; } else { content = xmlText; extension = 'xml'; } } const langSuffix = translateTo ? `${languageCode}-${translateTo}` : languageCode; const filename = sanitizeFilename(`${title} - ${languageName} (${langSuffix}).${extension}`); const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); NotificationManager.show(t('subtitleDownloaded'), { duration: 3000, type: 'success', }); logger.debug('Subtitle downloaded:', filename); } catch (error) { logger.error('Error downloading subtitle:', error); NotificationManager.show(`${t('subtitleDownloadFailed')} ${error.message}`, { duration: 5000, type: 'error', }); throw error; } } async function downloadVideo(options = {}) { const { format = DownloadConfig.DEFAULTS.format, quality = DownloadConfig.DEFAULTS.videoQuality, audioBitrate = DownloadConfig.DEFAULTS.audioBitrate, embedThumbnail = DownloadConfig.DEFAULTS.embedThumbnail, onProgress = null, } = options; const videoId = getVideoId(); if (!videoId) { throw new Error('Video ID not found'); } const videoUrl = getVideoUrl(); const title = getVideoTitle(); NotificationManager.show(t('startingDownload'), { duration: 2000, type: 'info', }); try { logger.debug('Fetching API key...'); const keyResponse = await gmXmlHttpRequest({ method: 'GET', url: DownloadConfig.API.KEY_URL, headers: DownloadConfig.HEADERS, }); if (keyResponse.status !== 200) { throw new Error(`Failed to get API key: ${keyResponse.status}`); } const keyData = JSON.parse(keyResponse.responseText); if (!keyData || !keyData.key) { throw new Error('API key not found in response'); } const { key } = keyData; logger.debug('API key obtained'); let payload; if (format === 'video') { const codec = parseInt(quality, 10) > 1080 ? 'vp9' : 'h264'; payload = { link: videoUrl, format: 'mp4', audioBitrate: '128', videoQuality: quality, filenameStyle: 'pretty', vCodec: codec, }; } else { payload = { link: videoUrl, format: 'mp3', audioBitrate, filenameStyle: 'pretty', }; } logger.debug('Requesting conversion...', payload); const customHeaders = { ...DownloadConfig.HEADERS, key, }; const downloadResponse = await gmXmlHttpRequest({ method: 'POST', url: DownloadConfig.API.CONVERT_URL, headers: customHeaders, data: JSON.stringify(payload), }); if (downloadResponse.status !== 200) { throw new Error(`Conversion failed: ${downloadResponse.status}`); } const apiDownloadInfo = JSON.parse(downloadResponse.responseText); logger.debug('Conversion response:', apiDownloadInfo); if (!apiDownloadInfo.url) { throw new Error('No download URL received from API'); } logger.debug('Downloading file from:', apiDownloadInfo.url); return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest === 'undefined') { logger.warn('GM_xmlhttpRequest not available, opening in new tab'); window.open(apiDownloadInfo.url, '_blank'); resolve(); return; } GM_xmlhttpRequest({ method: 'GET', url: apiDownloadInfo.url, responseType: 'blob', headers: { 'User-Agent': DownloadConfig.HEADERS['User-Agent'], Referer: 'https://mp3yt.is/', Accept: '*/*', }, onprogress: progress => { if (onProgress) { onProgress({ loaded: progress.loaded, total: progress.total, percent: progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0, }); } }, onload: async response => { if (response.status === 200 && response.response) { let blob = response.response; if (blob.size === 0) { reject(new Error(t('zeroBytesError'))); return; } console.log(`[Download] File downloaded: ${formatBytes(blob.size)}`); if (format === 'audio' && embedThumbnail) { try { logger.debug('Embedding album art...'); const thumbnailUrl = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`; const albumArt = await createSquareAlbumArt(thumbnailUrl); blob = await embedAlbumArtToMP3(blob, albumArt, { title }); logger.debug('Album art embedded successfully'); } catch (error) { logger.error('Failed to embed album art:', error); } } const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; const filename = apiDownloadInfo.filename || `${title}.${format === 'video' ? 'mp4' : 'mp3'}`; a.download = sanitizeFilename(filename); document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(blobUrl), 100); NotificationManager.show(t('downloadCompleted'), { duration: 3000, type: 'success', }); logger.debug('Download completed:', filename); resolve(); } else { reject(new Error(`Download failed: ${response.status}`)); } }, onerror: () => reject(new Error('Download failed - network error')), ontimeout: () => reject(new Error('Download timeout')), }); }); } catch (error) { logger.error('Error:', error); NotificationManager.show(`${t('downloadFailed')} ${error.message}`, { duration: 5000, type: 'error', }); throw error; } } let _modalElements = null; function createTabButtons(onTabChange) { const tabContainer = document.createElement('div'); Object.assign(tabContainer.style, { display: 'flex', gap: '8px', padding: '12px', justifyContent: 'center', alignItems: 'center', background: 'transparent', }); const videoTab = document.createElement('button'); videoTab.textContent = t('videoTab'); videoTab.dataset.format = 'video'; const audioTab = document.createElement('button'); audioTab.textContent = t('audioTab'); audioTab.dataset.format = 'audio'; const subTab = document.createElement('button'); subTab.textContent = t('subtitleTab'); subTab.dataset.format = 'subtitle'; [videoTab, audioTab, subTab].forEach(btn => { Object.assign(btn.style, { flex: 'initial', padding: '8px 18px', border: '1px solid rgba(255,255,255,0.06)', background: 'transparent', cursor: 'pointer', fontSize: '13px', fontWeight: '600', transition: 'all 0.18s ease', color: '#666', borderRadius: '999px', }); btn.type = 'button'; btn.style.outline = 'none'; btn.style.userSelect = 'none'; btn.setAttribute('aria-pressed', 'false'); }); function setActive(btn) { [videoTab, audioTab, subTab].forEach(b => { b.style.background = 'transparent'; b.style.color = '#666'; b.style.border = '1px solid rgba(255,255,255,0.06)'; b.style.boxShadow = 'none'; b.setAttribute('aria-pressed', 'false'); }); Object.assign(btn.style, { background: '#10c56a', color: '#fff', border: '1px solid rgba(0,0,0,0.06)', boxShadow: '0 1px 0 rgba(0,0,0,0.04) inset', }); btn.setAttribute('aria-pressed', 'true'); try { onTabChange(btn.dataset.format); } catch { } } [videoTab, audioTab, subTab].forEach(btn => { btn.addEventListener('click', () => { setActive(btn); try { btn.blur(); } catch { } }); }); tabContainer.appendChild(videoTab); tabContainer.appendChild(audioTab); tabContainer.appendChild(subTab); setTimeout(() => setActive(videoTab), 0); return tabContainer; } function buildModalForm() { const qualitySelect = document.createElement('div'); qualitySelect.role = 'radiogroup'; qualitySelect.value = DownloadConfig.DEFAULTS.videoQuality; Object.assign(qualitySelect.style, { display: 'flex', flexWrap: 'wrap', gap: '10px', padding: '12px 6px', borderRadius: '10px', width: '100%', alignItems: 'center', justifyContent: 'center', background: 'transparent', }); const embedCheckbox = document.createElement('input'); embedCheckbox.type = 'checkbox'; embedCheckbox.checked = DownloadConfig.DEFAULTS.embedThumbnail; const embedLabel = document.createElement('label'); embedLabel.style.fontSize = '13px'; embedLabel.style.display = 'flex'; embedLabel.style.alignItems = 'center'; embedLabel.style.gap = '6px'; embedLabel.style.color = '#fff'; embedLabel.style.display = 'none'; embedLabel.appendChild(embedCheckbox); embedLabel.appendChild(document.createTextNode(t('embedThumbnail'))); const subtitleWrapper = document.createElement('div'); subtitleWrapper.style.display = 'none'; const subtitleSelect = createSubtitleSelect(); const formatSelect = document.createElement('div'); formatSelect.role = 'radiogroup'; formatSelect.value = 'srt'; Object.assign(formatSelect.style, { display: 'flex', gap: '8px', padding: '6px 0', borderRadius: '6px', width: '100%', alignItems: 'center', justifyContent: 'center', background: 'transparent', }); ['srt', 'txt', 'xml'].forEach(fmt => { const btn = document.createElement('button'); btn.type = 'button'; btn.dataset.value = fmt; btn.textContent = fmt.toUpperCase(); Object.assign(btn.style, { padding: '6px 12px', borderRadius: '999px', border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.02)', color: '#fff', cursor: 'pointer', fontSize: '13px', fontWeight: '600', }); btn.addEventListener('click', () => { Array.from(formatSelect.children).forEach(c => { c.style.background = 'transparent'; c.style.color = '#fff'; c.style.border = '1px solid rgba(255,255,255,0.08)'; }); btn.style.background = '#111'; btn.style.color = '#10c56a'; btn.style.border = '1px solid rgba(16,197,106,0.15)'; formatSelect.value = fmt; }); formatSelect.appendChild(btn); }); const _defaultFmtBtn = Array.from(formatSelect.children).find( c => c.dataset.value === formatSelect.value ); if (_defaultFmtBtn) _defaultFmtBtn.click(); subtitleWrapper.appendChild(subtitleSelect); subtitleWrapper.appendChild(formatSelect); const cancelBtn = document.createElement('button'); cancelBtn.textContent = t('cancel'); Object.assign(cancelBtn.style, { padding: '8px 16px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.12)', background: 'transparent', cursor: 'pointer', fontSize: '14px', color: '#fff', }); const downloadBtn = document.createElement('button'); downloadBtn.textContent = t('download'); Object.assign(downloadBtn.style, { padding: '8px 20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.12)', background: 'transparent', color: '#fff', cursor: 'pointer', fontSize: '14px', fontWeight: '600', }); const progressWrapper = document.createElement('div'); progressWrapper.style.display = 'none'; progressWrapper.style.marginTop = '12px'; const progressBar = document.createElement('div'); Object.assign(progressBar.style, { width: '100%', height: '3px', background: '#e0e0e0', borderRadius: '5px', overflow: 'hidden', marginBottom: '6px', }); const progressFill = document.createElement('div'); Object.assign(progressFill.style, { width: '0%', height: '100%', background: '#1a73e8', transition: 'width 200ms linear', }); progressBar.appendChild(progressFill); const progressText = document.createElement('div'); progressText.style.fontSize = '12px'; progressText.style.color = '#666'; progressWrapper.appendChild(progressBar); progressWrapper.appendChild(progressText); return { qualitySelect, embedLabel, subtitleWrapper, subtitleSelect, formatSelect, cancelBtn, downloadBtn, progressWrapper, progressFill, progressText, }; } function disableFormControls(formParts) { formParts.qualitySelect.disabled = true; formParts.downloadBtn.disabled = true; formParts.cancelBtn.disabled = true; } function enableFormControls(formParts) { formParts.qualitySelect.disabled = false; formParts.downloadBtn.disabled = false; formParts.cancelBtn.disabled = false; } function initializeProgress(formParts) { formParts.progressWrapper.style.display = ''; formParts.progressFill.style.width = '0%'; formParts.progressText.textContent = t('starting'); } async function handleSubtitleDownload(formParts, getSubtitlesData) { const subtitlesData = getSubtitlesData(); const selectedIndex = parseInt(formParts.subtitleSelect.value, 10); const subtitle = subtitlesData.all[selectedIndex]; const subtitleFormat = formParts.formatSelect.value; if (!subtitle) { throw new Error(t('noSubtitleSelected')); } const videoId = getVideoId(); await downloadSubtitle({ videoId, url: subtitle.url, languageCode: subtitle.languageCode, languageName: subtitle.name, format: subtitleFormat, translateTo: subtitle.translateTo || null, }); } async function handleMediaDownload(formParts, format) { const opts = { format, quality: formParts.qualitySelect.value, audioBitrate: formParts.qualitySelect.value, embedThumbnail: format === 'audio', onProgress: p => { formParts.progressFill.style.width = `${p.percent || 0}%`; formParts.progressText.textContent = `${p.percent || 0}% � ${formatBytes(p.loaded || 0)} / ${p.total ? formatBytes(p.total) : '�'}`; }, }; await downloadVideo(opts); } function completeDownload(formParts) { formParts.progressText.textContent = t('completed'); setTimeout(() => closeModal(), 800); } function handleDownloadError(formParts, err) { formParts.progressText.textContent = `${t('downloadFailed')} ${err?.message || 'error'}`; enableFormControls(formParts); } function wireModalEvents(formParts, activeFormatGetter, getSubtitlesData) { formParts.cancelBtn.addEventListener('click', () => closeModal()); formParts.downloadBtn.addEventListener('click', async () => { disableFormControls(formParts); initializeProgress(formParts); const format = activeFormatGetter(); try { if (format === 'subtitle') { await handleSubtitleDownload(formParts, getSubtitlesData); } else { await handleMediaDownload(formParts, format); } completeDownload(formParts); } catch (err) { handleDownloadError(formParts, err); } }); } async function loadSubtitlesForForm(formParts, subtitlesData) { const videoId = getVideoId(); if (!videoId) return; formParts.subtitleSelect.setPlaceholder(t('loading')); formParts.subtitleSelect.disabled = true; try { const data = await getSubtitles(videoId); if (!data) { formParts.subtitleSelect.setPlaceholder(t('noSubtitles')); return; } subtitlesData.original = data.subtitles; subtitlesData.translated = data.autoTransSubtitles.map(autot => ({ ...autot, url: data.subtitles[0]?.url || '', translateTo: autot.languageCode, })); subtitlesData.all = [...subtitlesData.original, ...subtitlesData.translated]; if (subtitlesData.all.length === 0) { formParts.subtitleSelect.setPlaceholder(t('noSubtitles')); return; } const opts = subtitlesData.all.map((sub, idx) => ({ value: idx, text: sub.name + (sub.translateTo ? t('autoTranslateSuffix') : ''), })); formParts.subtitleSelect.setOptions(opts); formParts.subtitleSelect.disabled = false; } catch (err) { logger.error('Failed to load subtitles:', err); formParts.subtitleSelect.setPlaceholder(t('subtitleLoadError')); } } function updateQualityOptionsForForm(formParts, activeFormat, subtitlesData) { if (activeFormat === 'subtitle') { formParts.qualitySelect.style.display = 'none'; formParts.embedLabel.style.display = 'none'; formParts.subtitleWrapper.style.display = 'block'; loadSubtitlesForForm(formParts, subtitlesData); return; } if (activeFormat === 'video') { formParts.qualitySelect.style.display = 'flex'; formParts.embedLabel.style.display = 'none'; formParts.subtitleWrapper.style.display = 'none'; formParts.qualitySelect.innerHTML = ''; const lowQuals = DownloadConfig.VIDEO_QUALITIES.filter(q => parseInt(q, 10) <= 1080); const highQuals = DownloadConfig.VIDEO_QUALITIES.filter(q => parseInt(q, 10) > 1080); function makeQualityButton(q) { const btn = document.createElement('button'); btn.type = 'button'; btn.dataset.value = q; btn.textContent = `${q}p`; Object.assign(btn.style, { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '8px 12px', borderRadius: '999px', border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.02)', color: '#fff', cursor: 'pointer', fontSize: '13px', fontWeight: '600', }); btn.addEventListener('click', () => { Array.from(formParts.qualitySelect.children).forEach(c => { if (c.dataset && c.dataset.value) { c.style.background = 'transparent'; c.style.color = '#fff'; c.style.border = '1px solid rgba(255,255,255,0.08)'; } }); btn.style.background = '#111'; btn.style.color = '#10c56a'; btn.style.border = '1px solid rgba(16,197,106,0.15)'; formParts.qualitySelect.value = q; }); return btn; } lowQuals.forEach(q => formParts.qualitySelect.appendChild(makeQualityButton(q))); if (highQuals.length > 0) { const labelWrap = document.createElement('div'); Object.assign(labelWrap.style, { display: 'flex', alignItems: 'center', gap: '12px', width: '100%', margin: '8px 0', }); const lineLeft = document.createElement('div'); lineLeft.style.flex = '1'; lineLeft.style.borderTop = '1px solid rgba(255,255,255,0.06)'; const label = document.createElement('div'); label.textContent = t('vp9Label'); Object.assign(label.style, { fontSize: '12px', color: 'rgba(255,255,255,0.7)', padding: '0 8px', }); const lineRight = document.createElement('div'); lineRight.style.flex = '1'; lineRight.style.borderTop = '1px solid rgba(255,255,255,0.06)'; labelWrap.appendChild(lineLeft); labelWrap.appendChild(label); labelWrap.appendChild(lineRight); formParts.qualitySelect.appendChild(labelWrap); highQuals.forEach(q => formParts.qualitySelect.appendChild(makeQualityButton(q))); } formParts.qualitySelect.value = DownloadConfig.DEFAULTS.videoQuality; const defaultBtn = Array.from(formParts.qualitySelect.children).find( c => c.dataset && c.dataset.value === formParts.qualitySelect.value ); if (defaultBtn) defaultBtn.click(); return; } formParts.qualitySelect.style.display = 'flex'; formParts.embedLabel.style.display = 'flex'; formParts.subtitleWrapper.style.display = 'none'; formParts.qualitySelect.innerHTML = ''; DownloadConfig.AUDIO_BITRATES.forEach(b => { const btn = document.createElement('button'); btn.type = 'button'; btn.dataset.value = b; btn.textContent = `${b} kbps`; Object.assign(btn.style, { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '8px 12px', borderRadius: '999px', border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.02)', color: '#fff', cursor: 'pointer', fontSize: '13px', fontWeight: '600', }); btn.addEventListener('click', () => { Array.from(formParts.qualitySelect.children).forEach(c => { c.style.background = 'transparent'; c.style.color = '#fff'; c.style.border = '1px solid rgba(255,255,255,0.08)'; }); btn.style.background = '#111'; btn.style.color = '#10c56a'; btn.style.border = '1px solid rgba(16,197,106,0.15)'; formParts.qualitySelect.value = b; }); formParts.qualitySelect.appendChild(btn); }); formParts.qualitySelect.value = DownloadConfig.DEFAULTS.audioBitrate; const defaultAudioBtn = Array.from(formParts.qualitySelect.children).find( c => c.dataset.value === formParts.qualitySelect.value ); if (defaultAudioBtn) defaultAudioBtn.click(); formParts.embedLabel.style.display = 'none'; } function createModalUI() { if (_modalElements) return _modalElements; let activeFormat = 'video'; const subtitlesData = { all: [], original: [], translated: [] }; const overlay = document.createElement('div'); Object.assign(overlay.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: '999999', }); const box = document.createElement('div'); Object.assign(box.style, { width: '420px', maxWidth: '94%', background: 'rgba(20,20,20,0.64)', color: '#fff', borderRadius: '12px', boxShadow: '0 8px 40px rgba(0,0,0,0.6)', fontFamily: 'Arial, sans-serif', border: '1px solid rgba(255,255,255,0.06)', backdropFilter: 'blur(8px)', }); const formParts = buildModalForm(); const tabContainer = createTabButtons(format => { activeFormat = format; updateQualityOptionsForForm(formParts, activeFormat, subtitlesData); }); const content = document.createElement('div'); content.style.padding = '16px'; content.appendChild(formParts.qualitySelect); content.appendChild(formParts.embedLabel); content.appendChild(formParts.subtitleWrapper); content.appendChild(formParts.progressWrapper); const btnRow = document.createElement('div'); Object.assign(btnRow.style, { display: 'flex', gap: '8px', padding: '16px', justifyContent: 'center', }); btnRow.appendChild(formParts.cancelBtn); btnRow.appendChild(formParts.downloadBtn); box.appendChild(tabContainer); box.appendChild(content); box.appendChild(btnRow); overlay.appendChild(box); updateQualityOptionsForForm(formParts, activeFormat, subtitlesData); wireModalEvents( formParts, () => activeFormat, () => subtitlesData ); _modalElements = { overlay, box, ...formParts }; return _modalElements; } function openModal() { const els = createModalUI(); if (!els) return; try { if (!document.body.contains(els.overlay)) document.body.appendChild(els.overlay); } catch { } } function closeModal() { if (!_modalElements) return; try { if (_modalElements.overlay && _modalElements.overlay.parentNode) { _modalElements.overlay.parentNode.removeChild(_modalElements.overlay); } } catch { } _modalElements = null; } function init() { console.log('[YouTube+ Download] API module loaded'); console.log('[YouTube+ Download] Use window.YouTubePlusDownload.downloadVideo() to download'); } if (typeof window !== 'undefined') { window.YouTubePlusDownload = { downloadVideo, getSubtitles, downloadSubtitle, getVideoId, getVideoUrl, getVideoTitle, sanitizeFilename, formatBytes, DownloadConfig, openModal, init, }; } if (typeof window !== 'undefined') { window.YouTubeDownload = { init, openModal, getVideoId, getVideoTitle, version: '2.2', }; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); (function () { 'use strict'; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const _getLanguage = () => { try { if (_globalI18n && typeof _globalI18n.getLanguage === 'function') { return _globalI18n.getLanguage(); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.getLanguage === 'function' ) { return window.YouTubeUtils.getLanguage(); } } catch { } const htmlLang = document.documentElement.lang || 'en'; return htmlLang.startsWith('ru') ? 'ru' : 'en'; }; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') { return _globalI18n.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const config = { enabled: true, storageKey: 'youtube_top_button_settings', }; const addStyles = () => { if (document.getElementById('custom-styles')) return; const style = document.createElement('style'); style.id = 'custom-styles'; style.textContent = ` :root{--scrollbar-width:8px;--scrollbar-track:transparent;--scrollbar-thumb:rgba(144,144,144,.5);--scrollbar-thumb-hover:rgba(170,170,170,.7);--scrollbar-thumb-active:rgba(190,190,190,.9);} ::-webkit-scrollbar{width:var(--scrollbar-width)!important;height:var(--scrollbar-width)!important;} ::-webkit-scrollbar-track{background:var(--scrollbar-track)!important;border-radius:4px!important;} ::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb)!important;border-radius:4px!important;transition:background .2s!important;} ::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)!important;} ::-webkit-scrollbar-thumb:active{background:var(--scrollbar-thumb-active)!important;} ::-webkit-scrollbar-corner{background:transparent!important;} *{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);} html[dark]{--scrollbar-thumb:rgba(144,144,144,.4);--scrollbar-thumb-hover:rgba(170,170,170,.6);--scrollbar-thumb-active:rgba(190,190,190,.8);} .top-button{position:fixed;bottom:16px;right:16px;width:40px;height:40px;background:var(--yt-top-btn-bg,rgba(0,0,0,.7));color:var(--yt-top-btn-color,#fff);border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2100;opacity:0;visibility:hidden;transition:all .3s;backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);border:1px solid var(--yt-top-btn-border,rgba(255,255,255,.1));background:rgba(255,255,255,.12);box-shadow:0 8px 32px 0 rgba(31,38,135,.18);} .top-button:hover{background:var(--yt-top-btn-hover,rgba(0,0,0,.15));transform:translateY(-2px) scale(1.07);box-shadow:0 8px 32px rgba(0,0,0,.25);} .top-button.visible{opacity:1;visibility:visible;} .top-button svg{transition:transform .2s;} .top-button:hover svg{transform:translateY(-1px) scale(1.1);} html[dark]{--yt-top-btn-bg:rgba(255,255,255,.10);--yt-top-btn-color:#fff;--yt-top-btn-border:rgba(255,255,255,.18);--yt-top-btn-hover:rgba(255,255,255,.18);} html:not([dark]){--yt-top-btn-bg:rgba(255,255,255,.12);--yt-top-btn-color:#222;--yt-top-btn-border:rgba(0,0,0,.08);--yt-top-btn-hover:rgba(255,255,255,.18);} #right-tabs .top-button{position:absolute;z-index:1000;} ytd-watch-flexy:not([tyt-tab^="#"]) #right-tabs .top-button{display:none;} ytd-playlist-panel-renderer .top-button{position:absolute;z-index:1000;} ytd-watch-flexy[flexy] #movie_player, ytd-watch-flexy[flexy] #movie_player .html5-video-container, ytd-watch-flexy[flexy] .html5-main-video{width:100%!important; max-width:100%!important;} ytd-watch-flexy[flexy] .html5-main-video{height:auto!important; max-height:100%!important; object-fit:contain!important; transform:none!important;} ytd-watch-flexy[flexy] #player-container-outer, ytd-watch-flexy[flexy] #movie_player{display:flex!important; align-items:center!important; justify-content:center!important;} `; (document.head || document.documentElement).appendChild(style); }; const handleScroll = (scrollContainer, button) => { try { if (!button || !scrollContainer) return; button.classList.toggle('visible', scrollContainer.scrollTop > 100); } catch (error) { console.error('[YouTube+][Enhanced] Error in handleScroll:', error); } }; const setupScrollListener = () => { try { document.querySelectorAll('.tab-content-cld').forEach(tab => { tab.removeEventListener('scroll', tab._topButtonScrollHandler); }); const activeTab = document.querySelector( '#right-tabs .tab-content-cld:not(.tab-content-hidden)' ); const button = document.getElementById('right-tabs-top-button'); if (activeTab && button) { const debounceFunc = typeof YouTubeUtils !== 'undefined' && YouTubeUtils.debounce ? YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounceFunc(() => handleScroll(activeTab, button), 100); activeTab._topButtonScrollHandler = scrollHandler; activeTab.addEventListener('scroll', scrollHandler, { passive: true }); handleScroll(activeTab, button); } } catch (error) { console.error('[YouTube+][Enhanced] Error in setupScrollListener:', error); } }; const createButton = () => { try { const rightTabs = document.querySelector('#right-tabs'); if (!rightTabs || document.getElementById('right-tabs-top-button')) return; if (!config.enabled) return; const button = document.createElement('button'); button.id = 'right-tabs-top-button'; button.className = 'top-button'; button.title = t('scrollToTop'); button.setAttribute('aria-label', t('scrollToTop')); button.innerHTML = ''; button.addEventListener('click', () => { try { const activeTab = document.querySelector( '#right-tabs .tab-content-cld:not(.tab-content-hidden)' ); if (activeTab) activeTab.scrollTo({ top: 0, behavior: 'smooth' }); } catch (error) { console.error('[YouTube+][Enhanced] Error scrolling to top:', error); } }); rightTabs.style.position = 'relative'; rightTabs.appendChild(button); setupScrollListener(); } catch (error) { console.error('[YouTube+][Enhanced] Error creating button:', error); } }; const createUniversalButton = () => { try { if (document.getElementById('universal-top-button')) return; if (!config.enabled) return; const button = document.createElement('button'); button.id = 'universal-top-button'; button.className = 'top-button'; button.title = t('scrollToTop'); button.setAttribute('aria-label', t('scrollToTop')); button.innerHTML = ''; button.addEventListener('click', () => { try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (error) { console.error('[YouTube+][Enhanced] Error scrolling to top:', error); } }); document.body.appendChild(button); const debounceFunc = typeof YouTubeUtils !== 'undefined' && YouTubeUtils.debounce ? YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounceFunc(() => { button.classList.toggle('visible', window.scrollY > 100); }, 100); window.addEventListener('scroll', scrollHandler, { passive: true }); button.classList.toggle('visible', window.scrollY > 100); } catch (error) { console.error('[YouTube+][Enhanced] Error creating universal button:', error); } }; const createPlaylistPanelButton = () => { try { const playlistPanel = document.querySelector('ytd-playlist-panel-renderer'); if (!playlistPanel || document.getElementById('playlist-panel-top-button')) return; if (!config.enabled) return; const button = document.createElement('button'); button.id = 'playlist-panel-top-button'; button.className = 'top-button'; button.title = t('scrollToTop'); button.setAttribute('aria-label', t('scrollToTop')); button.innerHTML = ''; const scrollContainer = playlistPanel.querySelector('#items'); if (!scrollContainer) return; button.addEventListener('click', () => { try { scrollContainer.scrollTo({ top: 0, behavior: 'smooth' }); } catch (error) { console.error('[YouTube+][Enhanced] Error scrolling to top:', error); } }); playlistPanel.style.position = playlistPanel.style.position || 'relative'; button.style.position = 'absolute'; button.style.bottom = '16px'; button.style.right = '16px'; button.style.zIndex = '1000'; playlistPanel.appendChild(button); const debounceFunc = typeof YouTubeUtils !== 'undefined' && YouTubeUtils.debounce ? YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounceFunc(() => handleScroll(scrollContainer, button), 100); scrollContainer.addEventListener('scroll', scrollHandler, { passive: true }); handleScroll(scrollContainer, button); } catch (error) { console.error('[YouTube+][Enhanced] Error creating playlist panel button:', error); } }; const RETURN_DISLIKE_API = 'https://returnyoutubedislikeapi.com/votes'; const DISLIKE_CACHE_TTL = 10 * 60 * 1000; const dislikeCache = new Map(); let dislikeObserver = null; let dislikePollTimer = null; const formatCompactNumber = number => { try { return new Intl.NumberFormat(_getLanguage() || 'en', { notation: 'compact', compactDisplay: 'short', }).format(Number(number) || 0); } catch { return String(number || 0); } }; const fetchDislikes = async videoId => { if (!videoId) return 0; const cached = dislikeCache.get(videoId); if (cached && Date.now() < cached.expiresAt) return cached.value; try { if (typeof GM_xmlhttpRequest !== 'undefined') { const text = await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => reject(new Error('timeout')), 8000); GM_xmlhttpRequest({ method: 'GET', url: `${RETURN_DISLIKE_API}?videoId=${encodeURIComponent(videoId)}`, timeout: 8000, headers: { Accept: 'application/json' }, onload: r => { clearTimeout(timeoutId); if (r.status >= 200 && r.status < 300) resolve(r.responseText); else reject(new Error(`HTTP ${r.status}`)); }, onerror: e => { clearTimeout(timeoutId); reject(e || new Error('network')); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error('timeout')); }, }); }); const parsed = JSON.parse(text || '{}'); const val = Number(parsed.dislikes || 0) || 0; dislikeCache.set(videoId, { value: val, expiresAt: Date.now() + DISLIKE_CACHE_TTL }); return val; } const controller = new AbortController(); const id = setTimeout(() => controller.abort(), 8000); try { const resp = await fetch(`${RETURN_DISLIKE_API}?videoId=${encodeURIComponent(videoId)}`, { method: 'GET', cache: 'no-cache', signal: controller.signal, headers: { Accept: 'application/json' }, }); clearTimeout(id); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const json = await resp.json(); const val = Number(json.dislikes || 0) || 0; dislikeCache.set(videoId, { value: val, expiresAt: Date.now() + DISLIKE_CACHE_TTL }); return val; } finally { clearTimeout(id); } } catch { return 0; } }; const getVideoIdForDislike = () => { try { const urlObj = new URL(window.location.href); const pathname = urlObj.pathname || ''; if (pathname.startsWith('/shorts/')) return pathname.slice(8); if (pathname.startsWith('/clip/')) { const meta = document.querySelector( "meta[itemprop='videoId'], meta[itemprop='identifier']" ); return meta?.getAttribute('content') || null; } return urlObj.searchParams.get('v'); } catch { return null; } }; const getButtonsContainer = () => { return ( document.querySelector( 'ytd-menu-renderer.ytd-watch-metadata > div#top-level-buttons-computed' ) || document.querySelector('ytd-menu-renderer.ytd-video-primary-info-renderer > div') || document.querySelector('#menu-container #top-level-buttons-computed') || null ); }; const getDislikeButtonShorts = () => { const activeReel = document.querySelector('ytd-reel-video-renderer[is-active]'); if (activeReel) { return ( activeReel.querySelector('dislike-button-view-model') || activeReel.querySelector('#dislike-button') || null ); } return ( document.querySelector('dislike-button-view-model') || document.querySelector('#dislike-button') || null ); }; const getDislikeButtonFromContainer = buttons => { if (!buttons) return null; const segmented = buttons.querySelector('ytd-segmented-like-dislike-button-renderer'); if (segmented) { return segmented.children[1] || document.querySelector('#segmented-dislike-button') || null; } const viewModel = buttons.querySelector('dislike-button-view-model'); if (viewModel) return viewModel; return buttons.children && buttons.children[1] ? buttons.children[1] : null; }; const getDislikeButton = () => { const isShorts = window.location.pathname.startsWith('/shorts'); if (isShorts) { return getDislikeButtonShorts(); } const buttons = getButtonsContainer(); return getDislikeButtonFromContainer(buttons); }; const getOrCreateDislikeText = dislikeButton => { if (!dislikeButton) return null; const textSpan = dislikeButton.querySelector('span.yt-core-attributed-string') || dislikeButton.querySelector('#text') || dislikeButton.querySelector('yt-formatted-string') || dislikeButton.querySelector('span[role="text"]'); if (textSpan) return textSpan; const created = document.createElement('span'); created.id = 'ytp-plus-dislike-text'; created.setAttribute('role', 'text'); created.style.marginLeft = '6px'; const btn = dislikeButton.querySelector('button') || dislikeButton; try { btn.appendChild(created); btn.style.minWidth = 'auto'; } catch {} return created; }; const setDislikeDisplay = (dislikeButton, count) => { try { const container = getOrCreateDislikeText(dislikeButton); if (!container) return; const formatted = formatCompactNumber(count); if (container.innerText !== String(formatted)) container.innerText = String(formatted); } catch { } }; const setupDislikeObserver = dislikeButton => { if (!dislikeButton) return; if (dislikeObserver) { dislikeObserver.disconnect(); dislikeObserver = null; } dislikeObserver = new MutationObserver(() => { const vid = getVideoIdForDislike(); const cached = dislikeCache.get(vid); if (cached) setDislikeDisplay(dislikeButton, cached.value); }); try { dislikeObserver.observe(dislikeButton, { childList: true, subtree: true, attributes: true }); } catch {} }; const initReturnDislike = () => { try { if (dislikePollTimer) return; let attempts = 0; const maxAttempts = window.location.pathname.startsWith('/shorts') ? 100 : 50; const interval = window.location.pathname.startsWith('/shorts') ? 100 : 200; dislikePollTimer = setInterval(async () => { attempts++; const btn = getDislikeButton(); if (btn || attempts >= maxAttempts) { clearInterval(dislikePollTimer); dislikePollTimer = null; if (btn) { const vid = getVideoIdForDislike(); const val = await fetchDislikes(vid); setDislikeDisplay(btn, val); setupDislikeObserver(btn); } } }, interval); } catch { } }; const cleanupReturnDislike = () => { try { if (dislikePollTimer) { clearInterval(dislikePollTimer); dislikePollTimer = null; } if (dislikeObserver) { dislikeObserver.disconnect(); dislikeObserver = null; } const created = document.getElementById('ytp-plus-dislike-text'); if (created && created.parentNode) created.parentNode.removeChild(created); } catch {} }; const observeTabChanges = () => { try { const observer = new MutationObserver(mutations => { try { if ( mutations.some( m => m.type === 'attributes' && m.attributeName === 'class' && m.target instanceof Element && m.target.classList.contains('tab-content-cld') ) ) { setTimeout(setupScrollListener, 100); } } catch (error) { console.error('[YouTube+][Enhanced] Error in mutation observer:', error); } }); const rightTabs = document.querySelector('#right-tabs'); if (rightTabs) { observer.observe(rightTabs, { attributes: true, subtree: true, attributeFilter: ['class'], }); return observer; } return null; } catch (error) { console.error('[YouTube+][Enhanced] Error in observeTabChanges:', error); return null; } }; const needsUniversalButton = () => { const path = window.location.pathname; const { search } = window.location; if (path === '/results' && search.includes('search_query=')) return true; if (path === '/playlist' && search.includes('list=')) return true; if (path === '/' || path === '/feed/subscriptions') return true; return false; }; const handleTabButtonClick = e => { try { const { target } = (e); const tabButton = target?.closest?.('.tab-btn[tyt-tab-content]'); if (tabButton) { setTimeout(setupScrollListener, 100); } } catch (error) { console.error('[YouTube+][Enhanced] Error in click handler:', error); } }; const setupEvents = () => { try { document.addEventListener('click', handleTabButtonClick, true); } catch (error) { console.error('[YouTube+][Enhanced] Error in setupEvents:', error); } }; const init = () => { try { addStyles(); setupEvents(); const checkForTabs = () => { try { if (document.querySelector('#right-tabs')) { createButton(); observeTabChanges(); } else { setTimeout(checkForTabs, 500); } } catch (error) { console.error('[YouTube+][Enhanced] Error checking for tabs:', error); } }; const checkForPlaylistPanel = () => { try { const playlistPanel = document.querySelector('ytd-playlist-panel-renderer'); if (playlistPanel && !document.getElementById('playlist-panel-top-button')) { createPlaylistPanelButton(); } } catch (error) { console.error('[YouTube+][Enhanced] Error checking for playlist panel:', error); } }; const checkPageType = () => { try { if (needsUniversalButton() && !document.getElementById('universal-top-button')) { createUniversalButton(); } checkForPlaylistPanel(); } catch (error) { console.error('[YouTube+][Enhanced] Error checking page type:', error); } }; checkForTabs(); setTimeout(checkPageType, 500); try { initReturnDislike(); } catch {} const observer = new MutationObserver(() => { checkForPlaylistPanel(); }); observer.observe(document.body, { childList: true, subtree: true, }); window.addEventListener('yt-navigate-finish', () => { try { cleanupReturnDislike(); } catch {} setTimeout(() => { checkPageType(); checkForTabs(); try { initReturnDislike(); } catch {} }, 300); }); } catch (error) { console.error('[YouTube+][Enhanced] Error in initialization:', error); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); (function () { 'use strict'; const CONFIG = { enabled: true, storageKey: 'youtube_endscreen_settings', selectors: '.ytp-ce-element-show,.ytp-ce-element,.ytp-endscreen-element,.ytp-ce-covering-overlay,.ytp-cards-teaser,.teaser-carousel,.ytp-cards-button,.iv-drawer,.video-annotations,.ytp-overlay-bottom-right', debounceMs: 32, batchSize: 20, }; const state = { observer: null, styleEl: null, isActive: false, removeCount: 0, lastCheck: 0, ytNavigateListenerKey: null, settingsNavListenerKey: null, }; const debounce = (fn, ms) => { try { return ( (window.YouTubeUtils && window.YouTubeUtils.debounce) || ((f, t) => { let id; return (...args) => { clearTimeout(id); id = setTimeout(() => f(...args), t); }; })(fn, ms) ); } catch { let id; return (...args) => { clearTimeout(id); id = setTimeout(() => fn(...args), ms); }; } }; const fastRemove = elements => { const len = Math.min(elements.length, CONFIG.batchSize); for (let i = 0; i < len; i++) { const el = elements[i]; if (el?.isConnected) { el.style.cssText = 'display:none!important;visibility:hidden!important'; try { el.remove(); state.removeCount++; } catch {} } } }; const settings = { load: () => { try { const data = localStorage.getItem(CONFIG.storageKey); CONFIG.enabled = data ? (JSON.parse(data).enabled ?? true) : true; } catch { CONFIG.enabled = true; } }, save: () => { try { localStorage.setItem(CONFIG.storageKey, JSON.stringify({ enabled: CONFIG.enabled })); } catch {} settings.apply(); }, apply: () => (CONFIG.enabled ? init() : cleanup()), }; const injectCSS = () => { if (state.styleEl || !CONFIG.enabled) return; const styles = `${CONFIG.selectors}{display:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important;transform:scale(0)!important}`; YouTubeUtils.StyleManager.add('end-screen-remover', styles); state.styleEl = true; }; const removeEndScreens = () => { if (!CONFIG.enabled) return; const now = performance.now(); if (now - state.lastCheck < CONFIG.debounceMs) return; state.lastCheck = now; const elements = document.querySelectorAll(CONFIG.selectors); if (elements.length) fastRemove(elements); }; const getClassNameValue = node => { if (typeof node.className === 'string') { return node.className; } if (node.className && typeof node.className === 'object' && 'baseVal' in node.className) { return (node.className).baseVal; } return ''; }; const isRelevantNode = node => { if (!(node instanceof Element)) return false; const classNameValue = getClassNameValue(node); return classNameValue.includes('ytp-') || node.querySelector?.('.ytp-ce-element'); }; const hasRelevantChanges = mutations => { for (const { addedNodes } of mutations) { for (const node of addedNodes) { if (isRelevantNode(node)) return true; } } return false; }; const createEndScreenObserver = throttledRemove => { return new MutationObserver(mutations => { if (hasRelevantChanges(mutations)) { throttledRemove(); } }); }; const setupWatcher = () => { if (state.observer || !CONFIG.enabled) return; const throttledRemove = debounce(removeEndScreens, CONFIG.debounceMs); state.observer = createEndScreenObserver(throttledRemove); YouTubeUtils.cleanupManager.registerObserver(state.observer); const target = document.querySelector('#movie_player') || document.body; state.observer.observe(target, { childList: true, subtree: true, attributeFilter: ['class', 'style'], }); }; const cleanup = () => { state.observer?.disconnect(); state.observer = null; state.styleEl?.remove(); state.styleEl = null; state.isActive = false; }; const init = () => { if (state.isActive || !CONFIG.enabled) return; state.isActive = true; injectCSS(); removeEndScreens(); setupWatcher(); }; const addSettingsUI = () => { const section = document.querySelector('.ytp-plus-settings-section[data-section="advanced"]'); if (!section || section.querySelector('.endscreen-settings')) return; const container = document.createElement('div'); container.className = 'ytp-plus-settings-item endscreen-settings'; container.innerHTML = `
${YouTubeUtils.t('endscreenHideDesc')}${state.removeCount ? ` (${state.removeCount} ${YouTubeUtils.t('removedSuffix').replace('{n}', '')?.trim() || 'removed'})` : ''}
`; section.appendChild(container); container.querySelector('input').addEventListener( 'change', e => { const { target } = (e); const { checked } = (target); CONFIG.enabled = checked; settings.save(); }, { passive: true } ); }; const handlePageChange = debounce(() => { if (location.pathname === '/watch') { cleanup(); requestIdleCallback ? requestIdleCallback(init) : setTimeout(init, 1); } }, 50); settings.load(); const { readyState } = document; if (readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } const handleSettingsNavClick = e => { const { target } = (e); if (target?.dataset?.section === 'advanced') { setTimeout(addSettingsUI, 10); } }; if (!state.ytNavigateListenerKey) { state.ytNavigateListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'yt-navigate-finish', (handlePageChange), { passive: true } ); } const settingsObserver = new MutationObserver(mutations => { for (const { addedNodes } of mutations) { for (const node of addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { setTimeout(addSettingsUI, 25); return; } } } }); YouTubeUtils.cleanupManager.registerObserver(settingsObserver); if (document.body) { settingsObserver.observe(document.body, { childList: true }); } else { document.addEventListener('DOMContentLoaded', () => { settingsObserver.observe(document.body, { childList: true }); }); } if (!state.settingsNavListenerKey) { state.settingsNavListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', handleSettingsNavClick, { passive: true, capture: true } ); } })(); (function () { 'use strict'; const RESUME_STORAGE_KEY = 'youtube_resume_times_v1'; const OVERLAY_ID = 'yt-resume-overlay'; const AUTO_HIDE_MS = 20000; const _globalI18n_resume = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const _getLanguage_resume = () => { try { if (_globalI18n_resume && typeof _globalI18n_resume.getLanguage === 'function') { return _globalI18n_resume.getLanguage(); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.getLanguage === 'function' ) { return window.YouTubeUtils.getLanguage(); } } catch { } const htmlLang = document.documentElement.lang || 'en'; return htmlLang.startsWith('ru') ? 'ru' : 'en'; }; const t = (key, params = {}) => { try { if (_globalI18n_resume && typeof _globalI18n_resume.t === 'function') { return _globalI18n_resume.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const readStorage = () => { try { return JSON.parse(localStorage.getItem(RESUME_STORAGE_KEY) || '{}'); } catch { return {}; } }; const writeStorage = obj => { try { localStorage.setItem(RESUME_STORAGE_KEY, JSON.stringify(obj)); } catch {} }; const getVideoIdFromUrlParams = () => { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('v') || null; }; const getVideoIdFromCanonical = () => { const meta = document.querySelector('link[rel="canonical"]'); if (!meta || !meta.href) return null; const u = new URL(meta.href); const vParam = u.searchParams.get('v'); if (vParam) return vParam; const pathMatch = u.pathname.match(/\/(watch|shorts)\/([^\/\?]+)/); return pathMatch && pathMatch[2] ? pathMatch[2] : null; }; const getVideoIdFromPlayerResponse = () => { if ( window.ytInitialPlayerResponse && window.ytInitialPlayerResponse.videoDetails && window.ytInitialPlayerResponse.videoDetails.videoId ) { return window.ytInitialPlayerResponse.videoDetails.videoId; } return null; }; const getVideoIdFromPathname = () => { const pathMatch = window.location.pathname.match(/\/(watch|shorts)\/([^\/\?]+)/); return pathMatch && pathMatch[2] ? pathMatch[2] : null; }; const getVideoId = () => { try { return ( getVideoIdFromUrlParams() || getVideoIdFromCanonical() || getVideoIdFromPlayerResponse() || getVideoIdFromPathname() || null ); } catch { return null; } }; const injectResumeOverlayStyles = () => { const resumeOverlayStyles = ` .ytp-resume-overlay{min-width:180px;max-width:36vw;background:rgba(24, 24, 24, 0.3);color:var(--yt-spec-text-primary,#fff);padding:12px 14px;border-radius:12px;backdrop-filter:blur(8px) saturate(150%);-webkit-backdrop-filter:blur(8px) saturate(150%);box-shadow:0 14px 40px rgba(0,0,0,0.48);border:1.25px solid rgba(255,255,255,0.06);font-family:Arial,Helvetica,sans-serif;display:flex;flex-direction:column;align-items:center;text-align:center} .ytp-resume-overlay .ytp-resume-title{font-weight:600;margin-bottom:8px} .ytp-resume-overlay .ytp-resume-actions{display:flex;gap:8px;justify-content:center;margin-top:6px} .ytp-resume-overlay .ytp-resume-btn{padding:6px 12px;border-radius:8px;border:none;cursor:pointer} .ytp-resume-overlay .ytp-resume-btn.primary{background:#1e88e5;color:#fff} .ytp-resume-overlay .ytp-resume-btn.ghost{background:rgba(255,255,255,0.06);color:#fff} `; try { if (window.YouTubeUtils?.StyleManager) { YouTubeUtils.StyleManager.add('ytp-resume-overlay-styles', resumeOverlayStyles); } else if (!document.getElementById('ytp-resume-overlay-styles')) { const s = document.createElement('style'); s.id = 'ytp-resume-overlay-styles'; s.textContent = resumeOverlayStyles; (document.head || document.documentElement).appendChild(s); } } catch {} }; const positionOverlayWrapper = (wrap, player) => { wrap.className = 'ytp-resume-overlay'; if (player) { try { const playerStyle = window.getComputedStyle(player); if (playerStyle.position === 'static') player.style.position = 'relative'; } catch {} wrap.style.cssText = 'position:absolute;left:50%;bottom:5%;transform:translate(-50%,-50%);z-index:9999;pointer-events:auto;'; player.appendChild(wrap); } else { wrap.style.cssText = 'position:fixed;left:50%;bottom:5%;transform:translate(-50%,-50%);z-index:1200;pointer-events:auto;'; document.body.appendChild(wrap); } }; const createOverlayButton = (className, text, onClick, wrap) => { const btn = document.createElement('button'); btn.className = `ytp-resume-btn ${className}`; btn.textContent = text; btn.addEventListener('click', () => { try { onClick(); } catch {} try { wrap.remove(); } catch {} }); return btn; }; const registerOverlayCleanup = (cancel, wrap) => { if (window.YouTubeUtils?.cleanupManager) { YouTubeUtils.cleanupManager.register(() => { try { cancel(); } catch {} try { wrap.remove(); } catch {} }); } }; const createOverlay = (seconds, onResume, onRestart) => { if (document.getElementById(OVERLAY_ID)) return null; const wrap = document.createElement('div'); wrap.id = OVERLAY_ID; const player = document.querySelector('#movie_player'); injectResumeOverlayStyles(); positionOverlayWrapper(wrap, player); const title = document.createElement('div'); title.className = 'ytp-resume-title'; title.textContent = `${t('resumePlayback')} (${formatTime(seconds)})`; const btnResume = createOverlayButton('primary', t('resume'), onResume, wrap); const btnRestart = createOverlayButton('ghost', t('startOver'), onRestart, wrap); const actions = document.createElement('div'); actions.className = 'ytp-resume-actions'; actions.appendChild(btnResume); actions.appendChild(btnRestart); wrap.appendChild(title); wrap.appendChild(actions); const to = setTimeout(() => { try { wrap.remove(); } catch {} }, AUTO_HIDE_MS); const cancel = () => clearTimeout(to); registerOverlayCleanup(cancel, wrap); return cancel; }; const formatTime = secs => { const s = Math.floor(secs % 60) .toString() .padStart(2, '0'); const m = Math.floor((secs / 60) % 60).toString(); const h = Math.floor(secs / 3600); return h ? `${h}:${m.padStart(2, '0')}:${s}` : `${m}:${s}`; }; const attachResumeHandlers = videoEl => { if (!videoEl) return; const getCurrentVideoId = () => getVideoId(); const vid = getCurrentVideoId(); if (!vid) return; const storage = readStorage(); const saved = storage[vid]; let timeUpdateHandler = null; let lastSavedAt = 0; const SAVE_THROTTLE_MS = 800; const startSaving = () => { if (timeUpdateHandler) return; timeUpdateHandler = () => { try { const currentVid = getCurrentVideoId(); if (!currentVid) return; const curSec = Math.floor(videoEl.currentTime || 0); const now = Date.now(); if (curSec && (!lastSavedAt || now - lastSavedAt > SAVE_THROTTLE_MS)) { const s = readStorage(); s[currentVid] = curSec; writeStorage(s); lastSavedAt = now; } } catch {} }; videoEl.addEventListener('timeupdate', timeUpdateHandler, { passive: true }); if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { YouTubeUtils.cleanupManager.register(() => { try { videoEl.removeEventListener('timeupdate', timeUpdateHandler); } catch {} }); } }; const stopSaving = () => { if (!timeUpdateHandler) return; try { videoEl.removeEventListener('timeupdate', timeUpdateHandler); } catch {} timeUpdateHandler = null; lastSavedAt = 0; }; if (saved && saved > 5 && !document.getElementById(OVERLAY_ID)) { const cancelTimeout = createOverlay( saved, () => { try { videoEl.currentTime = saved; videoEl.play(); } catch {} }, () => { try { videoEl.currentTime = 0; videoEl.play(); } catch {} } ); if (window.YouTubeUtils && YouTubeUtils.cleanupManager && cancelTimeout) { YouTubeUtils.cleanupManager.register(cancelTimeout); } } const onPlay = () => startSaving(); const onPause = () => stopSaving(); videoEl.addEventListener('play', onPlay, { passive: true }); videoEl.addEventListener('pause', onPause, { passive: true }); if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { YouTubeUtils.cleanupManager.register(() => { try { videoEl.removeEventListener('play', onPlay); videoEl.removeEventListener('pause', onPause); } catch {} }); } }; const findVideoElement = () => { const selectors = [ 'video.html5-main-video', 'video.video-stream', '#movie_player video', 'video', ]; for (const selector of selectors) { const video = document.querySelector(selector); if (video && video.tagName === 'VIDEO') { return (video); } } return null; }; const initResume = () => { if (window.location.pathname !== '/watch') { const existingOverlay = document.getElementById(OVERLAY_ID); if (existingOverlay) { existingOverlay.remove(); } return; } const existingOverlay = document.getElementById(OVERLAY_ID); if (existingOverlay) { existingOverlay.remove(); } const videoEl = findVideoElement(); if (videoEl) { attachResumeHandlers(videoEl); } else { setTimeout(initResume, 500); } }; const onNavigate = () => setTimeout(initResume, 150); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initResume, { once: true }); } else { initResume(); } if (window && window.document) { if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { YouTubeUtils.cleanupManager.registerListener(document, 'yt-navigate-finish', onNavigate, { passive: true, }); } else { document.addEventListener('yt-navigate-finish', onNavigate, { passive: true }); } } })(); (async function () { 'use strict'; const globalContext = typeof unsafeWindow === 'undefined' ? (window) : (unsafeWindow); const gmApi = globalContext?.GM ?? null; const gmInfo = globalContext?.GM_info ?? null; const scriptVersion = gmInfo?.script?.version ?? null; if (scriptVersion && /-(alpha|beta|dev|test)$/.test(scriptVersion)) { console.log( '%cytp - YouTube Play All\n', 'color: #bf4bcc; font-size: 32px; font-weight: bold', 'You are currently running a test version:', scriptVersion ); } if ( Object.prototype.hasOwnProperty.call(window, 'trustedTypes') && !window.trustedTypes.defaultPolicy ) { window.trustedTypes.createPolicy('default', { createHTML: string => string }); } const insertStylesSafely = html => { try { const target = document.head || document.documentElement; if (target && typeof target.insertAdjacentHTML === 'function') { target.insertAdjacentHTML('beforeend', html); return; } const onReady = () => { try { const targetElement = document.head || document.documentElement; if (targetElement && typeof targetElement.insertAdjacentHTML === 'function') { targetElement.insertAdjacentHTML('beforeend', html); } } catch {} }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', onReady, { once: true }); } else { onReady(); } } catch {} }; insertStylesSafely(``); const getVideoId = url => { try { return new URLSearchParams(new URL(url).search).get('v'); } catch { return null; } }; const queryHTMLElement = selector => { const el = document.querySelector(selector); return el instanceof HTMLElement ? el : null; }; const getPlayer = () => (document.querySelector('#movie_player')); const isAdPlaying = () => !!document.querySelector('.ad-interrupting'); const buildUrl = (v, list, ytpRandom) => { const randomParam = ytpRandom === null ? '' : `&ytp-random=${ytpRandom}`; return `/watch?v=${v}&list=${list}${randomParam}`; }; const redirect = (v, list, ytpRandom = null) => { if (location.host === 'm.youtube.com') { const url = buildUrl(v, list, ytpRandom); window.location.href = url; return; } try { const playlistPanel = document.querySelector('ytd-playlist-panel-renderer #items'); if (!playlistPanel) { const url = `/watch?v=${v}&list=${list}${ytpRandom === null ? '' : `&ytp-random=${ytpRandom}`}`; window.location.href = url; return; } const redirector = document.createElement('a'); redirector.className = 'yt-simple-endpoint style-scope ytd-playlist-panel-video-renderer'; redirector.setAttribute('hidden', ''); redirector.data = { commandMetadata: { webCommandMetadata: { url: buildUrl(v, list, ytpRandom), webPageType: 'WEB_PAGE_TYPE_WATCH', rootVe: 3832, }, }, watchEndpoint: { videoId: v, playlistId: list, }, }; playlistPanel.append(redirector); redirector.click(); } catch { const url = `/watch?v=${v}&list=${list}${ytpRandom === null ? '' : `&ytp-random=${ytpRandom}`}`; window.location.href = url; } }; let id = ''; const getOrCreateParentContainer = () => { let parent = location.host === 'm.youtube.com' ? queryHTMLElement( 'ytm-feed-filter-chip-bar-renderer .chip-bar-contents, ytm-feed-filter-chip-bar-renderer > div' ) : queryHTMLElement('ytd-feed-filter-chip-bar-renderer iron-selector#chips'); if (parent === null) { const grid = queryHTMLElement('ytd-rich-grid-renderer, ytm-rich-grid-renderer'); if (!grid) { console.warn('[YouTube+][Play All]', 'Could not find grid container'); return null; } let existingContainer = grid.querySelector('.ytp-button-container'); if (!existingContainer) { grid.insertAdjacentHTML('afterbegin', '
'); existingContainer = grid.querySelector('.ytp-button-container'); } parent = existingContainer instanceof HTMLElement ? existingContainer : null; } return parent; }; const getPlaylistIds = () => { if (window.location.pathname.endsWith('/videos')) { return { allPlaylist: 'UULF', popularPlaylist: 'UULP' }; } if (window.location.pathname.endsWith('/shorts')) { return { allPlaylist: 'UUSH', popularPlaylist: 'UUPS' }; } return { allPlaylist: 'UULV', popularPlaylist: 'UUPV' }; }; const addPlayButton = (parent, allPlaylist, popularPlaylist, playlistSuffix) => { if (parent.querySelector(':nth-child(2).selected, :nth-child(2).iron-selected')) { parent.insertAdjacentHTML( 'beforeend', `Play Popular` ); } else if (parent.querySelector(':nth-child(1).selected, :nth-child(1).iron-selected')) { parent.insertAdjacentHTML( 'beforeend', `Play All` ); } else { parent.insertAdjacentHTML( 'beforeend', `No Playlist Found` ); } }; const setupMobileNavigation = parent => { const navigate = href => window.location.assign(href); parent.querySelectorAll('.ytp-btn').forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); navigate(btn.href); }); }); }; const setupDesktopNavigation = (parent, allPlaylist, playlistSuffix) => { const navigate = href => window.location.assign(href); const attachNavigationHandler = elements => { elements.forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); navigate(btn.href); }); }); }; attachNavigationHandler(parent.querySelectorAll('.ytp-play-all-btn:not(.ytp-unsupported)')); parent.insertAdjacentHTML( 'beforeend', ` Play Random ` ); document.querySelectorAll('.ytp-random-popover').forEach(popover => popover.remove()); document.body.insertAdjacentHTML( 'beforeend', `` ); attachNavigationHandler(parent.querySelectorAll('.ytp-random-btn a')); const randomPopover = document.querySelector('.ytp-random-popover'); if (randomPopover) { attachNavigationHandler(randomPopover.querySelectorAll('a')); const randomMoreOptionsBtn = document.querySelector('.ytp-random-more-options-btn'); if (randomMoreOptionsBtn) { randomMoreOptionsBtn.addEventListener('click', () => { const rect = randomMoreOptionsBtn.getBoundingClientRect(); randomPopover.style.top = `${rect.bottom}px`; randomPopover.style.left = `${rect.right}px`; randomPopover.removeAttribute('hidden'); }); randomPopover.addEventListener('mouseleave', () => { randomPopover.setAttribute('hidden', ''); }); } } }; const apply = () => { if (id === '') { console.warn('[YouTube+][Play All]', 'Channel ID not yet determined'); return; } const parent = getOrCreateParentContainer(); if (!parent) { console.warn('[YouTube+][Play All]', 'Could not find parent container'); return; } if (parent.querySelector('.ytp-play-all-btn, .ytp-random-btn')) { console.log('[YouTube+][Play All]', 'Buttons already exist, skipping'); return; } const { allPlaylist, popularPlaylist } = getPlaylistIds(); const playlistSuffix = id.startsWith('UC') ? id.substring(2) : id; addPlayButton(parent, allPlaylist, popularPlaylist, playlistSuffix); if (location.host === 'm.youtube.com') { setupMobileNavigation(parent); } else { setupDesktopNavigation(parent, allPlaylist, playlistSuffix); } }; const observer = new MutationObserver(() => { removeButton(); apply(); }); const isValidPath = () => window.location.pathname.endsWith('/videos') || window.location.pathname.endsWith('/shorts') || window.location.pathname.endsWith('/streams'); const extractChannelIdFromCanonical = () => { try { const canonical = document.querySelector('link[rel="canonical"]'); if (!canonical || !canonical.href) { return null; } const match = canonical.href.match(/\/channel\/(UC[a-zA-Z0-9_-]{22})/); if (match && match[1]) { return match[1]; } const handleMatch = canonical.href.match(/\/@([^\/]+)/); if (handleMatch) { const pageData = document.querySelector('ytd-browse[page-subtype="channels"]'); if (pageData) { const channelId = pageData.getAttribute('channel-id'); if (channelId && channelId.startsWith('UC')) { return channelId; } } } } catch (e) { console.warn('[YouTube+][Play All]', 'Error extracting channel ID from canonical:', e); } return null; }; const extractChannelIdFromHTML = async () => { try { const html = await (await fetch(location.href)).text(); const canonicalMatch = html.match( / { observer.disconnect(); if (!isValidPath()) { return; } const element = document.querySelector( 'ytd-rich-grid-renderer, ytm-feed-filter-chip-bar-renderer .iron-selected, ytm-feed-filter-chip-bar-renderer .chip-bar-contents .selected' ); if (element) { observer.observe(element, { attributes: true, childList: false, subtree: false, }); } if (document.querySelector('.ytp-play-all-btn')) { return; } const channelId = extractChannelIdFromCanonical(); if (channelId) { id = channelId; apply(); return; } const htmlChannelId = await extractChannelIdFromHTML(); if (htmlChannelId) { id = htmlChannelId; apply(); } }; const removeButton = () => document.querySelectorAll('.ytp-btn').forEach(element => element.remove()); if (location.host === 'm.youtube.com') { setInterval(addButton, 1000); } else { window.addEventListener('yt-navigate-start', removeButton); window.addEventListener('yt-navigate-finish', addButton); try { setTimeout(addButton, 300); } catch {} } (() => { const getItems = playlist => { return new Promise(resolve => { const payload = { uri: `https://www.youtube.com/playlist?list=${playlist}`, requestType: `ytp ${gmInfo?.script?.version ?? 'unknown'}`, }; const markFailure = () => { const emulator = document.querySelector('.ytp-playlist-emulator'); if (emulator instanceof HTMLElement) { emulator.setAttribute('data-failed', 'rejected'); } }; const handleSuccess = data => { resolve(data); }; const handleError = () => { markFailure(); resolve({ status: 'error', items: [] }); }; if (gmApi && typeof gmApi.xmlHttpRequest === 'function') { gmApi.xmlHttpRequest({ method: 'POST', url: 'https://ytplaylist.robert.wesner.io/api/list', data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', }, onload: response => { try { handleSuccess(JSON.parse(response.responseText)); } catch (parseError) { console.error( '[YouTube+][Play All]', 'Failed to parse playlist response:', parseError ); handleError(); } }, onerror: _error => { handleError(); }, }); return; } fetch('https://ytplaylist.robert.wesner.io/api/list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }) .then(resp => resp.json()) .then(handleSuccess) .catch(err => { console.error('[YouTube+][Play All]', 'Playlist fetch failed:', err); handleError(); }); }); }; const processItems = items => { const itemsContainer = document.querySelector('.ytp-playlist-emulator .items'); const params = new URLSearchParams(window.location.search); const list = params.get('list'); if (!(itemsContainer instanceof HTMLElement)) { return; } items.forEach( item => { const element = document.createElement('div'); element.className = 'item'; element.textContent = item.title; element.setAttribute('data-id', item.videoId); element.addEventListener('click', () => redirect(item.videoId, list)); itemsContainer.append(element); } ); markCurrentItem(params.get('v')); }; const playNextEmulationItem = () => { document.querySelector(`.ytp-playlist-emulator .items .item[data-current] + .item`)?.click(); }; const markCurrentItem = videoId => { const existing = document.querySelector(`.ytp-playlist-emulator .items .item[data-current]`); if (existing) { existing.removeAttribute('data-current'); } const current = document.querySelector( `.ytp-playlist-emulator .items .item[data-id="${videoId}"]` ); if (current instanceof HTMLElement) { current.setAttribute('data-current', ''); const { parentElement } = current; if (parentElement instanceof HTMLElement) { const docElement = (document.documentElement); const fontSize = parseFloat(getComputedStyle(docElement).fontSize || '16'); parentElement.scrollTop = current.offsetTop - 12 * fontSize; } } }; const shouldSkipEmulation = (params, list) => { if (!window.location.pathname.endsWith('/watch')) return true; if (!list) return true; if (params.has('ytp-random')) return true; if (list.startsWith('TLPQ')) return true; if (list.length <= 4) return true; if (!new URLSearchParams(window.location.search).has('list')) return true; if ( !document.querySelector( '#secondary-inner > ytd-playlist-panel-renderer#playlist #items:empty' ) ) { return true; } return false; }; const handleExistingEmulator = (existingEmulator, list, videoId) => { if (!existingEmulator) return false; if (list === existingEmulator.getAttribute('data-list')) { markCurrentItem(videoId); return true; } window.location.reload(); return true; }; const createPlaylistEmulator = list => { const playlistEmulator = document.createElement('div'); playlistEmulator.className = 'ytp-playlist-emulator'; playlistEmulator.innerHTML = `
Playlist emulator
It looks like YouTube is unable to handle this large playlist. Playlist emulation is a limited fallback feature of ytp to enable you to watch even more content.
`; playlistEmulator.setAttribute('data-list', list); const playlistHost = document.querySelector( '#secondary-inner > ytd-playlist-panel-renderer#playlist' ); if (playlistHost instanceof HTMLElement) { playlistHost.insertAdjacentElement('afterend', (playlistEmulator)); } return playlistEmulator; }; const loadPlaylistItems = list => { getItems(list).then(response => { if (response?.status === 'running') { setTimeout(() => { getItems(list).then(nextResponse => { if (nextResponse && Array.isArray(nextResponse.items)) { processItems(nextResponse.items); } }); }, 5000); return; } if (response && Array.isArray(response.items)) { processItems(response.items); } }); }; const setupNextButton = () => { const nextButtonInterval = setInterval(() => { const nextButton = document.querySelector( '#ytd-player .ytp-next-button.ytp-button:not([ytp-emulation="applied"])' ); if (nextButton) { clearInterval(nextButtonInterval); const newButton = document.createElement('span'); newButton.className = nextButton.className; newButton.innerHTML = nextButton.innerHTML; nextButton.replaceWith(newButton); newButton.setAttribute('ytp-emulation', 'applied'); newButton.addEventListener('click', () => playNextEmulationItem()); } }, 1000); }; const setupKeyboardShortcuts = () => { document.addEventListener( 'keydown', event => { if (event.shiftKey && event.key.toLowerCase() === 'n') { event.stopImmediatePropagation(); event.preventDefault(); playNextEmulationItem(); } }, true ); }; const setupAutoAdvance = () => { setInterval(() => { const player = getPlayer(); if (!player || typeof player.getProgressState !== 'function') return; const progressState = player.getProgressState(); if (!progressState) return; if (!isAdPlaying()) { const isNearEnd = typeof progressState.current === 'number' && typeof progressState.duration === 'number' && progressState.current >= progressState.duration - 2; if (isNearEnd) { if (typeof player.pauseVideo === 'function') player.pauseVideo(); if (typeof player.seekTo === 'function') player.seekTo(0); playNextEmulationItem(); } } }, 500); }; const emulatePlaylist = () => { const params = new URLSearchParams(window.location.search); const list = params.get('list'); if (shouldSkipEmulation(params, list)) return; const existingEmulator = document.querySelector('.ytp-playlist-emulator'); if (handleExistingEmulator(existingEmulator, list, params.get('v'))) return; createPlaylistEmulator(list); loadPlaylistItems(list); setupNextButton(); setupKeyboardShortcuts(); setupAutoAdvance(); }; if (location.host === 'm.youtube.com') { console.log('[YouTube+][Play All]', 'Mobile playlist emulation not yet supported'); } else { window.addEventListener('yt-navigate-finish', () => setTimeout(emulatePlaylist, 1000)); } })(); (() => { if (location.host === 'm.youtube.com') { return; } const urlParams = new URLSearchParams(window.location.search); if (!urlParams.has('ytp-random') || urlParams.get('ytp-random') === '0') { return; } const ytpRandomParam = urlParams.get('ytp-random'); const ytpRandom = ytpRandomParam === 'prefer-newest' || ytpRandomParam === 'prefer-oldest' ? ytpRandomParam : 'random'; const getStorageKey = () => `ytp-random-${urlParams.get('list')}`; const getStorage = () => JSON.parse(localStorage.getItem(getStorageKey()) || '{}'); const isWatched = videoId => getStorage()[videoId] || false; const markWatched = videoId => { localStorage.setItem(getStorageKey(), JSON.stringify({ ...getStorage(), [videoId]: true })); document .querySelectorAll('#wc-endpoint[href*=zsA3X40nz9w]') .forEach(element => element.parentElement.setAttribute('hidden', '')); }; try { if (Array.isArray(getStorage())) { localStorage.removeItem(getStorageKey()); } } catch { localStorage.removeItem(getStorageKey()); } const calculateVideoIndex = (videosLength, randomMode) => { const preferredCount = Math.max(1, Math.min(Math.floor(videosLength * 0.2), 20)); let videoIndex; switch (randomMode) { case 'prefer-newest': videoIndex = Math.floor(Math.random() * preferredCount); break; case 'prefer-oldest': videoIndex = videosLength - preferredCount + Math.floor(Math.random() * preferredCount); break; default: videoIndex = Math.floor(Math.random() * videosLength); } return Math.max(0, Math.min(videoIndex, videosLength - 1)); }; const reloadWithVideo = (videoId, params, randomMode) => { params.set('v', videoId); params.set('ytp-random', randomMode); params.delete('t'); params.delete('index'); params.delete('ytp-random-initial'); window.location.href = `${window.location.pathname}?${params.toString()}`; }; const manualRedirectFallback = (videoId, listId, randomMode) => { const redirector = document.createElement('a'); redirector.className = 'yt-simple-endpoint style-scope ytd-playlist-panel-video-renderer'; redirector.setAttribute('hidden', ''); redirector.data = { commandMetadata: { webCommandMetadata: { url: `/watch?v=${videoId}&list=${listId}&ytp-random=${randomMode}`, webPageType: 'WEB_PAGE_TYPE_WATCH', rootVe: 3832, }, }, watchEndpoint: { videoId, playlistId: listId, }, }; const listContainer = document.querySelector('ytd-playlist-panel-renderer #items'); if (listContainer instanceof HTMLElement) { listContainer.append(redirector); } else { document.body.appendChild(redirector); } redirector.click(); }; const navigateToVideo = (videoId, params, randomMode) => { try { redirect(videoId, params.get('list'), randomMode); } catch (error) { console.error( '[YouTube+][Play All]', 'Error using redirect(), falling back to manual redirect:', error ); manualRedirectFallback(videoId, params.get('list'), randomMode); } }; const playNextRandom = (reload = false) => { const playerInstance = getPlayer(); if (playerInstance && typeof playerInstance.pauseVideo === 'function') { playerInstance.pauseVideo(); } const videos = Object.entries(getStorage()).filter(([_, watched]) => !watched); if (videos.length === 0) return; const params = new URLSearchParams(window.location.search); const videoIndex = calculateVideoIndex(videos.length, ytpRandom); const selectedVideoId = videos[videoIndex][0]; if (reload) { reloadWithVideo(selectedVideoId, params, ytpRandom); } else { navigateToVideo(selectedVideoId, params, ytpRandom); } }; let isIntervalSet = false; const addRandomPlayNotice = playlistContainer => { const headerContainer = playlistContainer.querySelector('.header'); if (headerContainer) { headerContainer.insertAdjacentHTML( 'afterend', `
This playlist is using random play.
The videos will not be played in the order listed here.
` ); } }; const collectPlaylistAnchors = playlistContainer => { const anchorSelectors = [ '#wc-endpoint', 'ytd-playlist-panel-video-renderer a#wc-endpoint', 'ytd-playlist-panel-video-renderer a', 'a#video-title', '#secondary ytd-playlist-panel-renderer a[href*="/watch?"]', ]; const anchors = []; anchorSelectors.forEach(sel => { playlistContainer.querySelectorAll(sel).forEach(a => { if (a instanceof Element && a.tagName === 'A') anchors.push( (a)); }); }); const uniq = []; const seen = new Set(); anchors.forEach(a => { const href = a.href || a.getAttribute('href') || ''; if (!seen.has(href)) { seen.add(href); uniq.push(a); } }); return uniq; }; const processPlaylistAnchors = (anchors, storage) => { const navigate = href => { window.location.href = href; }; anchors.forEach(element => { let videoId = null; try { videoId = new URL(element.href, window.location.origin).searchParams.get('v'); } catch { videoId = new URLSearchParams(element.search || '').get('v'); } if (!videoId) return; if (!isWatched(videoId)) { storage[videoId] = false; } try { const u = new URL(element.href, window.location.origin); u.searchParams.set('ytp-random', ytpRandom); element.href = u.toString(); } catch { } element.addEventListener('click', event => { event.preventDefault(); navigate(element.href); }); const entryKey = getVideoId(element.href); if (isWatched(entryKey)) { element.parentElement?.setAttribute('hidden', ''); } }); }; const setupRandomPlayBadge = playlistContainer => { const header = playlistContainer.querySelector('h3 a'); if (!header || header.tagName !== 'A') return; const anchorHeader = ( (header)); anchorHeader.innerHTML += ` ${ytpRandom} ×`; anchorHeader.href = 'javascript:void(0)'; const badge = anchorHeader.querySelector('.ytp-random-badge'); if (badge) { badge.addEventListener('click', event => { event.preventDefault(); localStorage.removeItem(getStorageKey()); const params = new URLSearchParams(location.search); params.delete('ytp-random'); window.location.href = `${window.location.pathname}?${params.toString()}`; }); } }; const setupKeyboardShortcut = () => { document.addEventListener( 'keydown', event => { if (event.shiftKey && event.key.toLowerCase() === 'n') { event.stopImmediatePropagation(); event.preventDefault(); const videoId = getVideoId(location.href); markWatched(videoId); playNextRandom(true); } }, true ); }; const updateUrlWithRandomParam = randomMode => { const params = new URLSearchParams(location.search); params.set('ytp-random', randomMode); window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`); }; const isValidProgressState = progressState => { return ( progressState && typeof progressState.current === 'number' && typeof progressState.duration === 'number' ); }; const handleVideoProgress = (progressState, videoId, player) => { if (isAdPlaying()) return; if (progressState.current / progressState.duration >= 0.9) { if (videoId) markWatched(videoId); } if (progressState.current >= progressState.duration - 2) { if (typeof player.pauseVideo === 'function') player.pauseVideo(); if (typeof player.seekTo === 'function') player.seekTo(0); playNextRandom(); } }; const setupCustomNextButton = videoId => { const nextButton = document.querySelector( '#ytd-player .ytp-next-button.ytp-button:not([ytp-random="applied"])' ); if (nextButton instanceof HTMLElement) { const newButton = document.createElement('span'); newButton.className = nextButton.className; newButton.innerHTML = nextButton.innerHTML; nextButton.replaceWith(newButton); newButton.setAttribute('ytp-random', 'applied'); newButton.addEventListener('click', () => { if (videoId) markWatched(videoId); playNextRandom(); }); } }; const setupProgressMonitoring = () => { setInterval(() => { const videoId = getVideoId(location.href); updateUrlWithRandomParam(ytpRandom); const player = getPlayer(); if (!player || typeof player.getProgressState !== 'function') { return; } const progressState = player.getProgressState(); if (!isValidProgressState(progressState)) { return; } handleVideoProgress(progressState, videoId, player); setupCustomNextButton(videoId); }, 1000); }; const applyRandomPlay = () => { if (!window.location.pathname.endsWith('/watch')) { return; } const playlistContainer = document.querySelector('#secondary ytd-playlist-panel-renderer'); if (!playlistContainer || playlistContainer.hasAttribute('ytp-random')) { return; } playlistContainer.setAttribute('ytp-random', 'applied'); addRandomPlayNotice(playlistContainer); const storage = getStorage(); const anchors = collectPlaylistAnchors(playlistContainer); processPlaylistAnchors(anchors, storage); localStorage.setItem(getStorageKey(), JSON.stringify(storage)); if (urlParams.get('ytp-random-initial') === '1' || isWatched(getVideoId(location.href))) { playNextRandom(); return; } setupRandomPlayBadge(playlistContainer); setupKeyboardShortcut(); if (!isIntervalSet) { isIntervalSet = true; setupProgressMonitoring(); } }; setInterval(applyRandomPlay, 1000); })(); })().catch(error => console.error( '%cytp - YouTube Play All\n', 'color: #bf4bcc; font-size: 32px; font-weight: bold', error ) ); (function () { 'use strict'; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') return _globalI18n.t(key, params); if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const AdBlocker = { config: { skipInterval: 500, removeInterval: 1500, enableLogging: false, maxRetries: 2, enabled: true, storageKey: 'youtube_adblocker_settings', }, state: { isYouTubeShorts: false, isYouTubeMusic: location.hostname === 'music.youtube.com', lastSkipAttempt: 0, retryCount: 0, initialized: false, }, cache: { moviePlayer: null, ytdPlayer: null, lastCacheTime: 0, cacheTimeout: 5000, }, selectors: { ads: '#player-ads,.ytp-ad-module,.ad-showing,.ytp-ad-timed-pie-countdown-container,.ytp-ad-survey-questions', elements: '#masthead-ad,ytd-merch-shelf-renderer,.yt-mealbar-promo-renderer,ytmusic-mealbar-promo-renderer,ytmusic-statement-banner-renderer,.ytp-featured-product', video: 'video.html5-main-video', removal: 'ytd-reel-video-renderer .ytd-ad-slot-renderer', }, settings: { load() { try { const saved = localStorage.getItem(AdBlocker.config.storageKey); if (!saved) return; const parsed = JSON.parse(saved); if (typeof parsed !== 'object' || parsed === null) { console.warn('[YouTube+][AdBlocker]', 'Invalid settings format'); return; } if (typeof parsed.enabled === 'boolean') { AdBlocker.config.enabled = parsed.enabled; } else { AdBlocker.config.enabled = true; } if (typeof parsed.enableLogging === 'boolean') { AdBlocker.config.enableLogging = parsed.enableLogging; } else { AdBlocker.config.enableLogging = false; } } catch (error) { console.error('[YouTube+][AdBlocker]', 'Error loading settings:', error); AdBlocker.config.enabled = true; AdBlocker.config.enableLogging = false; } }, save() { try { const settingsToSave = { enabled: AdBlocker.config.enabled, enableLogging: AdBlocker.config.enableLogging, }; localStorage.setItem(AdBlocker.config.storageKey, JSON.stringify(settingsToSave)); } catch (error) { console.error('[YouTube+][AdBlocker]', 'Error saving settings:', error); } }, }, getPlayer() { const now = Date.now(); if (now - AdBlocker.cache.lastCacheTime > AdBlocker.cache.cacheTimeout) { AdBlocker.cache.moviePlayer = document.querySelector('#movie_player'); AdBlocker.cache.ytdPlayer = document.querySelector('#ytd-player'); AdBlocker.cache.lastCacheTime = now; } const playerEl = AdBlocker.cache.ytdPlayer; return { element: AdBlocker.cache.moviePlayer, player: playerEl?.getPlayer?.() || playerEl, }; }, skipAd() { if (!AdBlocker.config.enabled) return; const now = Date.now(); if (now - AdBlocker.state.lastSkipAttempt < 300) return; AdBlocker.state.lastSkipAttempt = now; if (location.pathname.startsWith('/shorts/')) return; const adElement = document.querySelector( '.ad-showing, .ytp-ad-timed-pie-countdown-container' ); if (!adElement) { AdBlocker.state.retryCount = 0; return; } try { const { player } = AdBlocker.getPlayer(); if (!player) return; const video = document.querySelector(AdBlocker.selectors.video); if (video) video.muted = true; if (AdBlocker.state.isYouTubeMusic && video) { (video).currentTime = video.duration || 999; } else if (typeof player.getVideoData === 'function') { const videoData = player.getVideoData(); if (videoData?.video_id) { const currentTime = Math.floor(player.getCurrentTime?.() || 0); if (typeof player.loadVideoById === 'function') { player.loadVideoById(videoData.video_id, currentTime); } } } AdBlocker.state.retryCount = 0; } catch { if (AdBlocker.state.retryCount < AdBlocker.config.maxRetries) { AdBlocker.state.retryCount++; setTimeout(AdBlocker.skipAd, 800); } } }, addCss() { if (document.querySelector('#yt-ab-styles') || !AdBlocker.config.enabled) return; const styles = `${AdBlocker.selectors.ads},${AdBlocker.selectors.elements}{display:none!important;}`; YouTubeUtils.StyleManager.add('yt-ab-styles', styles); }, removeCss() { YouTubeUtils.StyleManager.remove('yt-ab-styles'); }, removeElements() { if (!AdBlocker.config.enabled || AdBlocker.state.isYouTubeMusic) return; const remove = () => { const elements = document.querySelectorAll(AdBlocker.selectors.removal); elements.forEach(el => el.closest('ytd-reel-video-renderer')?.remove()); }; if (window.requestIdleCallback) { requestIdleCallback(remove, { timeout: 100 }); } else { setTimeout(remove, 0); } }, addSettingsUI() { const section = document.querySelector('.ytp-plus-settings-section[data-section="basic"]'); if (!section || section.querySelector('.ab-settings')) return; try { const item = document.createElement('div'); item.className = 'ytp-plus-settings-item ab-settings'; item.innerHTML = `
${t('adBlockerDescription')}
`; section.appendChild(item); item.querySelector('input').addEventListener('change', e => { const { target } = e; const input = (target); const { checked } = input; AdBlocker.config.enabled = checked; AdBlocker.settings.save(); AdBlocker.config.enabled ? AdBlocker.addCss() : AdBlocker.removeCss(); }); } catch (error) { YouTubeUtils.logError('AdBlocker', 'Failed to add settings UI', error); } }, init() { if (AdBlocker.state.initialized) return; AdBlocker.state.initialized = true; AdBlocker.settings.load(); if (AdBlocker.config.enabled) { AdBlocker.addCss(); AdBlocker.removeElements(); } const skipInterval = setInterval(AdBlocker.skipAd, AdBlocker.config.skipInterval); const removeInterval = setInterval(AdBlocker.removeElements, AdBlocker.config.removeInterval); YouTubeUtils.cleanupManager.registerInterval(skipInterval); YouTubeUtils.cleanupManager.registerInterval(removeInterval); const handleNavigation = () => { AdBlocker.state.isYouTubeShorts = location.pathname.startsWith('/shorts/'); AdBlocker.cache.lastCacheTime = 0; }; const originalPushState = history.pushState; history.pushState = function (...args) { const result = originalPushState.call(this, ...args); setTimeout(handleNavigation, 50); return result; }; const settingsObserver = new MutationObserver(_mutations => { for (const { addedNodes } of _mutations) { for (const node of addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { setTimeout(AdBlocker.addSettingsUI, 50); return; } } } }); YouTubeUtils.cleanupManager.registerObserver(settingsObserver); if (document.body) { settingsObserver.observe(document.body, { childList: true }); } else { document.addEventListener('DOMContentLoaded', () => { settingsObserver.observe(document.body, { childList: true }); }); } const clickHandler = e => { const { target } = (e); if (target?.dataset?.section === 'basic') { setTimeout(AdBlocker.addSettingsUI, 25); } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, { passive: true, capture: true, }); if (AdBlocker.config.enabled) { setTimeout(AdBlocker.skipAd, 200); } }, }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', AdBlocker.init, { once: true }); } else { AdBlocker.init(); } })(); (function () { 'use strict'; const _globalI18n_pip = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n_pip && typeof _globalI18n_pip.t === 'function') { return _globalI18n_pip.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const pipSettings = { enabled: true, shortcut: { key: 'P', shiftKey: true, altKey: false, ctrlKey: false }, storageKey: 'youtube_pip_settings', }; const PIP_SESSION_KEY = 'youtube_plus_pip_session'; const getVideoElement = () => { try { const candidate = (typeof YouTubeUtils?.querySelector === 'function' && YouTubeUtils.querySelector('video')) || document.querySelector('video'); if (candidate && candidate.tagName && candidate.tagName.toLowerCase() === 'video') { return (candidate); } return null; } catch (error) { console.error('[YouTube+][PiP]', 'Error getting video element:', error); return null; } }; const waitForMetadata = video => { if (!video) { return Promise.reject(new Error('[PiP] Invalid video element')); } if (video.readyState >= 1 && !video.seeking) { return Promise.resolve(); } return new Promise((resolve, reject) => { let settled = false; const cleanup = () => { video.removeEventListener('loadedmetadata', onLoaded); video.removeEventListener('error', onError); if (timeoutId) { clearTimeout(timeoutId); } }; const onLoaded = () => { if (settled) return; settled = true; cleanup(); resolve(); }; const onError = () => { if (settled) return; settled = true; cleanup(); reject(new Error('[PiP] Video metadata failed to load')); }; let timeoutId = setTimeout(() => { if (settled) return; settled = true; cleanup(); reject(new Error('[PiP] Timed out waiting for video metadata')); }, 3000); const registeredTimeout = YouTubeUtils?.cleanupManager?.registerTimeout?.(timeoutId); if (registeredTimeout) { timeoutId = registeredTimeout; } video.addEventListener('loadedmetadata', onLoaded, { once: true }); video.addEventListener('error', onError, { once: true }); }); }; const setSessionActive = isActive => { try { if (isActive) { sessionStorage.setItem(PIP_SESSION_KEY, 'true'); } else { sessionStorage.removeItem(PIP_SESSION_KEY); } } catch {} }; const wasSessionActive = () => { try { return sessionStorage.getItem(PIP_SESSION_KEY) === 'true'; } catch { return false; } }; const mergeShortcutSettings = parsedShortcut => { if (!parsedShortcut || typeof parsedShortcut !== 'object') return; if (typeof parsedShortcut.key === 'string' && parsedShortcut.key.length > 0) { pipSettings.shortcut.key = parsedShortcut.key; } if (typeof parsedShortcut.shiftKey === 'boolean') { pipSettings.shortcut.shiftKey = parsedShortcut.shiftKey; } if (typeof parsedShortcut.altKey === 'boolean') { pipSettings.shortcut.altKey = parsedShortcut.altKey; } if (typeof parsedShortcut.ctrlKey === 'boolean') { pipSettings.shortcut.ctrlKey = parsedShortcut.ctrlKey; } }; const isValidSettings = parsed => { if (typeof parsed !== 'object' || parsed === null) { console.warn('[YouTube+][PiP]', 'Invalid settings format'); return false; } return true; }; const loadSettings = () => { try { const saved = localStorage.getItem(pipSettings.storageKey); if (!saved) return; const parsed = JSON.parse(saved); if (!isValidSettings(parsed)) return; if (typeof parsed.enabled === 'boolean') { pipSettings.enabled = parsed.enabled; } mergeShortcutSettings(parsed.shortcut); } catch (e) { console.error('[YouTube+][PiP]', 'Error loading settings:', e); } }; const saveSettings = () => { try { const settingsToSave = { enabled: pipSettings.enabled, shortcut: pipSettings.shortcut, }; localStorage.setItem(pipSettings.storageKey, JSON.stringify(settingsToSave)); } catch (e) { console.error('[YouTube+][PiP]', 'Error saving settings:', e); } }; const getCurrentPiPElement = () => { const current = document.pictureInPictureElement; if (current && typeof current === 'object' && 'tagName' in current) { const tag = (current).tagName; if (typeof tag === 'string' && tag.toLowerCase() === 'video') { return ( (current)); } } return null; }; const togglePictureInPicture = async video => { if (!pipSettings.enabled || !video) return; try { const currentPiP = getCurrentPiPElement(); if (currentPiP && currentPiP !== video) { await document.exitPictureInPicture(); setSessionActive(false); } if (getCurrentPiPElement() === video) { await document.exitPictureInPicture(); setSessionActive(false); return; } if (video.disablePictureInPicture) { throw new Error('Picture-in-Picture is disabled by the video element'); } await waitForMetadata(video); await video.requestPictureInPicture(); setSessionActive(true); } catch (error) { console.error('[YouTube+][PiP] Failed to toggle Picture-in-Picture:', error); } }; const initPipStyles = () => { if (!document.getElementById('pip-styles')) { const styles = ` .pip-shortcut-editor { display: flex; align-items: center; gap: 8px; } .pip-shortcut-editor select, #pip-key {background: rgba(34, 34, 34, var(--yt-header-bg-opacity)); color: var(--yt-spec-text-primary); border: 1px solid var(--yt-spec-10-percent-layer); border-radius: var(--yt-radius-sm); padding: 4px;} `; YouTubeUtils.StyleManager.add('pip-styles', styles); } }; const getModifierValue = () => { const { ctrlKey, altKey, shiftKey } = pipSettings.shortcut; const mods = []; if (ctrlKey) mods.push('ctrl'); if (altKey) mods.push('alt'); if (shiftKey) mods.push('shift'); return mods.length > 0 ? mods.join('+') : 'none'; }; const createEnableToggle = advancedSection => { const enableItem = document.createElement('div'); enableItem.className = 'ytp-plus-settings-item pip-settings-item'; enableItem.innerHTML = `
${t('pipDescription')}
`; advancedSection.appendChild(enableItem); const shortcutItem = createShortcutItem(); advancedSection.appendChild(shortcutItem); return shortcutItem; }; const createShortcutItem = () => { const shortcutItem = document.createElement('div'); shortcutItem.className = 'ytp-plus-settings-item pip-shortcut-item'; shortcutItem.style.display = pipSettings.enabled ? 'flex' : 'none'; shortcutItem.innerHTML = `
${t('pipShortcutDescription')}
+
`; return shortcutItem; }; const setupEnableCheckbox = shortcutItem => { document.getElementById('pip-enable-checkbox').addEventListener('change', e => { const { target } = (e); const { checked } = (target); pipSettings.enabled = checked; shortcutItem.style.display = pipSettings.enabled ? 'flex' : 'none'; saveSettings(); }); }; const createModifierLabel = v => { if (v === 'none') return t('none'); return v .replace(/\+/g, '+') .split('+') .map(k => t(k.toLowerCase())) .join('+') .split('+') .map(k => k.charAt(0).toUpperCase() + k.slice(1)) .join('+'); }; const setupModifierSelect = modifierValue => { const native = document.getElementById('pip-modifier-combo'); if (!native) return; const opts = [ 'none', 'ctrl', 'alt', 'shift', 'ctrl+alt', 'ctrl+shift', 'alt+shift', 'ctrl+alt+shift', ]; const factory = window.YouTubePlusHelpers?.DOM?.createCustomSelect; if (typeof factory !== 'function') return; const custom = factory(); custom.setOptions(opts.map(v => ({ value: v, text: createModifierLabel(v) }))); custom.value = modifierValue; try { native.parentNode.replaceChild(custom, native); } catch { return; } custom.addEventListener('change', () => { const value = custom.value || ''; pipSettings.shortcut.ctrlKey = value.includes('ctrl'); pipSettings.shortcut.altKey = value.includes('alt'); pipSettings.shortcut.shiftKey = value.includes('shift'); saveSettings(); }); }; const setupKeyInput = () => { document.getElementById('pip-key').addEventListener('input', e => { const { target } = (e); const { value: val } = (target); if (val) { pipSettings.shortcut.key = val.toUpperCase(); saveSettings(); } }); document.getElementById('pip-key').addEventListener('keydown', e => e.stopPropagation()); }; const addPipSettingsToModal = () => { const advancedSection = YouTubeUtils.querySelector( '.ytp-plus-settings-section[data-section="advanced"]' ); if (!advancedSection || YouTubeUtils.querySelector('.pip-settings-item')) return; initPipStyles(); const shortcutItem = createEnableToggle(advancedSection); setupEnableCheckbox(shortcutItem); setupModifierSelect(getModifierValue()); setupKeyInput(); }; loadSettings(); document.addEventListener('keydown', e => { if (!pipSettings.enabled) return; const { shiftKey, altKey, ctrlKey, key } = pipSettings.shortcut; if ( e.shiftKey === shiftKey && e.altKey === altKey && e.ctrlKey === ctrlKey && e.key.toUpperCase() === key ) { const video = getVideoElement(); if (video) { togglePictureInPicture(video); } e.preventDefault(); } }); window.addEventListener('storage', e => { if (e.key === pipSettings.storageKey) { loadSettings(); } }); window.addEventListener('load', () => { if (!pipSettings.enabled || !wasSessionActive() || document.pictureInPictureElement) { return; } const resumePiP = () => { const video = getVideoElement(); if (!video) return; togglePictureInPicture(video).catch(() => { setSessionActive(false); }); }; const ensureCleanup = handler => { if (!handler) return; try { document.removeEventListener('pointerdown', handler, true); } catch {} }; const cleanupListeners = () => { ensureCleanup(pointerListener); ensureCleanup(keyListener); }; const pointerListener = () => { cleanupListeners(); resumePiP(); }; const keyListener = () => { cleanupListeners(); resumePiP(); }; document.addEventListener('pointerdown', pointerListener, { once: true, capture: true }); document.addEventListener('keydown', keyListener, { once: true, capture: true }); }); const observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { setTimeout(addPipSettingsToModal, 100); } } } document.addEventListener('leavepictureinpicture', () => { setSessionActive(false); }); if (YouTubeUtils.querySelector('.ytp-plus-settings-nav-item[data-section="advanced"].active')) { if (!YouTubeUtils.querySelector('.pip-settings-item')) { setTimeout(addPipSettingsToModal, 50); } } }); YouTubeUtils.cleanupManager.registerObserver(observer); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); } const clickHandler = e => { const { target } = (e); if (target?.classList && target.classList.contains('ytp-plus-settings-nav-item')) { if (target.dataset?.section === 'advanced') { setTimeout(addPipSettingsToModal, 50); } } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, true); })(); (function () { 'use strict'; if (window.location.hostname !== 'www.youtube.com' || window.frameElement) { return; } if (window._timecodeModuleInitialized) return; window._timecodeModuleInitialized = true; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') { return _globalI18n.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const config = { enabled: true, autoDetect: true, shortcut: { key: 'T', shiftKey: true, altKey: false, ctrlKey: false }, storageKey: 'youtube_timecode_settings', autoSave: true, autoTrackPlayback: true, panelPosition: null, export: true, }; const state = { timecodes: new Map(), dom: {}, isReloading: false, activeIndex: null, trackingId: 0, dragging: false, editingIndex: null, resizeListenerKey: null, }; let initStarted = false; const scheduleInitRetry = () => { const timeoutId = setTimeout(init, 250); YouTubeUtils.cleanupManager?.registerTimeout?.(timeoutId); }; const applyBooleanSettings = parsed => { const booleanFields = ['enabled', 'autoDetect', 'autoSave', 'autoTrackPlayback', 'export']; booleanFields.forEach(field => { const value = field === 'export' ? parsed.export : parsed[field]; if (typeof value === 'boolean') { config[field] = value; } }); }; const applyShortcutSettings = shortcut => { if (!shortcut || typeof shortcut !== 'object') return; const shortcutFields = { key: 'string', shiftKey: 'boolean', altKey: 'boolean', ctrlKey: 'boolean', }; Object.entries(shortcutFields).forEach(([field, expectedType]) => { if (typeof shortcut[field] === expectedType) { config.shortcut[field] = shortcut[field]; } }); }; const isValidPanelPosition = (left, top) => { return ( typeof left === 'number' && typeof top === 'number' && !isNaN(left) && !isNaN(top) && left >= 0 && top >= 0 ); }; const applyPanelPosition = panelPosition => { if (!panelPosition || typeof panelPosition !== 'object') return; const { left, top } = panelPosition; if (isValidPanelPosition(left, top)) { config.panelPosition = { left, top }; } }; const loadSettings = () => { try { const saved = localStorage.getItem(config.storageKey); if (!saved) return; const parsed = JSON.parse(saved); if (typeof parsed !== 'object' || parsed === null) { console.warn('[YouTube+][Timecode]', 'Invalid settings format'); return; } applyBooleanSettings(parsed); applyShortcutSettings(parsed.shortcut); applyPanelPosition(parsed.panelPosition); } catch (error) { console.error('[YouTube+][Timecode]', 'Error loading settings:', error); } }; const saveSettings = () => { try { const settingsToSave = { enabled: config.enabled, autoDetect: config.autoDetect, shortcut: config.shortcut, autoSave: config.autoSave, autoTrackPlayback: config.autoTrackPlayback, panelPosition: config.panelPosition, export: config.export, }; localStorage.setItem(config.storageKey, JSON.stringify(settingsToSave)); } catch (error) { console.error('[YouTube+][Timecode]', 'Error saving settings:', error); } }; const isValidPanel = panel => { return panel && panel instanceof HTMLElement; }; const areValidCoordinates = (left, top) => { return typeof left === 'number' && typeof top === 'number' && !isNaN(left) && !isNaN(top); }; const getPanelDimensions = panel => { const rect = panel.getBoundingClientRect(); return { width: rect.width || panel.offsetWidth || 0, height: rect.height || panel.offsetHeight || 0, }; }; const clamp = (value, min, max) => Math.min(Math.max(min, value), max); const clampPanelPosition = (panel, left, top) => { try { if (!isValidPanel(panel)) { console.warn('[YouTube+][Timecode]', 'Invalid panel element'); return { left: 0, top: 0 }; } if (!areValidCoordinates(left, top)) { console.warn('[YouTube+][Timecode]', 'Invalid position coordinates'); return { left: 0, top: 0 }; } const { width, height } = getPanelDimensions(panel); const maxLeft = Math.max(0, window.innerWidth - width); const maxTop = Math.max(0, window.innerHeight - height); return { left: clamp(left, 0, maxLeft), top: clamp(top, 0, maxTop), }; } catch (error) { console.error('[YouTube+][Timecode]', 'Error clamping panel position:', error); return { left: 0, top: 0 }; } }; const savePanelPosition = (left, top) => { try { if (typeof left !== 'number' || typeof top !== 'number' || isNaN(left) || isNaN(top)) { console.warn('[YouTube+][Timecode]', 'Invalid position coordinates for saving'); return; } config.panelPosition = { left, top }; saveSettings(); } catch (error) { console.error('[YouTube+][Timecode]', 'Error saving panel position:', error); } }; const applySavedPanelPosition = panel => { if (!panel || !config.panelPosition) return; requestAnimationFrame(() => { const { left, top } = clampPanelPosition( panel, config.panelPosition.left, config.panelPosition.top ); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = 'auto'; }); }; const showNotification = (message, duration = 2000, type = 'info') => { YouTubeUtils.NotificationManager.show(message, { duration, type }); }; const formatTime = seconds => { if (isNaN(seconds)) return '00:00'; const roundedSeconds = Math.round(seconds); const h = Math.floor(roundedSeconds / 3600); const m = Math.floor((roundedSeconds % 3600) / 60); const s = roundedSeconds % 60; return h > 0 ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` : `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; }; const parseTime = timeStr => { try { if (!timeStr || typeof timeStr !== 'string') return null; const str = timeStr.trim(); if (str.length === 0 || str.length > 12) return null; let match = str.match(/^(\d+):(\d{1,2}):(\d{2})$/); if (match) { const [, h, m, s] = match.map(Number); if (isNaN(h) || isNaN(m) || isNaN(s)) return null; if (m >= 60 || s >= 60 || h < 0 || m < 0 || s < 0) return null; const total = h * 3600 + m * 60 + s; return total <= 86400 ? total : null; } match = str.match(/^(\d{1,2}):(\d{2})$/); if (match) { const [, m, s] = match.map(Number); if (isNaN(m) || isNaN(s)) return null; if (m >= 60 || s >= 60 || m < 0 || s < 0) return null; return m * 60 + s; } return null; } catch (error) { console.error('[YouTube+][Timecode]', 'Error parsing time:', error); return null; } }; const extractTimecodes = text => { try { if (!text || typeof text !== 'string') return []; let processedText = text; if (processedText.length > 50000) { console.warn('[YouTube+][Timecode]', 'Text too long, truncating'); processedText = processedText.substring(0, 50000); } const timecodes = []; const seen = new Set(); const patterns = [ /(\d{1,2}:\d{2}(?::\d{2})?)\s*[-–—]\s*(.+?)$/gm, /^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+?)$/gm, /(\d{1,2}:\d{2}(?::\d{2})?)\s*[-–—:]\s*([^\n\r]{1,100}?)(?=\s*\d{1,2}:\d{2}|\s*$)/g, /(\d{1,2}:\d{2}(?::\d{2})?)\s*[–—-]\s*([^\n]+)/gm, /^(\d{1,2}:\d{2}(?::\d{2})?)\s*(.+)$/gm, ]; for (const pattern of patterns) { let match; let iterations = 0; const maxIterations = 1000; while ((match = pattern.exec(processedText)) !== null && iterations++ < maxIterations) { const time = parseTime(match[1]); if (time !== null && !seen.has(time)) { seen.add(time); let label = (match[2] || formatTime(time)) .trim() .replace(/^\d+[\.\)]\s*/, '') .substring(0, 100); label = label.replace(/[<>\"']/g, ''); if (label) { timecodes.push({ time, label, originalText: match[1] }); } } } if (iterations >= maxIterations) { console.warn('[YouTube+][Timecode]', 'Maximum iterations reached during extraction'); } } return timecodes.sort((a, b) => a.time - b.time); } catch (error) { console.error('[YouTube+][Timecode]', 'Error extracting timecodes:', error); return []; } }; const DESCRIPTION_SELECTORS = [ '#description-inline-expander yt-attributed-string', '#description-inline-expander yt-formatted-string', '#description-inline-expander ytd-text-inline-expander', '#description-inline-expander .yt-core-attributed-string', '#description ytd-text-inline-expander', '#description ytd-expandable-video-description-body-renderer', '#description.ytd-watch-metadata yt-formatted-string', '#description.ytd-watch-metadata #description-inline-expander', '#tab-info ytd-expandable-video-description-body-renderer yt-formatted-string', '#tab-info ytd-expandable-video-description-body-renderer yt-attributed-string', '#structured-description ytd-text-inline-expander', '#structured-description yt-formatted-string', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] yt-formatted-string', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] yt-attributed-string', 'ytd-watch-metadata #description', 'ytd-watch-metadata #description-inline-expander', '#description', ]; const DESCRIPTION_SELECTOR_COMBINED = DESCRIPTION_SELECTORS.join(','); const DESCRIPTION_EXPANDERS = [ '#description-inline-expander yt-button-shape button', '#description-inline-expander tp-yt-paper-button#expand', '#description-inline-expander tp-yt-paper-button[aria-label]', 'ytd-watch-metadata #description-inline-expander yt-button-shape button', 'ytd-text-inline-expander[collapsed] yt-button-shape button', 'ytd-text-inline-expander[collapsed] tp-yt-paper-button#expand', 'ytd-expandable-video-description-body-renderer #expand', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] #expand', ]; const sleep = (ms = 250) => new Promise(resolve => setTimeout(resolve, ms)); const collectDescriptionText = () => { const snippets = []; DESCRIPTION_SELECTORS.forEach(selector => { document.querySelectorAll(selector).forEach(node => { const text = node?.textContent?.trim(); if (text) { snippets.push(text); } }); }); return snippets.join('\n'); }; const isButtonExpanded = button => { const ariaExpanded = button.getAttribute('aria-expanded'); if (ariaExpanded === 'true') return true; const ariaLabel = button.getAttribute('aria-label')?.toLowerCase(); return ariaLabel && ariaLabel.includes('less'); }; const tryClickExpandButton = async button => { if (button.offsetParent === null) return false; try { (button).click(); await sleep(400); return true; } catch (error) { console.warn('[YouTube+][Timecode]', 'Failed to click expand button:', error); return false; } }; const tryExpandInlineExpander = async () => { const inlineExpander = document.querySelector('ytd-text-inline-expander[collapsed]'); if (!inlineExpander) return false; try { inlineExpander.removeAttribute('collapsed'); await sleep(300); return true; } catch (error) { YouTubeUtils.logError('TimecodePanel', 'Failed to expand description', error); return false; } }; const expandDescriptionIfNeeded = async () => { for (const selector of DESCRIPTION_EXPANDERS) { const button = document.querySelector(selector); if (!button) continue; if (isButtonExpanded(button)) return false; const clicked = await tryClickExpandButton(button); if (clicked) return true; } return await tryExpandInlineExpander(); }; const ensureDescriptionReady = async () => { const initialText = collectDescriptionText(); if (initialText) return; const maxAttempts = 3; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { await YouTubeUtils.waitForElement(DESCRIPTION_SELECTOR_COMBINED, 1500); } catch { } await sleep(200); const expanded = await expandDescriptionIfNeeded(); await sleep(expanded ? 500 : 200); const text = collectDescriptionText(); if (text && text.length > initialText.length) { return; } } }; const getCurrentVideoId = () => new URLSearchParams(window.location.search).get('v'); const detectTimecodes = async (options = {}) => { const { force = false } = options; if (!config.enabled) return []; if (!force && !config.autoDetect) return []; const videoId = getCurrentVideoId(); if (!videoId) return []; const cacheKey = `detect_${videoId}`; if (!force && state.timecodes.has(cacheKey)) { const cached = state.timecodes.get(cacheKey); if (Array.isArray(cached) && cached.length) { return cached; } state.timecodes.delete(cacheKey); } await ensureDescriptionReady(); const uniqueMap = new Map(); const descriptionText = collectDescriptionText(); if (descriptionText) { const extracted = extractTimecodes(descriptionText); extracted.forEach(tc => { if (tc.time >= 0 && tc.label?.trim()) { uniqueMap.set(tc.time.toString(), tc); } }); } const chapters = getYouTubeChapters(); chapters.forEach(chapter => { if (chapter.time >= 0 && chapter.label?.trim()) { uniqueMap.set(chapter.time.toString(), chapter); } }); const result = Array.from(uniqueMap.values()).sort((a, b) => a.time - b.time); const hadExistingItems = state.dom.list?.childElementCount > 0; if (result.length > 0) { updateTimecodePanel(result); state.timecodes.set(cacheKey, result); if (config.autoSave) saveTimecodesToStorage(result); } else { if (force || !hadExistingItems) { updateTimecodePanel([]); } if (force) { state.timecodes.delete(cacheKey); } } return result; }; const reloadTimecodes = async (buttonOverride = null) => { const button = buttonOverride || state.dom.reloadButton || document.getElementById('timecode-reload'); if (state.isReloading || !config.enabled) return; state.isReloading = true; if (button) { button.disabled = true; button.classList.add('loading'); } try { const result = await detectTimecodes({ force: true }); if (Array.isArray(result) && result.length) { showNotification(t('foundTimecodes').replace('{count}', result.length)); } else { updateTimecodePanel([]); showNotification(t('noTimecodesFound')); } } catch (error) { YouTubeUtils.logError('TimecodePanel', 'Reload failed', error); showNotification(t('reloadError')); } finally { if (button) { button.disabled = false; button.classList.remove('loading'); } state.isReloading = false; } }; const extractTextFromSelectors = (item, selectors) => { for (const sel of selectors) { const el = item.querySelector(sel); if (el?.textContent) { return el.textContent; } } return null; }; const createChapterObject = (timeText, titleText) => { if (!timeText) return null; const time = parseTime(timeText.trim()); if (time === null) return null; const cleanTitle = titleText?.trim().replace(/\s+/g, ' ') || formatTime(time); return { time, label: cleanTitle, isChapter: true, }; }; const getYouTubeChapters = () => { const selectors = [ 'ytd-macro-markers-list-item-renderer', 'ytd-chapter-renderer', 'ytd-engagement-panel-section-list-renderer[target-id*="description-chapters"] ytd-macro-markers-list-item-renderer', 'ytd-engagement-panel-section-list-renderer[target-id*="description-chapters"] #details', '#structured-description ytd-horizontal-card-list-renderer ytd-macro-markers-list-item-renderer', ]; const items = document.querySelectorAll(selectors.join(', ')); const chapters = new Map(); const timeSelectors = ['.time-info', '.timestamp', '#time', 'span[id*="time"]']; const titleSelectors = ['.marker-title', '.chapter-title', '#details', 'h4', '.title']; items.forEach(item => { const timeText = extractTextFromSelectors(item, timeSelectors); const titleText = extractTextFromSelectors(item, titleSelectors); const chapter = createChapterObject(timeText, titleText); if (chapter) { chapters.set(chapter.time.toString(), chapter); } }); return Array.from(chapters.values()).sort((a, b) => a.time - b.time); }; const buildModifierParts = (ctrlKey, altKey, shiftKey) => { const parts = []; if (ctrlKey) parts.push('ctrl'); if (altKey) parts.push('alt'); if (shiftKey) parts.push('shift'); return parts; }; const getModifierValue = () => { const { ctrlKey, altKey, shiftKey } = config.shortcut; const parts = buildModifierParts(ctrlKey, altKey, shiftKey); return parts.length > 0 ? parts.join('+') : 'none'; }; const createEnableCheckbox = () => { const enableDiv = document.createElement('div'); enableDiv.className = 'ytp-plus-settings-item timecode-settings-item'; enableDiv.innerHTML = `
${t('enableDescription')}
`; return enableDiv; }; const createShortcutConfig = () => { const shortcutDiv = document.createElement('div'); shortcutDiv.className = 'ytp-plus-settings-item timecode-settings-item timecode-shortcut-item'; shortcutDiv.style.display = config.enabled ? 'flex' : 'none'; shortcutDiv.innerHTML = `
${t('shortcutDescription')}
+
`; return shortcutDiv; }; const setupEnableListener = (advancedSection, shortcutDiv) => { advancedSection.addEventListener('change', e => { const el = (e.target); if (el.matches && el.matches('.ytp-plus-settings-checkbox[data-setting="enabled"]')) { config.enabled = (el).checked; shortcutDiv.style.display = config.enabled ? 'flex' : 'none'; toggleTimecodePanel(config.enabled); saveSettings(); } }); }; const createModifierLabel = value => { if (value === 'none') return t('none'); return value .split('+') .map(k => k.charAt(0).toUpperCase() + k.slice(1)) .join('+'); }; const setupModifierSelect = modifierValue => { const native = document.getElementById('timecode-modifier-combo'); if (!native) return; const opts = [ 'none', 'ctrl', 'alt', 'shift', 'ctrl+alt', 'ctrl+shift', 'alt+shift', 'ctrl+alt+shift', ]; const factory = window.YouTubePlusHelpers?.DOM?.createCustomSelect; if (typeof factory !== 'function') return; const custom = factory(); custom.setOptions(opts.map(v => ({ value: v, text: createModifierLabel(v) }))); custom.value = modifierValue; try { native.parentNode.replaceChild(custom, native); } catch { return; } custom.addEventListener('change', () => { const value = custom.value || ''; config.shortcut.ctrlKey = value.includes('ctrl'); config.shortcut.altKey = value.includes('alt'); config.shortcut.shiftKey = value.includes('shift'); saveSettings(); }); }; const setupKeyInputListener = () => { document.getElementById('timecode-key')?.addEventListener('input', e => { const input = (e.target); if (input.value) { config.shortcut.key = input.value.toUpperCase(); saveSettings(); } }); }; const addTimecodePanelSettings = () => { const advancedSection = YouTubeUtils.querySelector( '.ytp-plus-settings-section[data-section="advanced"]' ); if (!advancedSection || YouTubeUtils.querySelector('.timecode-settings-item')) return; const modifierValue = getModifierValue(); const enableDiv = createEnableCheckbox(); const shortcutDiv = createShortcutConfig(); advancedSection.append(enableDiv, shortcutDiv); setupEnableListener(advancedSection, shortcutDiv); setupModifierSelect(modifierValue); setupKeyInputListener(); }; const insertTimecodeStyles = () => { if (document.getElementById('timecode-panel-styles')) return; const styles = ` :root{--tc-panel-bg:rgba(255,255,255,0.06);--tc-panel-border:rgba(255,255,255,0.12);--tc-panel-color:#fff} html[dark],body[dark]{--tc-panel-bg:rgba(34,34,34,0.75);--tc-panel-border:rgba(255,255,255,0.12);--tc-panel-color:#fff} html:not([dark]){--tc-panel-bg:rgba(255,255,255,0.95);--tc-panel-border:rgba(0,0,0,0.08);--tc-panel-color:#222} #timecode-panel{position:fixed;right:20px;top:80px;background:var(--tc-panel-bg);border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,0.45);width:320px;max-height:70vh;z-index:10000;color:var(--tc-panel-color);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%);border:1.5px solid var(--tc-panel-border);transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .28s;overflow:hidden;display:flex;flex-direction:column} #timecode-panel.hidden{transform:translateX(300px);opacity:0;pointer-events:none} #timecode-panel.auto-tracking{box-shadow:0 12px 48px rgba(255,0,0,0.12);border-color:rgba(255,0,0,0.25)} #timecode-header{display:flex;justify-content:space-between;align-items:center;padding:14px;border-bottom:1px solid rgba(255,255,255,0.04);background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);cursor:move} #timecode-title{font-weight:600;margin:0;font-size:15px;user-select:none;display:flex;align-items:center;gap:8px} #timecode-tracking-indicator{width:8px;height:8px;background:red;border-radius:50%;opacity:0;transition:opacity .3s} #timecode-panel.auto-tracking #timecode-tracking-indicator{opacity:1} #timecode-current-time{font-family:monospace;font-size:12px;padding:2px 6px;background:rgba(255,0,0,.3);border-radius:3px;margin-left:auto} #timecode-header-controls{display:flex;align-items:center;gap:6px} #timecode-reload,#timecode-close{background:transparent;border:none;color:inherit;cursor:pointer;width:28px;height:28px;padding:0;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:background .18s,color .18s} #timecode-reload:hover,#timecode-close:hover{background:rgba(255,255,255,0.04)} #timecode-reload.loading{animation:timecode-spin .8s linear infinite} #timecode-list{overflow-y:auto;padding:8px 0;max-height:calc(70vh - 80px);scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent} #timecode-list::-webkit-scrollbar{width:6px} #timecode-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.3);border-radius:3px} .timecode-item{padding:10px 14px;display:flex;align-items:center;cursor:pointer;transition:background-color .16s,transform .12s;border-left:3px solid transparent;position:relative;border-radius:8px;margin:6px 10px} .timecode-item:hover{background:rgba(255,255,255,0.04);transform:translateY(-2px)} .timecode-item:hover .timecode-actions{opacity:1} .timecode-item.active{background:linear-gradient(90deg, rgba(255,68,68,0.12), rgba(255,68,68,0.04));border-left-color:#ff6666;box-shadow:inset 0 0 0 1px rgba(255,68,68,0.03)} .timecode-item.active.pulse{animation:pulse .8s ease-out} .timecode-item.editing{background:linear-gradient(90deg, rgba(255,170,0,0.08), rgba(255,170,0,0.03));border-left-color:#ffaa00} .timecode-item.editing .timecode-actions{opacity:1} @keyframes pulse{0%{transform:scale(1)}50%{transform:scale(1.02)}100%{transform:scale(1)}} @keyframes timecode-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} .timecode-time{font-family:monospace;margin-right:10px;color:rgba(255,255,255,.8);font-size:13px;min-width:45px} .timecode-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;flex:1} .timecode-item.has-chapter .timecode-time{color:#ff4444} .timecode-progress{width:0;height:2px;background:#ff4444;position:absolute;bottom:0;left:0;transition:width .3s;opacity:.8} .timecode-actions{position:absolute;right:8px;top:50%;transform:translateY(-50%);display:flex;gap:4px;opacity:0;transition:opacity .2s;background:rgba(0,0,0,.8);border-radius:4px;padding:2px} .timecode-action{background:none;border:none;color:rgba(255,255,255,.8);cursor:pointer;padding:4px;font-size:12px;border-radius:2px;transition:color .2s,background-color .2s} .timecode-action:hover{color:#fff;background:rgba(255,255,255,.2)} .timecode-action.edit:hover{color:#ffaa00} .timecode-action.delete:hover{color:#ff4444} #timecode-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;text-align:center;color:rgba(255,255,255,.7);font-size:13px} #timecode-form{padding:12px;border-top:1px solid rgba(255,255,255,.04);display:none} #timecode-form.visible{display:block} #timecode-form input{width:100%;margin-bottom:8px;padding:8px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);border-radius:4px;color:#fff;font-size:13px} #timecode-form input::placeholder{color:rgba(255,255,255,.6)} #timecode-form-buttons{display:flex;gap:8px;justify-content:flex-end} #timecode-form-buttons button{padding:6px 12px;border:none;border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s} #timecode-form-cancel{background:rgba(255,255,255,.2);color:#fff} #timecode-form-cancel:hover{background:rgba(255,255,255,.3)} #timecode-form-save{background:#ff4444;color:#fff} #timecode-form-save:hover{background:#ff6666} #timecode-actions{padding:10px;border-top:1px solid rgba(255,255,255,.04);display:flex;gap:8px;background:linear-gradient(180deg,transparent,rgba(0,0,0,0.03))} #timecode-actions button{padding:8px 12px;border:none;border-radius:8px;cursor:pointer;font-size:13px;transition:background .18s;color:inherit;background:rgba(255,255,255,0.02)} #timecode-actions button:hover{background:rgba(255,255,255,0.04)} #timecode-track-toggle.active{background:linear-gradient(90deg,#ff6b6b,#ff4444);color:#fff} `; YouTubeUtils.StyleManager.add('timecode-panel-styles', styles); }; const createTimecodePanel = () => { if (state.dom.panel) return state.dom.panel; document.querySelectorAll('#timecode-panel').forEach(p => p.remove()); const panel = document.createElement('div'); panel.id = 'timecode-panel'; panel.className = config.enabled ? '' : 'hidden'; if (config.autoTrackPlayback) panel.classList.add('auto-tracking'); panel.innerHTML = `

${t('timecodes')}

${t('noTimecodesFound')}
${t('clickToAdd')}
`; state.dom = { panel, list: panel.querySelector('#timecode-list'), empty: panel.querySelector('#timecode-empty'), form: panel.querySelector('#timecode-form'), timeInput: panel.querySelector('#timecode-form-time'), labelInput: panel.querySelector('#timecode-form-label'), currentTime: panel.querySelector('#timecode-current-time'), trackToggle: panel.querySelector('#timecode-track-toggle'), reloadButton: panel.querySelector('#timecode-reload'), }; panel.addEventListener('click', handlePanelClick); makeDraggable(panel); document.body.appendChild(panel); applySavedPanelPosition(panel); return panel; }; const handlePanelClick = e => { const { target } = e; const item = target.closest('.timecode-item'); let reloadButton = null; if (target.closest) { reloadButton = target.closest('#timecode-reload'); } else if (target.id === 'timecode-reload') { reloadButton = target; } if (reloadButton) { e.preventDefault(); reloadTimecodes(reloadButton); return; } let closeButton = null; if (target.closest) { closeButton = target.closest('#timecode-close'); } else if (target.id === 'timecode-close') { closeButton = target; } if (closeButton) { toggleTimecodePanel(false); } else if (target.id === 'timecode-add-btn') { const video = YouTubeUtils.querySelector('video'); if (video) showTimecodeForm(video.currentTime); } else if (target.id === 'timecode-track-toggle') { config.autoTrackPlayback = !config.autoTrackPlayback; target.textContent = config.autoTrackPlayback ? t('tracking') : t('track'); target.classList.toggle('active', config.autoTrackPlayback); state.dom.panel.classList.toggle('auto-tracking', config.autoTrackPlayback); saveSettings(); if (config.autoTrackPlayback) startTracking(); } else if (target.id === 'timecode-export-btn') { exportTimecodes(); } else if (target.id === 'timecode-form-cancel') { hideTimecodeForm(); } else if (target.id === 'timecode-form-save') { saveTimecodeForm(); } else if (target.classList.contains('timecode-action')) { e.stopPropagation(); const { action } = target.dataset; const index = parseInt(target.closest('.timecode-item').dataset.index, 10); if (action === 'edit') { editTimecode(index); } else if (action === 'delete') { deleteTimecode(index); } } else if (item && !target.closest('.timecode-actions')) { const time = parseFloat(item.dataset.time); const video = document.querySelector('video'); if (video && !isNaN(time)) { (video).currentTime = time; if (video.paused) video.play(); updateActiveItem(item); } } }; const editTimecode = index => { const timecodes = getCurrentTimecodes(); if (index < 0 || index >= timecodes.length) return; const timecode = timecodes[index]; state.editingIndex = index; const item = state.dom.list.querySelector(`.timecode-item[data-index="${index}"]`); if (item) { item.classList.add('editing'); state.dom.list.querySelectorAll('.timecode-item.editing').forEach(el => { if (el !== item) el.classList.remove('editing'); }); } showTimecodeForm(timecode.time, timecode.label); }; const deleteTimecode = index => { const timecodes = getCurrentTimecodes(); if (index < 0 || index >= timecodes.length) return; const timecode = timecodes[index]; if (timecode.isChapter && !timecode.isUserAdded) { showNotification(t('cannotDeleteChapter')); return; } if (!confirm(t('confirmDelete').replace('{label}', timecode.label))) return; timecodes.splice(index, 1); updateTimecodePanel(timecodes); saveTimecodesToStorage(timecodes); showNotification(t('timecodeDeleted')); }; const showTimecodeForm = (currentTime, existingLabel = '') => { const { form, timeInput, labelInput } = state.dom; form.classList.add('visible'); timeInput.value = formatTime(currentTime); labelInput.value = existingLabel; requestAnimationFrame(() => labelInput.focus()); }; const hideTimecodeForm = () => { state.dom.form.classList.remove('visible'); state.editingIndex = null; state.dom.list?.querySelectorAll('.timecode-item.editing').forEach(el => { el.classList.remove('editing'); }); }; const saveTimecodeForm = () => { const { timeInput, labelInput } = state.dom; const timeValue = timeInput.value.trim(); const labelValue = labelInput.value.trim(); const time = parseTime(timeValue); if (time === null) { showNotification(t('invalidTimeFormat')); return; } const timecodes = getCurrentTimecodes(); const newTimecode = { time, label: labelValue || formatTime(time), isUserAdded: true, isChapter: false, }; if (state.editingIndex === null) { timecodes.push(newTimecode); showNotification(t('timecodeAdded')); } else { const oldTimecode = timecodes[state.editingIndex]; if (oldTimecode.isChapter && !oldTimecode.isUserAdded) { showNotification(t('cannotEditChapter')); hideTimecodeForm(); return; } timecodes[state.editingIndex] = { ...oldTimecode, ...newTimecode }; showNotification(t('timecodeUpdated')); } const sorted = timecodes.sort((a, b) => a.time - b.time); updateTimecodePanel(sorted); saveTimecodesToStorage(sorted); hideTimecodeForm(); }; const exportTimecodes = () => { const timecodes = getCurrentTimecodes(); if (!timecodes.length) { showNotification(t('noTimecodesToExport')); return; } const exportBtn = state.dom.panel?.querySelector('#timecode-export-btn'); if (exportBtn) { exportBtn.textContent = t('copied'); exportBtn.style.backgroundColor = 'rgba(0,220,0,0.8)'; setTimeout(() => { exportBtn.textContent = t('export'); exportBtn.style.backgroundColor = ''; }, 2000); } const videoTitle = document.title.replace(/\s-\sYouTube$/, ''); let content = `${videoTitle}\n\nTimecodes:\n`; timecodes.forEach(tc => { content += `${formatTime(tc.time)} - ${tc.label}\n`; }); if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(content).then(() => { showNotification(t('timecodesCopied')); }); } }; const updateTimecodePanel = timecodes => { const { list, empty } = state.dom; if (!list || !empty) return; const isEmpty = !timecodes.length; empty.style.display = isEmpty ? 'flex' : 'none'; list.style.display = isEmpty ? 'none' : 'block'; if (isEmpty) { list.innerHTML = ''; return; } list.innerHTML = timecodes .map((tc, i) => { const timeStr = formatTime(tc.time); const label = (tc.label?.trim() || timeStr).replace( /[<>&"']/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' })[c] ); const isEditable = !tc.isChapter || tc.isUserAdded; return `
${timeStr}
${label}
${ isEditable ? `
` : '' }
`; }) .join(''); }; const updateActiveItem = activeItem => { const items = state.dom.list?.querySelectorAll('.timecode-item'); if (!items) return; items.forEach(item => item.classList.remove('active', 'pulse')); if (activeItem) { activeItem.classList.add('active', 'pulse'); setTimeout(() => activeItem.classList.remove('pulse'), 800); } }; const findActiveTimecodeIndices = (items, currentVideoTime) => { let activeIndex = -1; let nextIndex = -1; for (let i = 0; i < items.length; i++) { const timeData = items[i].dataset.time; if (!timeData) continue; const time = parseFloat(timeData); if (isNaN(time)) continue; if (currentVideoTime >= time) { activeIndex = i; } else if (nextIndex === -1) { nextIndex = i; } } return { activeIndex, nextIndex }; }; const updateActiveTimecodeState = (items, activeIndex) => { if (state.activeIndex === activeIndex) return; if (state.activeIndex !== null && state.activeIndex >= 0 && items[state.activeIndex]) { items[state.activeIndex].classList.remove('active'); } if (activeIndex >= 0 && items[activeIndex]) { items[activeIndex].classList.add('active'); try { items[activeIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { items[activeIndex].scrollIntoView(false); } } state.activeIndex = activeIndex; }; const updateTimecodeProgressBar = (items, activeIndex, nextIndex, currentVideoTime) => { if (activeIndex < 0 || nextIndex < 0 || !items[activeIndex]) return; const currentTimeData = items[activeIndex].dataset.time; const nextTimeData = items[nextIndex].dataset.time; if (!currentTimeData || !nextTimeData) return; const current = parseFloat(currentTimeData); const next = parseFloat(nextTimeData); if (isNaN(current) || isNaN(next) || next <= current) return; const progress = ((currentVideoTime - current) / (next - current)) * 100; const progressEl = items[activeIndex].querySelector('.timecode-progress'); if (progressEl) { const clampedProgress = Math.min(100, Math.max(0, progress)); progressEl.style.width = `${clampedProgress}%`; } }; const shouldStopTracking = (video, panel) => { return !video || !panel || panel.classList.contains('hidden') || !config.autoTrackPlayback; }; const cancelTracking = () => { if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } }; const updateCurrentTimeDisplay = (currentTimeEl, currentTime) => { if (currentTimeEl && !isNaN(currentTime)) { currentTimeEl.textContent = formatTime(currentTime); } }; const updateTimecodeItems = (items, currentTime) => { if (!items?.length) return; const { activeIndex, nextIndex } = findActiveTimecodeIndices(items, currentTime); updateActiveTimecodeState(items, activeIndex); updateTimecodeProgressBar(items, activeIndex, nextIndex, currentTime); }; const startTracking = () => { if (state.trackingId) return; const track = () => { try { const video = document.querySelector('video'); const { panel, currentTime, list } = state.dom; if (shouldStopTracking(video, panel)) { cancelTracking(); return; } updateCurrentTimeDisplay(currentTime, video.currentTime); updateTimecodeItems(list?.querySelectorAll('.timecode-item'), video.currentTime); if (config.autoTrackPlayback) { state.trackingId = requestAnimationFrame(track); } } catch (error) { console.warn('[YouTube+][Timecode]', 'Tracking error:', error); cancelTracking(); } }; state.trackingId = requestAnimationFrame(track); }; const stopTracking = () => { if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } }; const makeDraggable = panel => { const header = panel.querySelector('#timecode-header'); if (!header) return; let startX, startY, startLeft, startTop; const mouseDownHandler = e => { if (e.button !== 0) return; state.dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); if (!panel.style.left) { panel.style.left = `${rect.left}px`; } if (!panel.style.top) { panel.style.top = `${rect.top}px`; } panel.style.right = 'auto'; startLeft = parseFloat(panel.style.left) || rect.left; startTop = parseFloat(panel.style.top) || rect.top; const handleMove = event => { if (!state.dragging) return; const deltaX = event.clientX - startX; const deltaY = event.clientY - startY; const { left, top } = clampPanelPosition(panel, startLeft + deltaX, startTop + deltaY); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = 'auto'; }; const handleUp = () => { if (!state.dragging) return; state.dragging = false; document.removeEventListener('mousemove', handleMove); document.removeEventListener('mouseup', handleUp); const rectAfter = panel.getBoundingClientRect(); const { left, top } = clampPanelPosition(panel, rectAfter.left, rectAfter.top); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = 'auto'; savePanelPosition(left, top); }; document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleUp); }; YouTubeUtils.cleanupManager.registerListener(header, 'mousedown', mouseDownHandler); }; const saveTimecodesToStorage = timecodes => { const videoId = new URLSearchParams(window.location.search).get('v'); if (!videoId) return; try { const minimal = timecodes.map(tc => ({ t: tc.time, l: tc.label?.trim() || formatTime(tc.time), c: tc.isChapter || false, u: tc.isUserAdded || false, })); localStorage.setItem(`yt_tc_${videoId}`, JSON.stringify(minimal)); } catch {} }; const loadTimecodesFromStorage = () => { const videoId = new URLSearchParams(window.location.search).get('v'); if (!videoId) return null; try { const data = localStorage.getItem(`yt_tc_${videoId}`); return data ? JSON.parse(data) .map(tc => ({ time: tc.t, label: tc.l, isChapter: tc.c, isUserAdded: tc.u || false, })) .sort((a, b) => a.time - b.time) : null; } catch { return null; } }; const getCurrentTimecodes = () => { const items = state.dom.list?.querySelectorAll('.timecode-item'); if (!items) return []; return Array.from(items) .map(item => ({ time: parseFloat(item.dataset.time), label: item.querySelector('.timecode-label')?.textContent || formatTime(parseFloat(item.dataset.time)), isChapter: item.classList.contains('has-chapter'), isUserAdded: !item.classList.contains('has-chapter') || false, })) .sort((a, b) => a.time - b.time); }; const toggleTimecodePanel = show => { document.querySelectorAll('#timecode-panel').forEach(panel => { if (panel !== state.dom.panel) panel.remove(); }); const panel = state.dom.panel || createTimecodePanel(); const shouldShow = show === undefined ? panel.classList.contains('hidden') : show; panel.classList.toggle('hidden', !shouldShow); if (shouldShow) { applySavedPanelPosition(panel); const saved = loadTimecodesFromStorage(); if (saved?.length) { updateTimecodePanel(saved); } else if (config.autoDetect) { detectTimecodes().catch(err => console.error('[YouTube+][Timecode]', 'Detection failed:', err) ); } if (config.autoTrackPlayback) startTracking(); } else if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } }; const resetTimecodeState = () => { state.activeIndex = null; state.editingIndex = null; state.timecodes.clear(); }; const updatePanelOnNavigation = () => { const saved = loadTimecodesFromStorage(); if (saved?.length) { updateTimecodePanel(saved); } else if (config.autoDetect) { setTimeout( () => detectTimecodes().catch(err => console.error('[YouTube+][Timecode]', 'Detection failed:', err) ), 500 ); } if (config.autoTrackPlayback) startTracking(); }; const shouldUpdatePanel = () => { return config.enabled && state.dom.panel && !state.dom.panel.classList.contains('hidden'); }; const setupNavigationObserver = (currentVideoId, handleNavigationChange) => { const observer = new MutationObserver(() => { const newVideoId = getCurrentVideoId(); if (newVideoId !== currentVideoId) { handleNavigationChange(); } }); YouTubeUtils.cleanupManager.registerObserver(observer); if (document.body) { observer.observe(document.body, { subtree: true, childList: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { subtree: true, childList: true }); }); } }; const setupNavigation = () => { let currentVideoId = getCurrentVideoId(); const handleNavigationChange = () => { const newVideoId = getCurrentVideoId(); if (newVideoId === currentVideoId || window.location.pathname !== '/watch') return; currentVideoId = newVideoId; resetTimecodeState(); if (shouldUpdatePanel()) { updatePanelOnNavigation(); } }; document.addEventListener('yt-navigate-finish', handleNavigationChange); setupNavigationObserver(currentVideoId, handleNavigationChange); }; const setupKeyboard = () => { document.addEventListener('keydown', e => { if (!config.enabled) return; const { target } = e; const el = (target); if (el.matches && el.matches('input, textarea, [contenteditable]')) return; const { key, shiftKey, altKey, ctrlKey } = config.shortcut; if ( e.key.toUpperCase() === key && e.shiftKey === shiftKey && e.altKey === altKey && e.ctrlKey === ctrlKey ) { e.preventDefault(); toggleTimecodePanel(); } }); }; const cleanup = () => { stopTracking(); if (state.dom.panel) { state.dom.panel.remove(); state.dom.panel = null; } }; const setupSettingsObserver = () => { const observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { setTimeout(addTimecodePanelSettings, 100); return; } } } if ( document.querySelector( '.ytp-plus-settings-section[data-section="advanced"]:not(.hidden)' ) && !document.querySelector('.timecode-settings-item') ) { setTimeout(addTimecodePanelSettings, 50); } }); YouTubeUtils.cleanupManager.registerObserver(observer); return observer; }; const startObserving = observer => { const observerConfig = { childList: true, subtree: true, attributes: true, attributeFilter: ['class'], }; if (document.body) { observer.observe(document.body, observerConfig); } else { document.addEventListener( 'DOMContentLoaded', () => { observer.observe(document.body, observerConfig); }, { once: true } ); } }; const setupSettingsClickHandler = () => { const clickHandler = e => { const { target } = e; const el = (target); if ( el.classList?.contains('ytp-plus-settings-nav-item') && el.dataset.section === 'advanced' ) { setTimeout(addTimecodePanelSettings, 50); } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, true); }; const setupResizeHandler = () => { if (!config.enabled || state.resizeListenerKey) return; const onResize = YouTubeUtils.throttle(() => { if (!state.dom.panel) return; const rect = state.dom.panel.getBoundingClientRect(); const { left, top } = clampPanelPosition(state.dom.panel, rect.left, rect.top); state.dom.panel.style.left = `${left}px`; state.dom.panel.style.top = `${top}px`; state.dom.panel.style.right = 'auto'; savePanelPosition(left, top); }, 200); state.resizeListenerKey = YouTubeUtils.cleanupManager.registerListener( window, 'resize', onResize ); }; const init = () => { if (initStarted) return; const appRoot = (typeof YouTubeUtils?.querySelector === 'function' && YouTubeUtils.querySelector('ytd-app')) || document.querySelector('ytd-app'); if (!appRoot) { scheduleInitRetry(); return; } initStarted = true; loadSettings(); insertTimecodeStyles(); setupKeyboard(); setupNavigation(); const observer = setupSettingsObserver(); startObserving(observer); setupSettingsClickHandler(); setupResizeHandler(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } window.addEventListener('beforeunload', cleanup); })(); (function () { 'use strict'; if (window._playlistSearchInitialized) return; window._playlistSearchInitialized = true; const _globalI18n_playlist = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n_playlist && typeof _globalI18n_playlist.t === 'function') { return _globalI18n_playlist.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; const throttle = (func, limit) => { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; }; const config = { enabled: true, storageKey: 'youtube_playlist_search_settings', searchDebounceMs: 200, observerThrottleMs: 500, maxPlaylistItems: 5000, maxQueryLength: 200, }; const state = { searchInput: null, searchResults: null, originalItems: [], currentPlaylistId: null, mutationObserver: null, rafId: null, itemsCache: new Map(), }; const ITEM_TITLE_SELECTOR = '#video-title'; const ITEM_BYLINE_SELECTOR = '#byline'; const loadSettings = () => { try { const saved = localStorage.getItem(config.storageKey); if (saved) Object.assign(config, JSON.parse(saved)); } catch (error) { console.warn('[YouTube+][Playlist Search]', 'Failed to load settings:', error); } }; const getCurrentPlaylistId = () => { try { const urlParams = new URLSearchParams(window.location.search); const listId = urlParams.get('list'); if (listId && /^[a-zA-Z0-9_-]+$/.test(listId)) { return listId; } return null; } catch (error) { console.warn('[YouTube+][Playlist Search]', 'Failed to get playlist ID:', error); return null; } }; const sanitizeTitle = (title, maxLength = 100) => { const trimmed = title.trim(); return trimmed.length > maxLength ? `${trimmed.substring(0, maxLength)}...` : trimmed; }; const findTitleBySelectors = (playlistPanel, selectors) => { for (const selector of selectors) { const el = playlistPanel?.querySelector(selector) || document.querySelector(selector); if (el && el.textContent && el.textContent.trim()) { return sanitizeTitle(el.textContent); } } return null; }; const getTitleFromMeta = () => { const meta = document.querySelector('meta[name="title"]') || document.querySelector('meta[property="og:title"]'); return meta && meta.content ? sanitizeTitle(meta.content) : null; }; const getPlaylistDisplayName = (playlistPanel, listId) => { try { const titleSelectors = [ '.title', 'h3 a', '#header-title', '#title', '.playlist-title', 'h1.title', ]; const titleFromSelectors = findTitleBySelectors(playlistPanel, titleSelectors); if (titleFromSelectors) return titleFromSelectors; const titleFromMeta = getTitleFromMeta(); if (titleFromMeta) return titleFromMeta; } catch (error) { console.warn('[YouTube+][Playlist Search]', 'Failed to get display name:', error); } return listId && typeof listId === 'string' ? listId.substring(0, 50) : 'playlist'; }; const setupPanelObserver = observer => { try { if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener( 'DOMContentLoaded', () => { try { observer.observe(document.body, { childList: true, subtree: true }); } catch (observeError) { console.error( '[YouTube+][PlaylistSearch] observer.observe failed after DOMContentLoaded:', observeError ); } }, { once: true } ); } } catch (observeError) { console.error('[YouTube+][PlaylistSearch] observer.observe failed:', observeError); } }; const waitForPlaylistPanel = () => { const observer = new MutationObserver((_mutations, obs) => { const panel = document.querySelector('ytd-playlist-panel-renderer'); if (panel) { try { obs.disconnect(); } catch {} addSearchUI(); } }); setupPanelObserver(observer); setTimeout(() => { try { observer.disconnect(); } catch {} }, 5000); }; const addSearchUI = () => { if (!config.enabled) return; const playlistId = getCurrentPlaylistId(); if (!playlistId) return; const playlistPanel = document.querySelector('ytd-playlist-panel-renderer'); if (!playlistPanel) { waitForPlaylistPanel(); return; } if (playlistPanel.querySelector('.ytplus-playlist-search')) return; state.currentPlaylistId = playlistId; const searchContainer = document.createElement('div'); searchContainer.className = 'ytplus-playlist-search'; searchContainer.style.cssText = ` padding: 8px 16px; background: transparent; border-bottom: 1px solid var(--yt-spec-10-percent-layer); position: sticky; top: 0; z-index: 1; `; const searchInput = document.createElement('input'); searchInput.type = 'text'; const playlistName = getPlaylistDisplayName(playlistPanel, playlistId); searchInput.placeholder = t('searchPlaceholder').replace('{playlist}', playlistName); searchInput.className = 'ytplus-playlist-search-input'; searchInput.style.cssText = ` width: 93%; padding: 8px 16px; border: 1px solid var(--yt-spec-10-percent-layer); border-radius: 20px; background: var(--yt-spec-badge-chip-background); color: var(--yt-spec-text-primary); font-size: 14px; font-family: 'Roboto', Arial, sans-serif; outline: none; transition: border-color 0.2s; `; searchInput.addEventListener('focus', () => { searchInput.style.borderColor = 'var(--yt-spec-call-to-action)'; }); searchInput.addEventListener('blur', () => { searchInput.style.borderColor = 'var(--yt-spec-10-percent-layer)'; }); const debouncedFilter = debounce(value => { filterPlaylistItems(value); }, config.searchDebounceMs); searchInput.addEventListener('input', e => { const { target } = e; const inputEl = (target); const { value } = inputEl; debouncedFilter(value); }); searchContainer.appendChild(searchInput); state.searchInput = searchInput; const rawItemsContainer = playlistPanel.querySelector('.playlist-items.style-scope.ytd-playlist-panel-renderer') || playlistPanel.querySelector('.playlist-items') || playlistPanel.querySelector('#items'); if (rawItemsContainer) { const itemsContainer = ( (rawItemsContainer) ); const firstVideo = itemsContainer.querySelector('ytd-playlist-panel-video-renderer'); if (firstVideo && firstVideo.parentElement === itemsContainer) { itemsContainer.insertBefore(searchContainer, (firstVideo)); } else { itemsContainer.appendChild(searchContainer); } } else { playlistPanel.insertBefore(searchContainer, playlistPanel.firstChild); } collectOriginalItems(); setupPlaylistObserver(); }; const handlePlaylistMutations = throttle(() => { const currentCount = state.originalItems.length; const newItems = document.querySelectorAll( 'ytd-playlist-panel-renderer ytd-playlist-panel-video-renderer' ); if (Math.abs(newItems.length - currentCount) > 0) { collectOriginalItems(); if (state.searchInput && state.searchInput.value) { filterPlaylistItems(state.searchInput.value); } } }, config.observerThrottleMs); const setupBodyObserverFallback = () => { const bodyObserver = new MutationObserver((_mutations, obs) => { const panel = document.querySelector('ytd-playlist-panel-renderer'); if (panel) { try { state.mutationObserver.observe(panel, { childList: true, subtree: true }); } catch (err) { console.warn( '[YouTube+][Playlist Search] Failed to observe playlist panel after fallback:', err ); } obs.disconnect(); } }); try { bodyObserver.observe(document.body, { childList: true, subtree: true }); setTimeout(() => bodyObserver.disconnect(), 5000); } catch (err) { console.warn( '[YouTube+][Playlist Search] Failed to observe document.body for playlist fallback:', err ); } }; const observePlaylistPanel = playlistPanel => { try { if (playlistPanel && playlistPanel instanceof Element && playlistPanel.isConnected) { state.mutationObserver.observe(playlistPanel, { childList: true, subtree: true }); } else if (document.body) { setupBodyObserverFallback(); } } catch (observeError) { console.error( '[YouTube+][Playlist Search] Failed to set up playlist observer:', observeError ); } }; const setupPlaylistObserver = () => { if (state.mutationObserver) { state.mutationObserver.disconnect(); } const playlistPanel = document.querySelector('ytd-playlist-panel-renderer'); if (!playlistPanel) return; state.mutationObserver = new MutationObserver(handlePlaylistMutations); observePlaylistPanel(playlistPanel); }; const getPlaylistItems = () => { return document.querySelectorAll( 'ytd-playlist-panel-renderer ytd-playlist-panel-video-renderer' ); }; const limitPlaylistItems = items => { if (items.length > config.maxPlaylistItems) { console.warn( `[YouTube+][Playlist Search] Playlist has ${items.length} items, limiting to ${config.maxPlaylistItems}` ); } return Array.from(items).slice(0, config.maxPlaylistItems); }; const extractItemData = (item, index) => { const videoId = item.getAttribute('video-id') || `item-${index}`; const titleEl = item.querySelector(ITEM_TITLE_SELECTOR); const bylineEl = item.querySelector(ITEM_BYLINE_SELECTOR); return { element: item, videoId, title: titleEl?.textContent?.trim()?.toLowerCase() || '', channel: bylineEl?.textContent?.trim()?.toLowerCase() || '', }; }; const getCachedItemData = (item, index) => { const videoId = item.getAttribute('video-id') || `item-${index}`; if (state.itemsCache.has(videoId)) { return state.itemsCache.get(videoId); } const itemData = extractItemData(item, index); state.itemsCache.set(videoId, itemData); return itemData; }; const collectOriginalItems = () => { const items = getPlaylistItems(); state.itemsCache.clear(); const itemsArray = limitPlaylistItems(items); state.originalItems = itemsArray.map((item, index) => getCachedItemData(item, index)); }; const filterPlaylistItems = query => { if (state.rafId) { cancelAnimationFrame(state.rafId); } if (query && typeof query !== 'string') { console.warn('[YouTube+][Playlist Search]', 'Invalid query type'); return; } let processedQuery = query; if (processedQuery && processedQuery.length > config.maxQueryLength) { processedQuery = processedQuery.substring(0, config.maxQueryLength); } if (!processedQuery || processedQuery.trim() === '') { state.rafId = requestAnimationFrame(() => { state.originalItems.forEach(item => { item.element.style.display = ''; }); state.rafId = null; }); return; } const searchTerm = processedQuery.toLowerCase().trim(); let visibleCount = 0; state.rafId = requestAnimationFrame(() => { const updates = []; state.originalItems.forEach(item => { const matches = item.title.includes(searchTerm) || item.channel.includes(searchTerm); if (matches) { if (item.element.style.display === 'none') { updates.push({ element: item.element, display: '' }); } visibleCount++; } else if (item.element.style.display !== 'none') { updates.push({ element: item.element, display: 'none' }); } }); updates.forEach(update => { update.element.style.display = update.display; }); updateResultsCount(visibleCount, state.originalItems.length); state.rafId = null; }); }; const updateResultsCount = (visible, total) => { console.log('[YouTube+][Playlist Search]', `Showing ${visible} of ${total} videos`); }; const cleanup = () => { const searchUI = document.querySelector('.ytplus-playlist-search'); if (searchUI) { searchUI.remove(); } if (state.mutationObserver) { state.mutationObserver.disconnect(); state.mutationObserver = null; } if (state.rafId) { cancelAnimationFrame(state.rafId); state.rafId = null; } state.itemsCache.clear(); state.searchInput = null; state.originalItems = []; state.currentPlaylistId = null; }; const handleNavigation = () => { cleanup(); setTimeout(addSearchUI, 300); }; const init = () => { loadSettings(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addSearchUI, { once: true }); } else { addSearchUI(); } document.addEventListener('yt-navigate-finish', handleNavigation); window.addEventListener('beforeunload', cleanup); }; if (typeof window !== 'undefined') { window.YouTubePlaylistSearch = { init, cleanup, version: '2.2', }; } init(); })(); (function () { 'use strict'; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') { return _globalI18n.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; function extractVideoId(thumbnailSrc) { try { if (!thumbnailSrc || typeof thumbnailSrc !== 'string') return null; const match = thumbnailSrc.match(/\/vi\/([^\/]+)\//); const videoId = match ? match[1] : null; if (videoId && !/^[a-zA-Z0-9_-]{11}$/.test(videoId)) { console.warn('[YouTube+][Thumbnail]', 'Invalid video ID format:', videoId); return null; } return videoId; } catch (error) { console.error('[YouTube+][Thumbnail]', 'Error extracting video ID:', error); return null; } } function extractShortsId(href) { try { if (!href || typeof href !== 'string') return null; const match = href.match(/\/shorts\/([^\/\?]+)/); const shortsId = match ? match[1] : null; if (shortsId && !/^[a-zA-Z0-9_-]{11}$/.test(shortsId)) { console.warn('[YouTube+][Thumbnail]', 'Invalid shorts ID format:', shortsId); return null; } return shortsId; } catch (error) { console.error('[YouTube+][Thumbnail]', 'Error extracting shorts ID:', error); return null; } } function isValidUrlString(url) { if (!url || typeof url !== 'string') { console.warn('[YouTube+][Thumbnail]', 'Invalid URL provided'); return false; } return true; } function hasValidProtocol(parsedUrl) { if (parsedUrl.protocol !== 'https:') { console.warn('[YouTube+][Thumbnail]', 'Only HTTPS URLs are allowed'); return false; } return true; } function hasValidDomain(parsedUrl) { const { hostname } = parsedUrl; if (!hostname.endsWith('ytimg.com') && !hostname.endsWith('youtube.com')) { console.warn('[YouTube+][Thumbnail]', 'Only YouTube image domains are allowed'); return false; } return true; } function parseAndValidateUrl(url) { try { const parsedUrl = new URL(url); if (!hasValidProtocol(parsedUrl)) return null; if (!hasValidDomain(parsedUrl)) return null; return parsedUrl; } catch (error) { console.error('[YouTube+][Thumbnail]', 'Invalid URL:', error); return null; } } async function checkViaHeadRequest(url) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); try { const response = await fetch(url, { method: 'HEAD', signal: controller.signal, }).catch(() => null); clearTimeout(timeoutId); return response ? response.ok : true; } catch { clearTimeout(timeoutId); return null; } } function cleanupImageElement(img) { if (img.parentNode) { document.body.removeChild(img); } } function checkViaImageLoad(url) { return new Promise(resolve => { const img = document.createElement('img'); img.style.display = 'none'; const timeout = setTimeout(() => { cleanupImageElement(img); resolve(false); }, 3000); img.onload = () => { clearTimeout(timeout); cleanupImageElement(img); resolve(true); }; img.onerror = () => { clearTimeout(timeout); cleanupImageElement(img); resolve(false); }; document.body.appendChild(img); img.src = url; }); } async function checkImageExists(url) { try { if (!isValidUrlString(url)) return false; const parsedUrl = parseAndValidateUrl(url); if (!parsedUrl) return false; const headResult = await checkViaHeadRequest(url); if (headResult !== null) return headResult; return await checkViaImageLoad(url); } catch (error) { console.error('[YouTube+][Thumbnail]', 'Error checking image:', error); return false; } } function createSpinner() { const spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); spinner.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); spinner.setAttribute('width', '16'); spinner.setAttribute('height', '16'); spinner.setAttribute('viewBox', '0 0 24 24'); spinner.setAttribute('fill', 'none'); spinner.setAttribute('stroke', 'white'); spinner.setAttribute('stroke-width', '2'); spinner.setAttribute('stroke-linecap', 'round'); spinner.setAttribute('stroke-linejoin', 'round'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M21 12a9 9 0 1 1-6.219-8.56'); spinner.appendChild(path); spinner.style.animation = 'spin 1s linear infinite'; if (!document.querySelector('#spinner-keyframes')) { const style = document.createElement('style'); style.id = 'spinner-keyframes'; style.textContent = ` @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `; (document.head || document.documentElement).appendChild(style); } return spinner; } function isValidVideoId(videoId) { return videoId && typeof videoId === 'string' && /^[a-zA-Z0-9_-]{11}$/.test(videoId); } function isValidOverlayElement(overlayElement) { return overlayElement && overlayElement instanceof HTMLElement; } function getShortsThumbnailUrls(videoId) { return { primary: `https://i.ytimg.com/vi/${videoId}/oardefault.jpg`, fallback: `https://i.ytimg.com/vi/${videoId}/oar2.jpg`, }; } function getVideoThumbnailUrls(videoId) { return { primary: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`, fallback: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`, }; } async function loadAndShowThumbnail(videoId, isShorts) { const urls = isShorts ? getShortsThumbnailUrls(videoId) : getVideoThumbnailUrls(videoId); const isPrimaryAvailable = await checkImageExists(urls.primary); showImageModal(isPrimaryAvailable ? urls.primary : urls.fallback); } function replaceWithSpinner(overlayElement, originalSvg) { const spinner = createSpinner(); overlayElement.replaceChild(spinner, originalSvg); return spinner; } function restoreOriginalSvg(overlayElement, spinner, originalSvg) { try { if (spinner && spinner.parentNode) { overlayElement.replaceChild(originalSvg, spinner); } } catch (restoreError) { console.error('[YouTube+][Thumbnail]', 'Error restoring original SVG:', restoreError); if (spinner && spinner.parentNode) { spinner.parentNode.removeChild(spinner); } } } async function openThumbnail(videoId, isShorts, overlayElement) { try { if (!isValidVideoId(videoId)) { console.error('[YouTube+][Thumbnail]', 'Invalid video ID:', videoId); return; } if (!isValidOverlayElement(overlayElement)) { console.error('[YouTube+][Thumbnail]', 'Invalid overlay element'); return; } const originalSvg = overlayElement.querySelector('svg'); if (!originalSvg) { console.warn('[YouTube+][Thumbnail]', 'No SVG found in overlay element'); return; } const spinner = replaceWithSpinner(overlayElement, originalSvg); try { await loadAndShowThumbnail(videoId, isShorts); } finally { restoreOriginalSvg(overlayElement, spinner, originalSvg); } } catch (error) { console.error('[YouTube+][Thumbnail]', 'Error opening thumbnail:', error); } } (function addThumbnailStyles() { try { const css = ` :root { --thumbnail-btn-bg-light: rgba(255, 255, 255, 0.85); --thumbnail-btn-bg-dark: rgba(0, 0, 0, 0.7); --thumbnail-btn-hover-bg-light: rgba(255, 255, 255, 1); --thumbnail-btn-hover-bg-dark: rgba(0, 0, 0, 0.9); --thumbnail-btn-color-light: #222; --thumbnail-btn-color-dark: #fff; --thumbnail-modal-bg-light: rgba(255, 255, 255, 0.95); --thumbnail-modal-bg-dark: rgba(34, 34, 34, 0.85); --thumbnail-modal-title-light: #222; --thumbnail-modal-title-dark: #fff; --thumbnail-modal-btn-bg-light: rgba(0, 0, 0, 0.08); --thumbnail-modal-btn-bg-dark: rgba(255, 255, 255, 0.08); --thumbnail-modal-btn-hover-bg-light: rgba(0, 0, 0, 0.18); --thumbnail-modal-btn-hover-bg-dark: rgba(255, 255, 255, 0.18); --thumbnail-modal-btn-color-light: #222; --thumbnail-modal-btn-color-dark: #fff; --thumbnail-modal-btn-hover-color-light: #ff4444; --thumbnail-modal-btn-hover-color-dark: #ff4444; --thumbnail-glass-blur: blur(18px) saturate(180%); --thumbnail-glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); --thumbnail-glass-border: rgba(255, 255, 255, 0.2); } html[dark], body[dark] { --thumbnail-btn-bg: var(--thumbnail-btn-bg-dark); --thumbnail-btn-hover-bg: var(--thumbnail-btn-hover-bg-dark); --thumbnail-btn-color: var(--thumbnail-btn-color-dark); --thumbnail-modal-bg: var(--thumbnail-modal-bg-dark); --thumbnail-modal-title: var(--thumbnail-modal-title-dark); --thumbnail-modal-btn-bg: var(--thumbnail-modal-btn-bg-dark); --thumbnail-modal-btn-hover-bg: var(--thumbnail-modal-btn-hover-bg-dark); --thumbnail-modal-btn-color: var(--thumbnail-modal-btn-color-dark); --thumbnail-modal-btn-hover-color: var(--thumbnail-modal-btn-hover-color-dark); } html:not([dark]) { --thumbnail-btn-bg: var(--thumbnail-btn-bg-light); --thumbnail-btn-bg: var(--thumbnail-btn-bg-light); --thumbnail-btn-hover-bg: var(--thumbnail-btn-hover-bg-light); --thumbnail-btn-color: var(--thumbnail-btn-color-light); --thumbnail-modal-bg: var(--thumbnail-modal-bg-light); --thumbnail-modal-title: var(--thumbnail-modal-title-light); --thumbnail-modal-btn-bg: var(--thumbnail-modal-btn-bg-light); --thumbnail-modal-btn-hover-bg: var(--thumbnail-modal-btn-hover-bg-light); --thumbnail-modal-btn-color: var(--thumbnail-modal-btn-color-light); --thumbnail-modal-btn-hover-color: var(--thumbnail-modal-btn-hover-color-light); } .thumbnail-overlay-container { position: absolute; bottom: 8px; left: 8px; z-index: 9999; opacity: 0; transition: opacity 0.2s ease; } .thumbnail-overlay-button { width: 28px; height: 28px; background: var(--thumbnail-btn-bg); border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--thumbnail-btn-color); position: relative; box-shadow: var(--thumbnail-glass-shadow); backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); } .thumbnail-overlay-button:hover { background: var(--thumbnail-btn-hover-bg); } .thumbnail-dropdown { position: absolute; bottom: 100%; left: 0; background: var(--thumbnail-btn-hover-bg); border-radius: 8px; padding: 4px; margin-bottom: 4px; display: none; flex-direction: column; min-width: 140px; box-shadow: var(--thumbnail-glass-shadow); z-index: 10000; backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); } .thumbnail-dropdown.show { display: flex !important; } .thumbnail-dropdown-item { background: none; border: none; color: var(--thumbnail-btn-color); padding: 8px 12px; cursor: pointer; border-radius: 4px; font-size: 12px; text-align: left; white-space: nowrap; transition: background-color 0.2s ease; } .thumbnail-dropdown-item:hover { background: rgba(255,255,255,0.06); } .thumbnailPreview-button { position: absolute; bottom: 10px; left: 5px; background-color: var(--thumbnail-btn-bg); color: var(--thumbnail-btn-color); border: none; border-radius: 6px; padding: 3px; font-size: 18px; cursor: pointer; z-index: 2000; opacity: 0; transition: opacity 0.3s; display: flex; align-items: center; justify-content: center; box-shadow: var(--thumbnail-glass-shadow); backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); } .thumbnailPreview-container { position: relative; } .thumbnailPreview-container:hover .thumbnailPreview-button { opacity: 1; } .thumbnail-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.55); z-index: 100000; display: flex; align-items: center; justify-content: center; animation: fadeInModal 0.22s cubic-bezier(.4,0,.2,1); backdrop-filter: blur(8px) saturate(140%); -webkit-backdrop-filter: blur(8px) saturate(140%); } .thumbnail-modal-content { background: var(--thumbnail-modal-bg); border-radius: 20px; box-shadow: 0 12px 40px rgba(0,0,0,0.45); max-width: 78vw; max-height: 90vh; overflow: auto; position: relative; display: flex; flex-direction: column; align-items: center; animation: scaleInModal 0.22s cubic-bezier(.4,0,.2,1); border: 1.5px solid var(--thumbnail-glass-border); backdrop-filter: blur(14px) saturate(150%); -webkit-backdrop-filter: blur(14px) saturate(150%);} .thumbnail-modal-wrapper { display: flex; align-items: flex-start; gap: 12px; } .thumbnail-modal-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; } .thumbnail-modal-action-btn { width: 40px; height: 40px; border-radius: 50%; background: var(--thumbnail-modal-btn-bg); border: 1px solid rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,0.2); transition: transform 0.12s ease, background 0.12s ease; color: var(--thumbnail-modal-btn-color); } .thumbnail-modal-action-btn:hover { transform: translateY(-2px); } .thumbnail-modal-close { } .thumbnail-modal-open { } .thumbnail-modal-img { max-width: 72vw; max-height: 70vh; box-shadow: var(--thumbnail-glass-shadow); background: #222; border: 1px solid var(--thumbnail-glass-border); } .thumbnail-modal-options { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; } .thumbnail-modal-option-btn { background: var(--thumbnail-modal-btn-bg); color: var(--thumbnail-modal-btn-color); border: none; border-radius: 8px; padding: 8px 18px; font-size: 14px; cursor: pointer; transition: background 0.2s; margin-bottom: 6px; box-shadow: var(--thumbnail-glass-shadow); backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); } .thumbnail-modal-option-btn:hover { background: var(--thumbnail-modal-btn-hover-bg); color: var(--thumbnail-modal-btn-hover-color); } .thumbnail-modal-title { font-size: 18px; font-weight: 600; color: var(--thumbnail-modal-title); margin-bottom: 10px; text-align: center; text-shadow: 0 2px 8px rgba(0,0,0,0.15); } @keyframes fadeInModal { from { opacity: 0; } to { opacity: 1; } } @keyframes scaleInModal { from { transform: scale(0.95); } to { transform: scale(1); } } `; if ( window.YouTubeUtils && YouTubeUtils.StyleManager && typeof YouTubeUtils.StyleManager.add === 'function' ) { YouTubeUtils.StyleManager.add('thumbnail-viewer-styles', css); } else { const s = document.createElement('style'); s.id = 'ytplus-thumbnail-styles'; s.textContent = css; (document.head || document.documentElement).appendChild(s); } } catch { if (!document.getElementById('ytplus-thumbnail-styles')) { const s = document.createElement('style'); s.id = 'ytplus-thumbnail-styles'; s.textContent = '.thumbnail-modal-img{max-width:72vw;max-height:70vh;}'; (document.head || document.documentElement).appendChild(s); } } })(); function validateModalUrl(url) { if (!url || typeof url !== 'string') { console.error('[YouTube+][Thumbnail]', 'Invalid URL provided to modal'); return false; } try { const parsedUrl = new URL(url); if (parsedUrl.protocol !== 'https:') { console.error('[YouTube+][Thumbnail]', 'Only HTTPS URLs are allowed'); return false; } const allowedDomains = ['ytimg.com', 'youtube.com', 'ggpht.com', 'googleusercontent.com']; if (!allowedDomains.some(d => parsedUrl.hostname.endsWith(d))) { console.error('[YouTube+][Thumbnail]', 'Image domain not allowed:', parsedUrl.hostname); return false; } return true; } catch (urlError) { console.error('[YouTube+][Thumbnail]', 'Invalid URL format:', urlError); return false; } } function createModalImage(url) { const img = document.createElement('img'); img.className = 'thumbnail-modal-img'; img.src = url; img.alt = t('thumbnailPreview'); img.title = ''; img.style.cursor = 'pointer'; img.addEventListener('click', () => window.open(img.src, '_blank')); return img; } function createCloseButton(overlay) { const closeBtn = document.createElement('button'); closeBtn.className = 'thumbnail-modal-close thumbnail-modal-action-btn'; closeBtn.innerHTML = `\n \n \n \n `; closeBtn.title = t('close'); closeBtn.setAttribute('aria-label', t('close')); closeBtn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); overlay.remove(); }); return closeBtn; } function createNewTabButton(img) { const newTabBtn = document.createElement('button'); newTabBtn.className = 'thumbnail-modal-open thumbnail-modal-action-btn'; newTabBtn.innerHTML = `\n \n \n \n \n \n `; newTabBtn.title = t('clickToOpen'); newTabBtn.setAttribute('aria-label', t('clickToOpen')); newTabBtn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); window.open(img.src, '_blank'); }); return newTabBtn; } async function downloadImageAsBlob(imgSrc) { const response = await fetch(imgSrc); if (!response.ok) throw new Error('Network response was not ok'); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; try { const urlObj = new URL(imgSrc); const segments = urlObj.pathname.split('/'); a.download = segments[segments.length - 1] || 'thumbnail.jpg'; } catch { a.download = 'thumbnail.jpg'; } document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(blobUrl), 1500); } function createDownloadButton(img) { const downloadBtn = document.createElement('button'); downloadBtn.className = 'thumbnail-modal-download thumbnail-modal-action-btn'; downloadBtn.innerHTML = `\n \n \n \n \n \n `; downloadBtn.title = t('download'); downloadBtn.setAttribute('aria-label', t('download')); downloadBtn.addEventListener('click', async e => { e.preventDefault(); e.stopPropagation(); try { await downloadImageAsBlob(img.src); } catch { window.open(img.src, '_blank'); } }); return downloadBtn; } function setupModalKeyboard(overlay) { function escHandler(e) { if (e.key === 'Escape') { overlay.remove(); window.removeEventListener('keydown', escHandler, true); } } window.addEventListener('keydown', escHandler, true); } function setupImageErrorHandler(img, content) { img.addEventListener('error', () => { const err = document.createElement('div'); err.textContent = t('thumbnailLoadFailed'); err.style.color = 'white'; content.appendChild(err); }); } function showImageModal(url) { try { if (!validateModalUrl(url)) return; document.querySelectorAll('.thumbnail-modal-overlay').forEach(m => m.remove()); const overlay = document.createElement('div'); overlay.className = 'thumbnail-modal-overlay'; const content = document.createElement('div'); content.className = 'thumbnail-modal-content'; const img = createModalImage(url); const optionsDiv = document.createElement('div'); optionsDiv.className = 'thumbnail-modal-options'; const closeBtn = createCloseButton(overlay); const newTabBtn = createNewTabButton(img); const downloadBtn = createDownloadButton(img); content.appendChild(img); content.appendChild(optionsDiv); const wrapper = document.createElement('div'); wrapper.className = 'thumbnail-modal-wrapper'; const actionsDiv = document.createElement('div'); actionsDiv.className = 'thumbnail-modal-actions'; actionsDiv.appendChild(closeBtn); actionsDiv.appendChild(newTabBtn); actionsDiv.appendChild(downloadBtn); wrapper.appendChild(content); wrapper.appendChild(actionsDiv); overlay.appendChild(wrapper); overlay.addEventListener('click', ({ target }) => { if (target === overlay) overlay.remove(); }); setupModalKeyboard(overlay); setupImageErrorHandler(img, content); document.body.appendChild(overlay); } catch (error) { console.error('[YouTube+][Thumbnail]', 'Error showing modal:', error); } } let thumbnailPreviewCurrentVideoId = ''; let thumbnailPreviewClosed = false; let thumbnailInsertionAttempts = 0; const MAX_ATTEMPTS = 10; const RETRY_DELAY = 500; function isWatchPage() { const url = new URL(window.location.href); return url.pathname === '/watch' && url.searchParams.has('v'); } function getCurrentVideoId() { return new URLSearchParams(window.location.search).get('v'); } function removeOldOverlay() { const oldOverlay = document.querySelector('#thumbnailPreview-player-overlay'); if (oldOverlay) { oldOverlay.remove(); } } function shouldSkipThumbnailUpdate(newVideoId) { return !newVideoId || newVideoId === thumbnailPreviewCurrentVideoId || thumbnailPreviewClosed; } function findPlayerElement() { return document.querySelector('#movie_player') || document.querySelector('ytd-player'); } function createPlayerThumbnailOverlay(videoId, player) { const overlay = (createThumbnailOverlay(videoId, player)); overlay.id = 'thumbnailPreview-player-overlay'; overlay.dataset.videoId = videoId; overlay.style.cssText = ` position: absolute; top: 10%; right: 8px; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 6px; cursor: pointer; z-index: 1001; transition: all 0.15s ease; opacity: 0; `; return overlay; } function attemptInsertion() { const player = findPlayerElement(); if (!player) { thumbnailInsertionAttempts++; if (thumbnailInsertionAttempts < MAX_ATTEMPTS) { setTimeout(attemptInsertion, RETRY_DELAY); } else { thumbnailInsertionAttempts = 0; } return; } const overlayId = 'thumbnailPreview-player-overlay'; let overlay = player.querySelector(`#${overlayId}`); if (!overlay) { overlay = createPlayerThumbnailOverlay(thumbnailPreviewCurrentVideoId, player); overlay.tabIndex = 0; overlay.onmouseenter = () => { try { overlay.style.opacity = '0.5'; } catch {} }; overlay.onmouseleave = () => { try { overlay.style.opacity = '0'; } catch {} }; overlay.onfocus = () => { try { overlay.style.opacity = '0.5'; } catch {} }; overlay.onblur = () => { try { overlay.style.opacity = '0'; } catch {} }; overlay.addEventListener('keydown', e => { const ke = (e); if (ke && (ke.key === 'Enter' || ke.key === ' ')) { ke.preventDefault(); overlay.click(); } }); const playerAny = (player); if ( (getComputedStyle(playerAny)).position === 'static') { playerAny.style.position = 'relative'; } playerAny.appendChild(overlay); return; } if (overlay.dataset.videoId !== thumbnailPreviewCurrentVideoId) { overlay.remove(); attemptInsertion(); } thumbnailInsertionAttempts = 0; } function addOrUpdateThumbnailImage() { if (!isWatchPage()) return; const newVideoId = getCurrentVideoId(); if (newVideoId !== thumbnailPreviewCurrentVideoId) { thumbnailPreviewClosed = false; removeOldOverlay(); } if (shouldSkipThumbnailUpdate(newVideoId)) { return; } thumbnailPreviewCurrentVideoId = newVideoId; attemptInsertion(); } function createThumbnailOverlay(videoId, container) { const overlay = document.createElement('div'); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '16'); svg.setAttribute('height', '16'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'white'); svg.setAttribute('stroke-width', '2'); svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round'); svg.style.transition = 'stroke 0.2s ease'; const mainRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); mainRect.setAttribute('width', '18'); mainRect.setAttribute('height', '18'); mainRect.setAttribute('x', '3'); mainRect.setAttribute('y', '3'); mainRect.setAttribute('rx', '2'); mainRect.setAttribute('ry', '2'); svg.appendChild(mainRect); const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', '9'); circle.setAttribute('cy', '9'); circle.setAttribute('r', '2'); svg.appendChild(circle); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'm21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21'); svg.appendChild(path); overlay.appendChild(svg); overlay.style.cssText = ` position: absolute; bottom: 8px; left: 8px; background: rgba(0, 0, 0, 0.3); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; z-index: 1000; opacity: 0; transition: all 0.2s ease; `; overlay.onmouseenter = () => { overlay.style.background = 'rgba(0, 0, 0, 0.7)'; }; overlay.onmouseleave = () => { overlay.style.background = 'rgba(0, 0, 0, 0.3)'; }; overlay.onclick = async e => { e.preventDefault(); e.stopPropagation(); const isShorts = container.closest('ytm-shorts-lockup-view-model') || container.closest('.shortsLockupViewModelHost') || container.closest('[class*="shortsLockupViewModelHost"]') || container.querySelector('a[href*="/shorts/"]'); await openThumbnail(videoId, !!isShorts, overlay); }; return overlay; } function findThumbnailContainerFromImage(img) { return img.closest('yt-thumbnail-view-model') || img.parentElement; } function findShortsThumbnailContainer(shortsImg) { if (!shortsImg) return null; return ( shortsImg.closest('.ytCoreImageHost') || shortsImg.closest('[class*="ThumbnailContainer"]') || shortsImg.closest('[class*="ImageHost"]') || shortsImg.parentElement ); } function extractVideoInfo(container) { const img = container.querySelector('img[src*="ytimg.com"]'); if (!img?.src) return { videoId: null, thumbnailContainer: null }; const videoId = extractVideoId(img.src); const thumbnailContainer = findThumbnailContainerFromImage(img); return { videoId, thumbnailContainer }; } function extractShortsInfo(container) { const link = container.querySelector('a[href*="/shorts/"]'); if (!link?.href) return { videoId: null, thumbnailContainer: null }; const videoId = extractShortsId(link.href); const shortsImg = container.querySelector('img[src*="ytimg.com"]'); const thumbnailContainer = findShortsThumbnailContainer(shortsImg); return { videoId, thumbnailContainer }; } function ensureRelativePosition(thumbnailContainer) { if (getComputedStyle(thumbnailContainer).position === 'static') { thumbnailContainer.style.position = 'relative'; } } function setupOverlayHoverEffects(thumbnailContainer, overlay) { thumbnailContainer.onmouseenter = () => { overlay.style.opacity = '1'; }; thumbnailContainer.onmouseleave = () => { overlay.style.opacity = '0'; }; } function addThumbnailOverlay(container) { if (container.querySelector('.thumb-overlay')) return; let { videoId, thumbnailContainer } = extractVideoInfo(container); if (!videoId) { ({ videoId, thumbnailContainer } = extractShortsInfo(container)); } if (!videoId || !thumbnailContainer) return; ensureRelativePosition(thumbnailContainer); const overlay = createThumbnailOverlay(videoId, container); overlay.className = 'thumb-overlay'; thumbnailContainer.appendChild(overlay); setupOverlayHoverEffects(thumbnailContainer, overlay); } function createAvatarOverlay() { const overlay = document.createElement('div'); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '16'); svg.setAttribute('height', '16'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'white'); svg.setAttribute('stroke-width', '2'); svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round'); svg.style.transition = 'stroke 0.2s ease'; const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', '12'); circle.setAttribute('cy', '8'); circle.setAttribute('r', '5'); svg.appendChild(circle); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M20 21a8 8 0 0 0-16 0'); svg.appendChild(path); overlay.appendChild(svg); overlay.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.7); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 50%; cursor: pointer; z-index: 1000; opacity: 0; transition: all 0.2s ease; `; overlay.onmouseenter = () => { overlay.style.background = 'rgba(0, 0, 0, 0.9)'; }; overlay.onmouseleave = () => { overlay.style.background = 'rgba(0, 0, 0, 0.7)'; }; return overlay; } function addAvatarOverlay(img) { const container = img.parentElement; if (!container) return; if ( img.closest('.avatar-btn, #avatar-btn') || container.closest('.avatar-btn, #avatar-btn') || img.closest('button') || container.closest('button') || img.closest('.thumbnail-modal-wrapper') || container.closest('.thumbnail-modal-wrapper') ) { return; } if (container.querySelector('.avatar-overlay')) return; if (getComputedStyle(container).position === 'static') { container.style.position = 'relative'; } const overlay = createAvatarOverlay(); overlay.className = 'avatar-overlay'; overlay.onclick = e => { e.preventDefault(); e.stopPropagation(); const highResUrl = img.src.replace(/=s\d+-c-k-c0x00ffffff-no-rj.*/, '=s0'); showImageModal(highResUrl); }; container.appendChild(overlay); container.onmouseenter = () => { overlay.style.opacity = '1'; }; container.onmouseleave = () => { overlay.style.opacity = '0'; }; } function createBannerOverlay() { const overlay = document.createElement('div'); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '16'); svg.setAttribute('height', '16'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'white'); svg.setAttribute('stroke-width', '2'); svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round'); svg.style.transition = 'stroke 0.2s ease'; const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('x', '3'); rect.setAttribute('y', '3'); rect.setAttribute('width', '18'); rect.setAttribute('height', '18'); rect.setAttribute('rx', '2'); rect.setAttribute('ry', '2'); svg.appendChild(rect); const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', '9'); circle.setAttribute('cy', '9'); circle.setAttribute('r', '2'); svg.appendChild(circle); const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); polyline.setAttribute('points', '21,15 16,10 5,21'); svg.appendChild(polyline); overlay.appendChild(svg); overlay.style.cssText = ` position: absolute; bottom: 8px; left: 8px; background: rgba(0, 0, 0, 0.7); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; z-index: 1000; opacity: 0; transition: all 0.2s ease; `; overlay.onmouseenter = () => { overlay.style.background = 'rgba(0, 0, 0, 0.9)'; }; overlay.onmouseleave = () => { overlay.style.background = 'rgba(0, 0, 0, 0.7)'; }; return overlay; } function addBannerOverlay(img) { const container = img.parentElement; if (container.querySelector('.banner-overlay')) return; if (getComputedStyle(container).position === 'static') { container.style.position = 'relative'; } const overlay = createBannerOverlay(); overlay.className = 'banner-overlay'; overlay.onclick = e => { e.preventDefault(); e.stopPropagation(); const highResUrl = img.src.replace(/=w\d+-.*/, '=s0'); showImageModal(highResUrl); }; container.appendChild(overlay); container.onmouseenter = () => { overlay.style.opacity = '1'; }; container.onmouseleave = () => { overlay.style.opacity = '0'; }; } function processAvatars() { const avatarSelectors = [ 'yt-avatar-shape img', '#avatar img', 'ytd-channel-avatar-editor img', '.ytd-video-owner-renderer img[src*="yt"]', 'img[src*="yt3.ggpht.com"]', 'img[src*="yt4.ggpht.com"]', ]; avatarSelectors.forEach(selector => { document.querySelectorAll(selector).forEach(img => { if (!img.src) return; if (!img.src.includes('yt')) return; if (img.closest('.avatar-overlay')) return; const isAvatar = img.naturalWidth > 0 && img.naturalWidth === img.naturalHeight; if (isAvatar || img.src.includes('ggpht.com')) { addAvatarOverlay(img); } }); }); } function processBanners() { const bannerSelectors = [ 'yt-image-banner-view-model img', 'ytd-c4-tabbed-header-renderer img[src*="yt"]', '#channel-header img[src*="banner"]', 'img[src*="banner"]', ]; bannerSelectors.forEach(selector => { document.querySelectorAll(selector).forEach(img => { if (!img.src) return; if (img.closest('.banner-overlay')) return; const isBanner = (img.src.includes('banner') || img.src.includes('yt')) && img.naturalWidth > img.naturalHeight * 2; if (isBanner || img.src.includes('banner')) { addBannerOverlay(img); } }); }); } function processThumbnails() { const n1 = document.querySelectorAll('yt-thumbnail-view-model'); for (let i = 0; i < n1.length; i++) addThumbnailOverlay(n1[i]); const n2 = document.querySelectorAll('.ytd-thumbnail'); for (let i = 0; i < n2.length; i++) addThumbnailOverlay(n2[i]); const n3 = document.querySelectorAll('ytm-shorts-lockup-view-model'); for (let i = 0; i < n3.length; i++) addThumbnailOverlay(n3[i]); const n4 = document.querySelectorAll('.shortsLockupViewModelHost'); for (let i = 0; i < n4.length; i++) addThumbnailOverlay(n4[i]); const n5 = document.querySelectorAll('[class*="shortsLockupViewModelHost"]'); for (let i = 0; i < n5.length; i++) addThumbnailOverlay(n5[i]); } function processAll() { processThumbnails(); processAvatars(); processBanners(); addOrUpdateThumbnailImage(); } function setupMutationObserver() { const observer = new MutationObserver(() => { setTimeout(processAll, 50); }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true, }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true, }); }); } } function setupUrlChangeDetection() { let currentUrl = location.href; const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function (...args) { originalPushState.call(history, ...args); setTimeout(() => { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 500); } }, 100); }; history.replaceState = function (...args) { originalReplaceState.call(history, ...args); setTimeout(() => { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 500); } }, 100); }; window.addEventListener('popstate', () => { setTimeout(() => { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 500); } }, 100); }); setInterval(() => { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 300); } }, 500); } function initialize() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(init, 100); }); } else { setTimeout(init, 100); } } function init() { setupUrlChangeDetection(); setupMutationObserver(); processAll(); setTimeout(processAll, 500); setTimeout(processAll, 1000); setTimeout(processAll, 2000); } initialize(); })(); (function () { 'use strict'; const _globalI18n_shorts = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n_shorts && typeof _globalI18n_shorts.t === 'function') { return _globalI18n_shorts.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; let text = key; if (Object.keys(params).length === 0) return key; for (const [paramKey, val] of Object.entries(params)) { text = text.split(`{${paramKey}}`).join(String(val)); } return text; }; const config = { enabled: true, get shortcuts() { return { seekBackward: { key: 'ArrowLeft', get description() { return t('seekBackward'); }, }, seekForward: { key: 'ArrowRight', get description() { return t('seekForward'); }, }, volumeUp: { key: '+', get description() { return t('volumeUp'); }, }, volumeDown: { key: '-', get description() { return t('volumeDown'); }, }, mute: { key: 'm', get description() { return t('muteUnmute'); }, }, toggleCaptions: { key: 'c', get description() { return t('toggleCaptions'); }, }, showHelp: { key: '?', get description() { return t('showHideHelp'); }, editable: false, }, }; }, storageKey: 'youtube_shorts_keyboard_settings', }; const state = { helpVisible: false, lastAction: null, actionTimeout: null, editingShortcut: null, cachedVideo: null, lastVideoCheck: 0, }; const getCurrentVideo = (() => { const selectors = ['ytd-reel-video-renderer[is-active] video', '#shorts-player video', 'video']; return () => { const now = Date.now(); if (state.cachedVideo?.isConnected && now - state.lastVideoCheck < 100) { return state.cachedVideo; } for (const selector of selectors) { const video = YouTubeUtils.querySelector(selector); if (video) { state.cachedVideo = video; state.lastVideoCheck = now; return video; } } state.cachedVideo = null; return null; }; })(); const utils = { isInShortsPage: () => location.pathname.startsWith('/shorts/'), isInputFocused: () => { const el = document.activeElement; return el?.matches?.('input, textarea, [contenteditable="true"]') || el?.isContentEditable; }, loadSettings: () => { try { const saved = localStorage.getItem(config.storageKey); if (!saved) return; const parsed = JSON.parse(saved); if (typeof parsed !== 'object' || parsed === null) { console.warn('[YouTube+][Shorts]', 'Invalid settings format'); return; } if (typeof parsed.enabled === 'boolean') { config.enabled = parsed.enabled; } if (parsed.shortcuts && typeof parsed.shortcuts === 'object') { const defaultShortcuts = utils.getDefaultShortcuts(); for (const [action, shortcut] of Object.entries(parsed.shortcuts)) { if (!defaultShortcuts[action]) continue; if (!shortcut || typeof shortcut !== 'object') continue; const { key: sKey, editable: sEditable } = (shortcut); if (typeof sKey === 'string' && sKey.length > 0 && sKey.length <= 20) { config.shortcuts[action] = { key: sKey, description: defaultShortcuts[action].description, editable: sEditable !== false, }; } } } } catch (error) { console.error('[YouTube+][Shorts]', 'Error loading settings:', error); } }, saveSettings: () => { try { const settingsToSave = { enabled: config.enabled, shortcuts: config.shortcuts, }; localStorage.setItem(config.storageKey, JSON.stringify(settingsToSave)); } catch (error) { console.error('[YouTube+][Shorts]', 'Error saving settings:', error); } }, getDefaultShortcuts: () => ({ seekBackward: { key: 'ArrowLeft', get description() { return t('seekBackward'); }, }, seekForward: { key: 'ArrowRight', get description() { return t('seekForward'); }, }, volumeUp: { key: '+', get description() { return t('volumeUp'); }, }, volumeDown: { key: '-', get description() { return t('volumeDown'); }, }, mute: { key: 'm', get description() { return t('muteUnmute'); }, }, toggleCaptions: { key: 'c', get description() { return t('toggleCaptions'); }, }, showHelp: { key: '?', get description() { return t('showHideHelp'); }, editable: false, }, }), }; const feedback = (() => { let element = null; const create = () => { if (element) return element; element = document.createElement('div'); element.id = 'shorts-keyboard-feedback'; element.style.cssText = ` position:fixed;top:50%;left:50%;transform:translate(-50%,-50%); background:var(--shorts-feedback-bg,rgba(255,255,255,.1)); backdrop-filter:blur(16px) saturate(150%); border:1px solid var(--shorts-feedback-border,rgba(255,255,255,.15)); border-radius:20px; color:var(--shorts-feedback-color,#fff); padding:18px 32px;font-size:20px;font-weight:700; z-index:10000;opacity:0;visibility:hidden;pointer-events:none; transition:all .3s cubic-bezier(.4,0,.2,1);text-align:center; box-shadow:0 8px 32px rgba(0,0,0,.4); background: rgba(155, 155, 155, 0.15); border: 1px solid rgba(255,255,255,0.2); box-shadow: 0 8px 32px 0 rgba(31,38,135,0.37); backdrop-filter: blur(12px) saturate(180%); -webkit-backdrop-filter: blur(12px) saturate(180%); `; document.body.appendChild(element); return element; }; return { show: text => { state.lastAction = text; clearTimeout(state.actionTimeout); const el = create(); el.textContent = text; requestAnimationFrame(() => { el.style.opacity = '1'; el.style.visibility = 'visible'; el.style.transform = 'translate(-50%, -50%) scale(1.05)'; }); state.actionTimeout = setTimeout(() => { el.style.opacity = '0'; el.style.visibility = 'hidden'; el.style.transform = 'translate(-50%, -50%) scale(0.95)'; }, 1500); }, }; })(); const actions = { seekBackward: () => { const video = getCurrentVideo(); if (video) { video.currentTime = Math.max(0, video.currentTime - 5); feedback.show('-5s'); } }, seekForward: () => { const video = getCurrentVideo(); if (video) { video.currentTime = Math.min(video.duration || Infinity, video.currentTime + 5); feedback.show('+5s'); } }, toggleCaptions: () => { try { const buttons = document.querySelectorAll('button[aria-label]'); for (const b of buttons) { const aria = (b.getAttribute('aria-label') || '').toLowerCase(); if ( aria.includes('subtit') || aria.includes('caption') || aria.includes('субтит') || aria.includes('субтитр') || aria.includes('cc') ) { if (b.offsetParent !== null) { b.click(); break; } } } } catch { } const video = getCurrentVideo(); if (video && video.textTracks && video.textTracks.length) { const tracks = Array.from(video.textTracks).filter( tr => tr.kind === 'subtitles' || tr.kind === 'captions' || !tr.kind ); if (tracks.length) { const anyShowing = tracks.some(tr => tr.mode === 'showing'); tracks.forEach(tr => { tr.mode = anyShowing ? 'hidden' : 'showing'; }); feedback.show(anyShowing ? t('captionsOff') : t('captionsOn')); return; } } feedback.show(t('captionsUnavailable')); }, volumeUp: () => { const video = getCurrentVideo(); if (video) { video.volume = Math.min(1, video.volume + 0.1); feedback.show(`${Math.round(video.volume * 100)}%`); } }, volumeDown: () => { const video = getCurrentVideo(); if (video) { video.volume = Math.max(0, video.volume - 0.1); feedback.show(`${Math.round(video.volume * 100)}%`); } }, mute: () => { const video = getCurrentVideo(); try { const buttons = document.querySelectorAll('button[aria-label]'); for (const b of buttons) { const aria = (b.getAttribute('aria-label') || '').toLowerCase(); if ( aria.includes('mute') || aria.includes('unmute') || aria.includes('sound') || aria.includes('volume') || aria.includes('звук') || aria.includes('громк') ) { if (b.offsetParent !== null) { b.click(); setTimeout(() => { const v = getCurrentVideo(); if (v) feedback.show(v.muted ? '🔇' : '🔊'); }, 60); return; } } } } catch { } if (video) { video.muted = !video.muted; feedback.show(video.muted ? '🔇' : '🔊'); } }, showHelp: () => helpPanel.toggle(), }; const helpPanel = (() => { let panel = null; const create = () => { if (panel) return panel; panel = document.createElement('div'); panel.id = 'shorts-keyboard-help'; panel.className = 'glass-panel shorts-help-panel'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); panel.tabIndex = -1; const render = () => { panel.innerHTML = `

${t('keyboardShortcuts')}

${Object.entries(config.shortcuts) .map( ([action, shortcut]) => `
${shortcut.key === ' ' ? 'Space' : shortcut.key} ${shortcut.description}
` ) .join('')}
`; panel.querySelector('.help-close').onclick = () => helpPanel.hide(); panel.querySelector('.reset-all-shortcuts').onclick = () => { if (confirm(t('resetAllConfirm'))) { config.shortcuts = utils.getDefaultShortcuts(); utils.saveSettings(); feedback.show(t('shortcutsReset')); render(); } }; panel.querySelectorAll('kbd[data-action]:not(.non-editable)').forEach(kbd => { kbd.onclick = () => editShortcut(kbd.dataset.action, config.shortcuts[kbd.dataset.action].key); }); }; render(); document.body.appendChild(panel); return panel; }; return { show: () => { const p = create(); p.classList.add('visible'); state.helpVisible = true; p.focus(); }, hide: () => { if (panel) { panel.classList.remove('visible'); state.helpVisible = false; } }, toggle: () => (state.helpVisible ? helpPanel.hide() : helpPanel.show()), refresh: () => { if (panel) { panel.remove(); panel = null; } }, }; })(); const editShortcut = (actionKey, currentKey) => { const dialog = document.createElement('div'); dialog.className = 'glass-modal shortcut-edit-dialog'; dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true'); dialog.innerHTML = `

${t('editShortcut')}: ${config.shortcuts[actionKey].description}

${t('pressAnyKey')}

${t('current')}: ${currentKey === ' ' ? 'Space' : currentKey}
`; document.body.appendChild(dialog); state.editingShortcut = actionKey; const handleKey = e => { e.preventDefault(); e.stopPropagation(); if (e.key === 'Escape') return cleanup(); const conflict = Object.keys(config.shortcuts).find( key => key !== actionKey && config.shortcuts[key].key === e.key ); if (conflict) { feedback.show(t('keyAlreadyUsed', { key: e.key })); return; } config.shortcuts[actionKey].key = e.key; utils.saveSettings(); feedback.show(t('shortcutUpdated')); helpPanel.refresh(); cleanup(); }; const cleanup = () => { document.removeEventListener('keydown', handleKey, true); dialog.remove(); state.editingShortcut = null; }; dialog.querySelector('.shortcut-cancel').onclick = cleanup; dialog.onclick = ({ target }) => { if (target === dialog) cleanup(); }; document.addEventListener('keydown', handleKey, true); }; const addStyles = () => { if (document.getElementById('shorts-keyboard-styles')) return; const styles = ` :root{--shorts-feedback-bg:rgba(255,255,255,.15);--shorts-feedback-border:rgba(255,255,255,.2);--shorts-feedback-color:#fff;--shorts-help-bg:rgba(255,255,255,.15);--shorts-help-border:rgba(255,255,255,.2);--shorts-help-color:#fff;} html[dark],body[dark]{--shorts-feedback-bg:rgba(34,34,34,.7);--shorts-feedback-border:rgba(255,255,255,.15);--shorts-feedback-color:#fff;--shorts-help-bg:rgba(34,34,34,.7);--shorts-help-border:rgba(255,255,255,.1);--shorts-help-color:#fff;} html:not([dark]){--shorts-feedback-bg:rgba(255,255,255,.95);--shorts-feedback-border:rgba(0,0,0,.08);--shorts-feedback-color:#222;--shorts-help-bg:rgba(255,255,255,.98);--shorts-help-border:rgba(0,0,0,.08);--shorts-help-color:#222;} .shorts-help-panel{position:fixed;top:50%;left:25%;transform:translate(-50%,-50%) scale(.9);z-index:10001;opacity:0;visibility:hidden;transition:all .3s ease;width:340px;max-width:95vw;max-height:80vh;overflow:hidden;outline:none;color:var(--shorts-help-color,#fff);} .shorts-help-panel.visible{opacity:1;visibility:visible;transform:translate(-50%,-50%) scale(1);} .help-header{display:flex;justify-content:space-between;align-items:center;padding:24px 24px 12px;border-bottom:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);} html:not([dark]) .help-header{background:rgba(0,0,0,.04);border-bottom:1px solid rgba(0,0,0,.08);} .help-header h3{margin:0;font-size:20px;font-weight:700;} .help-close{display:flex;align-items:center;justify-content:center;padding:4px;} .help-content{padding:18px 24px;max-height:400px;overflow-y:auto;} .help-item{display:flex;align-items:center;margin-bottom:14px;gap:18px;} .help-item kbd{background:rgba(255,255,255,.15);color:inherit;padding:7px 14px;border-radius:8px;font-family:monospace;font-size:15px;font-weight:700;min-width:60px;text-align:center;border:1.5px solid rgba(255,255,255,.2);cursor:pointer;transition:all .2s;position:relative;} html:not([dark]) .help-item kbd{background:rgba(0,0,0,.06);color:#222;border:1.5px solid rgba(0,0,0,.08);} .help-item kbd:hover{background:rgba(255,255,255,.22);transform:scale(1.07);} .help-item kbd:after{content:"✎";position:absolute;top:-7px;right:-7px;font-size:11px;opacity:0;transition:opacity .2s;} .help-item kbd:hover:after{opacity:.7;} .help-item kbd.non-editable{cursor:default;opacity:.7;} .help-item kbd.non-editable:hover{background:rgba(255,255,255,.15);transform:none;} .help-item kbd.non-editable:after{display:none;} .help-item span{font-size:15px;color:rgba(255,255,255,.92);} html:not([dark]) .help-item span{color:#222;} .help-footer{padding:16px 24px 20px;border-top:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);text-align:center;} html:not([dark]) .help-footer{background:rgba(0,0,0,.04);border-top:1px solid rgba(0,0,0,.08);} .reset-all-shortcuts{display:inline-flex;align-items:center;justify-content:center;gap:var(--yt-space-sm);} .shortcut-edit-dialog{z-index:10002;} .shortcut-edit-content{padding:28px 32px;min-width:320px;text-align:center;display:flex;flex-direction:column;gap:var(--yt-space-md);color:inherit;} html:not([dark]) .shortcut-edit-content{color:#222;} .shortcut-edit-content h4{margin:0 0 14px;font-size:17px;font-weight:700;} .shortcut-edit-content p{margin:0 0 18px;font-size:15px;color:rgba(255,255,255,.85);} html:not([dark]) .shortcut-edit-content p{color:#222;} .current-shortcut{margin:18px 0;font-size:15px;} .current-shortcut kbd{background:rgba(255,255,255,.15);padding:5px 12px;border-radius:6px;font-family:monospace;border:1.5px solid rgba(255,255,255,.2);} html:not([dark]) .current-shortcut kbd{background:rgba(0,0,0,.06);color:#222;border:1.5px solid rgba(0,0,0,.08);} .shortcut-cancel{display:inline-flex;align-items:center;justify-content:center;gap:var(--yt-space-sm);} @media(max-width:480px){.shorts-help-panel{width:98vw;max-height:85vh}.help-header{padding:16px 10px 8px 10px}.help-content{padding:12px 10px}.help-item{gap:10px}.help-item kbd{min-width:44px;font-size:13px;padding:5px 7px}.shortcut-edit-content{margin:20px;min-width:auto}} #shorts-keyboard-feedback{background:var(--shorts-feedback-bg,rgba(255,255,255,.15));color:var(--shorts-feedback-color,#fff);border:1.5px solid var(--shorts-feedback-border,rgba(255,255,255,.2));border-radius:20px;box-shadow:0 8px 32px 0 rgba(31,38,135,.37);backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);} html:not([dark]) #shorts-keyboard-feedback{background:var(--shorts-feedback-bg,rgba(255,255,255,.95));color:var(--shorts-feedback-color,#222);border:1.5px solid var(--shorts-feedback-border,rgba(0,0,0,.08));} `; YouTubeUtils.StyleManager.add('shorts-keyboard-styles', styles); }; const handleKeydown = e => { if ( !config.enabled || !utils.isInShortsPage() || utils.isInputFocused() || state.editingShortcut ) { return; } let { key } = e; if (e.code === 'NumpadAdd') key = '+'; else if (e.code === 'NumpadSubtract') key = '-'; const action = Object.keys(config.shortcuts).find(k => config.shortcuts[k].key === key); if (action && actions[action]) { e.preventDefault(); e.stopPropagation(); actions[action](); } }; const init = () => { utils.loadSettings(); addStyles(); YouTubeUtils.cleanupManager.registerListener(document, 'keydown', handleKeydown, true); const clickHandler = ({ target }) => { if (state.helpVisible && target?.closest && !target.closest('#shorts-keyboard-help')) { helpPanel.hide(); } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler); document.addEventListener('keydown', e => { if (e.key === 'Escape' && state.helpVisible) { e.preventDefault(); helpPanel.hide(); } }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } if (utils.isInShortsPage() && !localStorage.getItem('shorts_keyboard_help_shown')) { setTimeout(() => { feedback.show('Press ? for shortcuts'); localStorage.setItem('shorts_keyboard_help_shown', 'true'); }, 2000); } })(); (function () { 'use strict'; function extractDigits(text) { if (!text || typeof text !== 'string') return null; const digits = text.replace(/[^\d]/g, ''); return digits ? Number(digits) : null; } function findFirstText(selectors) { for (const sel of selectors) { const el = document.querySelector(sel); if (!el) continue; try { const aria = el.getAttribute?.('aria-label'); if (aria?.trim()) return aria.trim(); } catch { } if (el.textContent?.trim()) return el.textContent.trim(); } return ''; } function findFirstAttr(selectors) { for (const sel of selectors) { const el = document.querySelector(sel); if (!el) continue; const href = el.getAttribute?.('href') || el.getAttribute?.('content'); if (href) return href; if (el.src) return el.src; } return ''; } function tryExtractFromAriaLabel(btn) { try { const ariaEl = btn.querySelector?.('[aria-label]'); if (!ariaEl) return null; const ariaLabel = ariaEl.getAttribute?.('aria-label'); if (!ariaLabel?.trim()) return null; return extractDigits(ariaLabel.trim()); } catch { return null; } } function tryExtractFromButtonText(btn) { const anchor = btn.querySelector?.('a'); const iconBtn = btn.querySelector?.('yt-icon-button'); const el = anchor || iconBtn || btn; const text = el?.textContent?.trim() || ''; return extractDigits(text); } function extractFromToggleButton(btn) { if (!btn) return null; const fromAria = tryExtractFromAriaLabel(btn); if (fromAria !== null) return fromAria; return tryExtractFromButtonText(btn); } function tryExtractDislikeFromToggles() { try { const toggleBtns = document.querySelectorAll('ytd-toggle-button-renderer'); if (toggleBtns?.length > 1) { return extractFromToggleButton(toggleBtns[1]); } } catch { } return null; } function extractViews() { const viewsText = findFirstText([ 'ytd-video-view-count-renderer span.view-count', 'span.view-count', 'yt-view-count-renderer span', 'ytd-video-primary-info-renderer #info-strings yt-formatted-string', ]); const views = extractDigits(viewsText); return views === null ? {} : { views }; } function extractLikes() { const likeBtnSelectors = [ 'ytd-toggle-button-renderer:nth-of-type(1) a', '#top-level-buttons-computed ytd-toggle-button-renderer:nth-of-type(1) yt-icon-button', 'ytd-toggle-button-renderer:nth-of-type(1) yt-formatted-string', ]; const likesText = findFirstText(likeBtnSelectors); const likes = extractDigits(likesText); if (likes !== null) { return { likes }; } const toggleBtns = document.querySelectorAll('ytd-toggle-button-renderer'); if (toggleBtns?.length) { const likeVal = extractFromToggleButton(toggleBtns[0]); if (likeVal !== null) { return { likes: likeVal }; } } return {}; } function extractDislikes() { const dislikeCandidate = tryExtractDislikeFromToggles(); if (dislikeCandidate !== null) { return { dislikes: dislikeCandidate }; } const toggleBtns = document.querySelectorAll('ytd-toggle-button-renderer'); if (toggleBtns?.length > 1) { const dislikeVal = extractFromToggleButton(toggleBtns[1]); if (dislikeVal !== null) { return { dislikes: dislikeVal }; } } return {}; } function extractComments() { const commentsText = findFirstText([ '#count > yt-formatted-string', 'ytd-comments-header-renderer #count', ]); const comments = extractDigits(commentsText); return comments === null ? {} : { comments }; } function extractSubscribers() { const subsText = findFirstText(['#subscriber-count', 'yt-formatted-string#owner-sub-count']); const subscribers = extractDigits(subsText); return subscribers === null ? {} : { subscribers }; } function extractThumbnail() { const thumb = findFirstAttr([ 'link[rel="image_src"]', 'meta[property="og:image"]', 'meta[name="og:image"]', 'meta[name="twitter:image"]', 'meta[itemprop="thumbnailUrl"]', 'meta[itemprop="image"]', '.ytp-thumbnail img', ]); return thumb ? { thumbnail: thumb } : {}; } function extractTitle() { const titleMeta = findFirstAttr([ 'meta[property="og:title"]', 'meta[name="title"]', 'meta[name="twitter:title"]', 'meta[itemprop="name"]', ]); return titleMeta ? { title: titleMeta } : {}; } function extractTimeComponents(match) { return { hours: parseInt(match[1] || '0', 10), minutes: parseInt(match[2] || '0', 10), seconds: parseInt(match[3] || '0', 10), }; } function convertToSeconds(components) { return components.hours * 3600 + components.minutes * 60 + components.seconds; } function parseISODuration(iso) { if (!iso || typeof iso !== 'string') return null; const match = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/i); if (!match) return null; const components = extractTimeComponents(match); return convertToSeconds(components); } function formatSeconds(sec) { if (!sec || isNaN(Number(sec))) return null; const s = Number(sec); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const ss = Math.floor(s % 60); if (h > 0) { return `${h}:${String(m).padStart(2, '0')}:${String(ss).padStart(2, '0')}`; } return `${m}:${String(ss).padStart(2, '0')}`; } function getMetaDurationSeconds() { const og = document.querySelector('meta[property="og:video:duration"]')?.getAttribute('content') || document.querySelector('meta[name="duration"]')?.getAttribute('content'); if (og && !isNaN(Number(og))) return Number(og); const iso = document.querySelector('meta[itemprop="duration"]')?.getAttribute('content'); if (iso) return parseISODuration(iso); return null; } function toNumber(v) { if (v === undefined || v === null) return null; const num = Number(v); return isNaN(num) ? null : num; } function getDurationFromAPI(apiStats) { if (!apiStats) return null; const candidates = [ apiStats.duration, apiStats.lengthSeconds, apiStats.durationSeconds, apiStats.videoLength, ]; for (const candidate of candidates) { const seconds = toNumber(candidate); if (seconds) return seconds; } return null; } function getDurationFromPlayer() { try { const lengthSeconds = window?.ytInitialPlayerResponse?.videoDetails?.lengthSeconds; return toNumber(lengthSeconds); } catch { return null; } } function getDurationFromSources(apiStats) { let seconds = getDurationFromAPI(apiStats); if (!seconds) { seconds = getDurationFromPlayer(); } if (!seconds) { seconds = getMetaDurationSeconds(); } return seconds ? formatSeconds(seconds) : null; } function getCountryFromAPI(apiStats) { if (!apiStats) return null; return apiStats.country || apiStats.region || null; } function getCountriesFromPlayer() { try { const countries = window?.ytInitialPlayerResponse?.microformat?.playerMicroformatRenderer?.availableCountries; if (Array.isArray(countries) && countries.length > 0) { return countries.join(', '); } } catch { } return null; } function getCountryFromSources(apiStats, pageStats) { const apiCountry = getCountryFromAPI(apiStats); if (apiCountry) return apiCountry; if (pageStats?.country) return pageStats.country; return getCountriesFromPlayer(); } function checkApiMonetization(apiStats, t) { if (apiStats?.monetized !== undefined) { return apiStats.monetized ? t('yes') : t('no'); } if (apiStats?.isMonetized !== undefined) { return apiStats.isMonetized ? t('yes') : t('no'); } return null; } function checkPageMonetization(pageStats, t) { if (pageStats?.monetization !== undefined) { return pageStats.monetization ? t('yes') : t('no'); } return null; } function checkPaidPromotion(t) { const bodyText = document.body?.innerText || ''; if (/paid promotion|includes paid promotion|платн/i.test(bodyText)) { return t('paidPromotion'); } return null; } function getMonetizationFromSources(apiStats, pageStats, t = s => s) { return ( checkApiMonetization(apiStats, t) || checkPageMonetization(pageStats, t) || checkPaidPromotion(t) || null ); } if (typeof window !== 'undefined') { window.YouTubeStatsHelpers = { extractDigits, findFirstText, findFirstAttr, extractFromToggleButton, tryExtractDislikeFromToggles, extractViews, extractLikes, extractDislikes, extractComments, extractSubscribers, extractThumbnail, extractTitle, parseISODuration, formatSeconds, getMetaDurationSeconds, getDurationFromSources, getCountryFromSources, getMonetizationFromSources, }; } })(); const parseSubscriberCount = countText => { const subMatch = countText.match(/[\d,\.]+[KMB]?/); if (!subMatch) return 0; const raw = subMatch[0].replace(/,/g, ''); const numCount = Number(raw.replace(/[KMB]/, '')) || 0; if (raw.includes('K')) { return Math.floor(numCount * 1000); } if (raw.includes('M')) { return Math.floor(numCount * 1000000); } if (raw.includes('B')) { return Math.floor(numCount * 1000000000); } return Math.floor(numCount); }; const extractSubscriberCountFromPage = () => { const subCountSelectors = [ '#subscriber-count', '.yt-subscription-button-subscriber-count-branded-horizontal', '[id*="subscriber"]', '.ytd-subscribe-button-renderer', ]; for (const selector of subCountSelectors) { const subCountElem = document.querySelector(selector); if (subCountElem) { const subText = subCountElem.textContent || subCountElem.innerText || ''; const count = parseSubscriberCount(subText); if (count > 0) { return count; } } } return 0; }; const createFallbackStats = followerCount => ({ followerCount, bottomOdos: [0, 0], error: true, timestamp: Date.now(), }); const isValidStatsResponse = stats => { return stats && typeof stats.followerCount !== 'undefined'; }; const getCachedStats = (cache, channelId, cacheDuration, utils) => { if (!cache.has(channelId)) return null; const cached = cache.get(channelId); const isRecent = Date.now() - cached.timestamp < cacheDuration; if (isRecent) { utils.log('Using cached stats for channel:', channelId); return cached; } return null; }; const exponentialBackoff = (retryNumber, maxRetries) => { const delay = 1000 * (maxRetries - retryNumber + 2); return new Promise(resolve => setTimeout(resolve, delay)); }; const fetchWithRetry = async (fetchFn, maxRetries, utils) => { let retries = maxRetries; while (retries > 0) { try { const stats = await fetchFn(); if (!isValidStatsResponse(stats)) { throw new Error('Invalid stats response structure'); } return stats; } catch (e) { utils.warn('Fetch attempt failed:', e.message); retries--; if (retries > 0) { await exponentialBackoff(retries, maxRetries); } } } return null; }; const cacheStats = (cache, channelId, stats) => { cache.set(channelId, { ...stats, timestamp: Date.now(), }); }; if (typeof window !== 'undefined') { window.YouTubePlusChannelStatsHelpers = { parseSubscriberCount, extractSubscriberCountFromPage, createFallbackStats, isValidStatsResponse, getCachedStats, exponentialBackoff, fetchWithRetry, cacheStats, }; } window.YouTubePlusStatsUtils = (() => { 'use strict'; function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } function formatDuration(seconds) { if (!seconds || seconds === 0) return '0:00'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); const pad = num => String(num).padStart(2, '0'); if (hours > 0) { return `${hours}:${pad(minutes)}:${pad(secs)}`; } return `${minutes}:${pad(secs)}`; } function createStatsRow(label, value, className = '') { const DOMUtils = window.YouTubePlusDOMUtils; if (DOMUtils && DOMUtils.createElement) { return DOMUtils.createElement( 'div', { className: `ytp-stats-row ${className}`, }, [ DOMUtils.createElement('span', { className: 'ytp-stats-label' }, [label]), DOMUtils.createElement('span', { className: 'ytp-stats-value' }, [value]), ] ); } const row = document.createElement('div'); row.className = `ytp-stats-row ${className}`; const labelEl = document.createElement('span'); labelEl.className = 'ytp-stats-label'; labelEl.textContent = label; const valueEl = document.createElement('span'); valueEl.className = 'ytp-stats-value'; valueEl.textContent = value; row.appendChild(labelEl); row.appendChild(valueEl); return row; } function createSectionHeader(title) { const DOMUtils = window.YouTubePlusDOMUtils; if (DOMUtils && DOMUtils.createElement) { return DOMUtils.createElement( 'div', { className: 'ytp-stats-section-header', }, [title] ); } const header = document.createElement('div'); header.className = 'ytp-stats-section-header'; header.textContent = title; return header; } function determineQualityLabel(height) { const qualityMap = [ { threshold: 2160, label: '4K' }, { threshold: 1440, label: '1440p' }, { threshold: 1080, label: '1080p' }, { threshold: 720, label: '720p' }, { threshold: 480, label: '480p' }, { threshold: 360, label: '360p' }, ]; for (const { threshold, label } of qualityMap) { if (height >= threshold) return label; } return height > 0 ? `${height}p` : 'Unknown'; } function getVideoQuality(video) { if (!video) return { width: 0, height: 0, quality: 'Unknown' }; const width = video.videoWidth || 0; const height = video.videoHeight || 0; const quality = determineQualityLabel(height); return { width, height, quality }; } function getVideoMetrics(video) { if (!video) { return { buffered: 0, played: 0, currentTime: 0, duration: 0, bufferedRanges: [], }; } const bufferedRanges = []; for (let i = 0; i < video.buffered.length; i++) { bufferedRanges.push({ start: video.buffered.start(i), end: video.buffered.end(i), }); } return { buffered: video.buffered.length > 0 ? video.buffered.end(video.buffered.length - 1) : 0, played: video.currentTime || 0, currentTime: video.currentTime || 0, duration: video.duration || 0, bufferedRanges, }; } function getBufferPercentage(video) { if (!video || !video.duration) return 0; const metrics = getVideoMetrics(video); return Math.round((metrics.buffered / video.duration) * 100); } function createOverlay({ className = '', content = '', closeable = true } = {}) { const DOMUtils = window.YouTubePlusDOMUtils; const overlay = DOMUtils && DOMUtils.createElement ? DOMUtils.createElement('div', { className: `ytp-overlay ${className}` }) : document.createElement('div'); if (!DOMUtils) { overlay.className = `ytp-overlay ${className}`; } if (content) { overlay.textContent = content; } if (closeable) { const closeBtn = DOMUtils && DOMUtils.createButton ? DOMUtils.createButton({ text: '×', className: 'ytp-overlay-close', ariaLabel: 'Close', }) : document.createElement('button'); if (!DOMUtils) { closeBtn.className = 'ytp-overlay-close'; closeBtn.textContent = '×'; closeBtn.setAttribute('aria-label', 'Close'); } closeBtn.addEventListener('click', () => { overlay.remove(); }); overlay.appendChild(closeBtn); } return overlay; } function positionOverlay(overlay, target, position = 'bottom') { if (!overlay || !target) return; const rect = target.getBoundingClientRect(); overlay.style.position = 'absolute'; switch (position) { case 'top': overlay.style.bottom = `${window.innerHeight - rect.top + 10}px`; overlay.style.left = `${rect.left}px`; break; case 'bottom': overlay.style.top = `${rect.bottom + 10}px`; overlay.style.left = `${rect.left}px`; break; case 'left': overlay.style.top = `${rect.top}px`; overlay.style.right = `${window.innerWidth - rect.left + 10}px`; break; case 'right': overlay.style.top = `${rect.top}px`; overlay.style.left = `${rect.right + 10}px`; break; default: break; } } return { formatBytes, formatDuration, createStatsRow, createSectionHeader, getVideoQuality, getVideoMetrics, getBufferPercentage, createOverlay, positionOverlay, }; })(); (function () { 'use strict'; const isStudioPage = () => { try { const host = location.hostname || ''; const href = location.href || ''; return ( host.includes('studio.youtube.com') || host.includes('studio.') || href.includes('studio.youtube.com') ); } catch { return false; } }; if (isStudioPage()) return; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') { return _globalI18n.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const styles = ` .videoStats{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;margin-left:8px;background:rgba(255,255,255,0.12);box-shadow:0 12px 30px rgba(0,0,0,0.32);backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);border:1.25px solid rgba(255,255,255,0.12);transition:transform .18s ease,background .18s} html[dark] .videoStats{background:rgba(24,24,24,0.68);border:1.25px solid rgba(255,255,255,0.08)}html:not([dark]) .videoStats{background:rgba(255,255,255,0.12);border:1.25px solid rgba(0,0,0,0.06)}.videoStats:hover{transform:translateY(-2px)}.videoStats svg{width:18px;height:18px;fill:var(--yt-spec-text-primary,#030303)}html[dark] .videoStats svg{fill:#fff}html:not([dark]) .videoStats svg{fill:#222} .shortsStats{display:flex;align-items:center;justify-content:center;margin-top:16px;margin-bottom:16px;width:48px;height:48px;border-radius:50%;cursor:pointer;background:rgba(255,255,255,0.12);box-shadow:0 12px 30px rgba(0,0,0,0.32);backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);border:1.25px solid rgba(255,255,255,0.12);transition:transform .22s ease}html[dark] .shortsStats{background:rgba(24,24,24,0.68);border:1.25px solid rgba(255,255,255,0.08)}html:not([dark]) .shortsStats{background:rgba(255,255,255,0.12);border:1.25px solid rgba(0,0,0,0.06)} .shortsStats:hover{transform:translateY(-3px)}.shortsStats svg{width:24px;height:24px;fill:#222}html[dark] .shortsStats svg{fill:#fff}html:not([dark]) .shortsStats svg{fill:#222} .stats-menu-container{position:relative;display:inline-block}.stats-horizontal-menu{position:absolute;display:flex;left:100%;top:0;height:100%;visibility:hidden;opacity:0;transition:visibility 0s,opacity 0.2s linear;z-index:100}.stats-menu-container:hover .stats-horizontal-menu{visibility:visible;opacity:1}.stats-menu-button{margin-left:8px;white-space:nowrap} .stats-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:linear-gradient(rgba(0,0,0,0.45),rgba(0,0,0,0.55));z-index:99999;display:flex;align-items:center;justify-content:center;animation:fadeInModal .18s;backdrop-filter:blur(20px) saturate(170%);-webkit-backdrop-filter:blur(20px) saturate(170%)} .stats-modal-container{max-width:900px;width:90vw;max-height:90vh;display:flex;flex-direction:column} .stats-modal-content{background:rgba(24,24,24,0.92);border-radius:20px;box-shadow:0 18px 40px rgba(0,0,0,0.45);overflow:hidden;display:flex;flex-direction:column;animation:scaleInModal .18s;border:1.5px solid rgba(255,255,255,0.08);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%)} html[dark] .stats-modal-content{background:rgba(24,24,24,0.92)} html:not([dark]) .stats-modal-content{background:rgba(255,255,255,0.95);color:#222;border:1.25px solid rgba(0,0,0,0.06)} .stats-modal-close{background:transparent;border:none;color:#fff;font-size:36px;line-height:1;width:36px;height:36px;cursor:pointer;transition:transform .15s ease,color .15s;display:flex;align-items:center;justify-content:center;border-radius:8px;padding:0} .stats-modal-close:hover{color:#ff6b6b;transform:scale(1.1)} html:not([dark]) .stats-modal-close{color:#666} html:not([dark]) .stats-modal-close:hover{color:#ff6b6b} .stats-modal-body{padding:24px;overflow-y:auto;flex:1} .stats-thumb-title-centered{font-size:16px;font-weight:600;color:#fff;margin:0 0 12px 0;text-align:center} html:not([dark]) .stats-thumb-title-centered{color:#111} .stats-thumb-row{display:flex;gap:16px;align-items:flex-start;margin-bottom:16px} .stats-thumb-img{width:450px;height:260px;object-fit:cover;border-radius:8px;flex-shrink:0;border:1px solid rgba(255,255,255,0.06)} html:not([dark]) .stats-thumb-img{border:1px solid rgba(0,0,0,0.06)} .stats-thumb-row .stats-grid{flex:1} .stats-thumb-left{display:flex;flex-direction:column;align-items:flex-start;gap:8px} .stats-thumb-left .stats-thumb-sub{font-size:13px;color:rgba(255,255,255,0.65)} html:not([dark]) .stats-thumb-left .stats-thumb-sub{color:rgba(0,0,0,0.6)} .stats-thumb-extras{display:flex;flex-direction:row;gap:10px;align-items:center;margin-top:8px} .stats-thumb-extras .stats-card{padding:8px 10px} .stats-thumb-meta{display:flex;flex-direction:column;justify-content:center} .stats-thumb-sub{font-size:13px;color:rgba(255,255,255,0.65)} html:not([dark]) .stats-thumb-sub{color:rgba(0,0,0,0.6)} .stats-loader{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;color:#fff} html:not([dark]) .stats-loader{color:#666} .stats-spinner{width:60px;height:60px;animation:spin 1s linear infinite;margin-bottom:16px} .stats-spinner circle{stroke-dasharray:80;stroke-dashoffset:60;animation:dash 1.5s ease-in-out infinite} .stats-error{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;color:#ff6b6b;text-align:center} .stats-error-icon{width:60px;height:60px;margin-bottom:16px;stroke:#ff6b6b} .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px} .stats-card{background:rgba(255,255,255,0.05);border-radius:12px;padding:20px;display:flex;align-items:center;gap:16px;border:1px solid rgba(255,255,255,0.1);transition:transform .2s ease,box-shadow .2s ease} html:not([dark]) .stats-card{background:rgba(0,0,0,0.03);border:1px solid rgba(0,0,0,0.1)} .stats-card:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,0.3)} .stats-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0} .stats-icon svg{width:24px;height:24px} .stats-icon-views{background:rgba(59,130,246,0.15);color:#3b82f6} .stats-icon-likes{background:rgba(34,197,94,0.15);color:#22c55e} .stats-icon-dislikes{background:rgba(239,68,68,0.15);color:#ef4444} .stats-icon-comments{background:rgba(168,85,247,0.15);color:#a855f7} .stats-icon-viewers{background:rgba(234,179,8,0.15);color:#eab308} .stats-icon-subscribers{background:rgba(236,72,153,0.15);color:#ec4899} .stats-icon-videos{background:rgba(14,165,233,0.15);color:#0ea5e9} .stats-info{flex:1;min-width:0} .stats-label{font-size:14px;color:rgba(255,255,255,0.7);margin-bottom:4px;font-weight:500} html:not([dark]) .stats-label{color:rgba(0,0,0,0.6)} .stats-value{font-size:28px;font-weight:700;color:#fff;line-height:1.2;margin-bottom:2px} html:not([dark]) .stats-value{color:#111} .stats-exact{font-size:13px;color:rgba(255,255,255,0.5);font-weight:400} html:not([dark]) .stats-exact{color:rgba(0,0,0,0.5)} @keyframes fadeInModal{from{opacity:0}to{opacity:1}} @keyframes scaleInModal{from{transform:scale(0.95);opacity:0}to{transform:scale(1);opacity:1}} @keyframes spin{to{transform:rotate(360deg)}} @keyframes dash{0%{stroke-dashoffset:80}50%{stroke-dashoffset:10}100%{stroke-dashoffset:80}} @media(max-width:768px){.stats-modal-container{width:95vw}.stats-grid{grid-template-columns:1fr}.stats-card{padding:16px}} `; const SETTINGS_KEY = 'youtube_stats_button_enabled'; let statsButtonEnabled = localStorage.getItem(SETTINGS_KEY) !== 'false'; let previousUrl = location.href; let isChecking = false; let experimentalNavListenerKey = null; let channelFeatures = { hasStreams: false, hasShorts: false, }; const rateLimiter = { requests: new Map(), maxRequests: 10, timeWindow: 60000, canRequest: key => { const now = Date.now(); const requests = rateLimiter.requests.get(key) || []; const recentRequests = requests.filter(time => now - time < rateLimiter.timeWindow); if (recentRequests.length >= rateLimiter.maxRequests) { console.warn( `[YouTube+][Stats] Rate limit exceeded for ${key}. Max ${rateLimiter.maxRequests} requests per minute.` ); return false; } recentRequests.push(now); rateLimiter.requests.set(key, recentRequests); return true; }, clear: () => { rateLimiter.requests.clear(); }, }; function addStyles() { if (!document.querySelector('#youtube-enhancer-styles')) { YouTubeUtils.StyleManager.add('youtube-enhancer-styles', styles); } } function isValidVideoId(id) { return id && /^[a-zA-Z0-9_-]{11}$/.test(id); } function getVideoIdFromParams() { const urlParams = new URLSearchParams(window.location.search); const videoId = urlParams.get('v'); return isValidVideoId(videoId) ? `https://www.youtube.com/watch?v=${videoId}` : null; } function getVideoIdFromShorts(url) { const shortsMatch = url.match(/\/shorts\/([^?]+)/); if (shortsMatch && isValidVideoId(shortsMatch[1])) { return `https://www.youtube.com/shorts/${shortsMatch[1]}`; } return null; } function getCurrentVideoUrl() { try { const url = window.location.href; if (!url.includes('youtube.com')) { return null; } const fromParams = getVideoIdFromParams(); if (fromParams) return fromParams; return getVideoIdFromShorts(url); } catch (error) { YouTubeUtils?.logError?.('Stats', 'Failed to get video URL', error); return null; } } function getChannelIdentifier() { try { const url = window.location.href; let identifier = ''; if (url.includes('/channel/')) { identifier = url.split('/channel/')[1].split('/')[0]; } else if (url.includes('/@')) { identifier = url.split('/@')[1].split('/')[0]; } if (identifier && /^[a-zA-Z0-9_-]+$/.test(identifier)) { return identifier; } return ''; } catch (error) { YouTubeUtils?.logError?.('Stats', 'Failed to get channel identifier', error); return ''; } } function validateYouTubeUrl(url) { if (!url || typeof url !== 'string') { return false; } try { const parsedUrl = new URL(url); if (parsedUrl.hostname !== 'www.youtube.com' && parsedUrl.hostname !== 'youtube.com') { console.warn('[YouTube+][Stats] Invalid domain for channel check'); return false; } return true; } catch (error) { YouTubeUtils?.logError?.('Stats', 'Invalid URL for channel check', error); return false; } } async function fetchChannelHtml(url) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const response = await fetch(url, { credentials: 'same-origin', signal: controller.signal, headers: { Accept: 'text/html', }, }); clearTimeout(timeoutId); if (!response.ok) { console.warn(`[YouTube+][Stats] HTTP ${response.status} when checking channel tabs`); return null; } const html = await response.text(); if (html.length > 5000000) { console.warn('[YouTube+][Stats] Response too large, skipping parse'); return null; } return html; } catch (error) { if (error.name === 'AbortError') { console.warn('[YouTube+][Stats] Channel check timed out'); } throw error; } } function extractYouTubeData(html) { const match = html.match(/var ytInitialData = (.+?);<\/script>/); if (!match || !match[1]) { return null; } try { return JSON.parse(match[1]); } catch (parseError) { YouTubeUtils?.logError?.('Stats', 'Failed to parse ytInitialData', parseError); return null; } } function getTabUrl(tab) { return tab?.tabRenderer?.endpoint?.commandMetadata?.webCommandMetadata?.url || null; } function tabMatches(url, pattern) { return typeof url === 'string' && pattern.test(url); } function isStreamsTab(tabUrl) { return tabMatches(tabUrl, /\/streams$/); } function isShortsTab(tabUrl) { return tabMatches(tabUrl, /\/shorts$/); } function hasBothContentTypes(hasStreams, hasShorts) { return hasStreams && hasShorts; } function updateContentTypeFlags(tabUrl, flags) { if (!flags.hasStreams && isStreamsTab(tabUrl)) { flags.hasStreams = true; } if (!flags.hasShorts && isShortsTab(tabUrl)) { flags.hasShorts = true; } } function analyzeChannelTabs(data) { const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || []; const flags = { hasStreams: false, hasShorts: false }; for (const tab of tabs) { const tabUrl = getTabUrl(tab); if (!tabUrl) continue; updateContentTypeFlags(tabUrl, flags); if (hasBothContentTypes(flags.hasStreams, flags.hasShorts)) break; } return flags; } function refreshStatsMenu() { const existingMenu = document.querySelector('.stats-menu-container'); if (existingMenu) { existingMenu.remove(); createStatsMenu(); } } async function checkChannelTabs(url) { if (isChecking) return; if (!validateYouTubeUrl(url)) { return; } if (!rateLimiter.canRequest('checkChannelTabs')) { return; } isChecking = true; try { const html = await fetchChannelHtml(url); if (!html) { isChecking = false; return; } const data = extractYouTubeData(html); if (!data) { isChecking = false; return; } channelFeatures = analyzeChannelTabs(data); refreshStatsMenu(); } catch (error) { YouTubeUtils?.logError?.('Stats', 'Failed to check channel tabs', error); } finally { isChecking = false; } } function isChannelPage(url) { try { return ( url && typeof url === 'string' && url.includes('youtube.com/') && (url.includes('/channel/') || url.includes('/@')) && !url.includes('/video/') && !url.includes('/watch') ); } catch { return false; } } const checkUrlChange = YouTubeUtils?.debounce?.(() => { try { const currentUrl = location.href; if (currentUrl !== previousUrl) { previousUrl = currentUrl; if (isChannelPage(currentUrl)) { setTimeout(() => checkChannelTabs(currentUrl), 500); } } } catch (error) { YouTubeUtils?.logError?.('Stats', 'URL change check failed', error); } }, 300) || function () { try { const currentUrl = location.href; if (currentUrl !== previousUrl) { previousUrl = currentUrl; if (isChannelPage(currentUrl)) { setTimeout(() => checkChannelTabs(currentUrl), 500); } } } catch (error) { console.error('[YouTube+][Stats] URL change check failed:', error); } }; function createStatsIcon() { const icon = document.createElement('div'); icon.className = 'videoStats'; const SVG_NS = window.YouTubePlusConstants?.SVG_NS || 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('viewBox', '0 0 512 512'); const path = document.createElementNS(SVG_NS, 'path'); path.setAttribute( 'd', 'M500 89c13.8-11 16-31.2 5-45s-31.2-16-45-5L319.4 151.5 211.2 70.4c-11.7-8.8-27.8-8.5-39.2 .6L12 199c-13.8 11-16 31.2-5 45s31.2 16 45 5L192.6 136.5l108.2 81.1c11.7 8.8 27.8 8.5 39.2-.6L500 89zM160 256l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32zM32 352l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32zm288-64c-17.7 0-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32zm96-32l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32z' ); svg.appendChild(path); icon.appendChild(svg); icon.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const videoUrl = getCurrentVideoUrl(); if (videoUrl) { const urlParams = new URLSearchParams(new URL(videoUrl).search); const videoId = urlParams.get('v') || videoUrl.match(/\/shorts\/([^?]+)/)?.[1]; if (videoId) { openStatsModal('video', videoId); } } }); return icon; } function insertUniversalIcon() { if (!statsButtonEnabled) return; let masthead = document.querySelector('ytd-masthead.style-scope'); if (!masthead) masthead = document.querySelector('ytd-masthead'); if (!masthead || document.querySelector('.videoStats')) return; const statsIcon = createStatsIcon(); let endElem = masthead.querySelector('#end.style-scope.ytd-masthead'); if (!endElem) endElem = masthead.querySelector('#end'); if (endElem) { endElem.insertBefore(statsIcon, endElem.firstChild); } else { masthead.appendChild(statsIcon); } } function createButton(text, svgPath, viewBox, className, onClick) { const buttonViewModel = document.createElement('button-view-model'); buttonViewModel.className = `yt-spec-button-view-model ${className}-view-model`; const button = document.createElement('button'); button.className = `yt-spec-button-shape-next yt-spec-button-shape-next--outline yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--enable-backdrop-filter-experiment ${className}-button`; button.setAttribute('aria-disabled', 'false'); button.setAttribute('aria-label', text); button.style.display = 'flex'; button.style.alignItems = 'center'; button.style.justifyContent = 'center'; button.style.gap = '8px'; button.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); onClick(); }); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', viewBox); svg.style.width = '20px'; svg.style.height = '20px'; svg.style.fill = 'currentColor'; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', svgPath); svg.appendChild(path); const buttonText = document.createElement('div'); buttonText.className = `yt-spec-button-shape-next__button-text-content ${className}-text`; buttonText.textContent = text; buttonText.style.display = 'flex'; buttonText.style.alignItems = 'center'; const touchFeedback = document.createElement('yt-touch-feedback-shape'); touchFeedback.style.borderRadius = 'inherit'; const touchFeedbackDiv = document.createElement('div'); touchFeedbackDiv.className = 'yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response'; touchFeedbackDiv.setAttribute('aria-hidden', 'true'); const strokeDiv = document.createElement('div'); strokeDiv.className = 'yt-spec-touch-feedback-shape__stroke'; const fillDiv = document.createElement('div'); fillDiv.className = 'yt-spec-touch-feedback-shape__fill'; touchFeedbackDiv.appendChild(strokeDiv); touchFeedbackDiv.appendChild(fillDiv); touchFeedback.appendChild(touchFeedbackDiv); button.appendChild(svg); button.appendChild(buttonText); button.appendChild(touchFeedback); buttonViewModel.appendChild(button); return buttonViewModel; } const INNERTUBE_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; const INNERTUBE_CLIENT_VERSION = '2.20201209.01.00'; function createInnerTubeRequestBody(videoId) { return { context: { client: { clientName: 'WEB', clientVersion: INNERTUBE_CLIENT_VERSION, hl: 'en', gl: 'US', }, }, videoId, }; } function createInnerTubeFetchOptions(videoId) { return { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-YouTube-Client-Name': '1', 'X-YouTube-Client-Version': INNERTUBE_CLIENT_VERSION, }, body: JSON.stringify(createInnerTubeRequestBody(videoId)), }; } function extractThumbnailUrl(details) { const thumbnails = details.thumbnail?.thumbnails; return thumbnails?.[thumbnails.length - 1]?.url || null; } function parseVideoStatsFromResponse(data) { const details = data.videoDetails || {}; const microformat = data.microformat?.playerMicroformatRenderer || {}; return { videoId: details.videoId, title: details.title, views: details.viewCount ? parseInt(details.viewCount, 10) : null, likes: null, thumbnail: extractThumbnailUrl(details), duration: details.lengthSeconds, country: microformat.availableCountries?.[0] || null, monetized: microformat.isFamilySafe !== undefined, channelId: details.channelId, }; } async function fetchVideoStatsInnerTube(videoId) { if (!videoId) return null; try { const url = `https://www.youtube.com/youtubei/v1/player?key=${INNERTUBE_API_KEY}&prettyPrint=false`; const response = await fetch(url, createInnerTubeFetchOptions(videoId)); if (!response.ok) { console.warn(`[YouTube+][Stats] InnerTube API failed:`, response.status); return null; } const data = await response.json(); return parseVideoStatsFromResponse(data); } catch (error) { console.error('[YouTube+][Stats] InnerTube fetch error:', error); return null; } } async function fetchDislikesData(videoId) { if (!videoId) return null; try { const response = await fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${videoId}`); if (!response.ok) return null; const data = await response.json(); return { likes: data.likes || null, dislikes: data.dislikes || null, rating: data.rating || null, }; } catch (error) { console.error('[YouTube+][Stats] Failed to fetch dislikes:', error); return null; } } async function fetchStats(type, id) { if (!id) return { ok: false, status: 0, data: null }; try { if (type === 'video') { const videoData = await fetchVideoStatsInnerTube(id); if (!videoData) { return { ok: false, status: 404, data: null }; } const dislikeData = await fetchDislikesData(id); if (dislikeData) { videoData.likes = dislikeData.likes; videoData.dislikes = dislikeData.dislikes; videoData.rating = dislikeData.rating; } return { ok: true, status: 200, data: videoData }; } const endpoint = `https://api.livecounts.io/youtube-live-subscriber-counter/stats/${id}`; const response = await fetch(endpoint, { method: 'GET', headers: { Accept: 'application/json', }, }); if (!response.ok) { console.warn(`[YouTube+][Stats] Failed to fetch ${type} stats:`, response.status); return { ok: false, status: response.status, data: null, url: endpoint }; } const data = await response.json(); return { ok: true, status: response.status, data, url: endpoint }; } catch (error) { YouTubeUtils?.logError?.('Stats', `Failed to fetch ${type} stats`, error); return { ok: false, status: 0, data: null }; } } function getPageVideoStats() { try { const helpers = window.YouTubeStatsHelpers || {}; if (!helpers.extractViews) { YouTubeUtils?.logError?.( 'Stats', 'YouTubeStatsHelpers not loaded', new Error('Missing helpers') ); return null; } const result = { ...helpers.extractViews(), ...helpers.extractLikes(), ...helpers.extractDislikes(), ...helpers.extractComments(), ...helpers.extractSubscribers(), ...helpers.extractThumbnail(), ...helpers.extractTitle(), }; return Object.keys(result).length > 0 ? result : null; } catch (e) { YouTubeUtils?.logError?.('Stats', 'Failed to read page stats', e); return null; } } function buildPageStatCard(value, labelKey, iconClass, iconSvg) { if (value === undefined || value === null) return ''; return `
${iconSvg}
${t(labelKey)}
${formatNumber(value)}
${(value || 0).toLocaleString()}
`; } function buildValueOnlyCard( value, iconOrClass = '', options = { showValue: true, showIcon: true } ) { const { showValue, showIcon } = options; if (!showValue && !showIcon) return ''; let displayVal = ''; if (showValue) { displayVal = value === undefined || value === null ? t('unknown') : value; } let iconContent = ''; let extraClass = ''; if (showIcon) { if (iconOrClass && typeof iconOrClass === 'string' && iconOrClass.indexOf('<') >= 0) { iconContent = iconOrClass; } else if (iconOrClass && typeof iconOrClass === 'string') { extraClass = ` ${iconOrClass}`; } } return `
${iconContent}
${showValue ? `
${displayVal}
` : ''}
`; } function buildStatCards(pageStats) { const cardConfigs = [ { value: pageStats.views, key: 'views', icon: 'stats-icon-views', svg: '', }, { value: pageStats.likes, key: 'likes', icon: 'stats-icon-likes', svg: '', }, { value: pageStats.dislikes, key: 'dislikes', icon: 'stats-icon-dislikes', svg: '', }, { value: pageStats.comments, key: 'comments', icon: 'stats-icon-comments', svg: '', }, ]; return cardConfigs .map(config => buildPageStatCard(config.value, config.key, config.icon, config.svg)) .filter(card => card); } function getThumbnailUrl(id, pageStats) { if (pageStats && pageStats.thumbnail) { return pageStats.thumbnail; } if (id) { return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; } return ''; } function buildExtraCards(extras) { const monetizationText = extras.monetization || t('unknown'); const countryText = extras.country || t('unknown'); const durationText = extras.duration || t('unknown'); const extraMonCard = buildValueOnlyCard(monetizationText, 'stats-icon-subscribers', { showValue: false, showIcon: true, }); const extraCountryCard = buildValueOnlyCard(countryText, 'stats-icon-views', { showValue: false, showIcon: true, }); const extraDurationCard = buildValueOnlyCard(durationText, 'stats-icon-videos', { showValue: true, showIcon: false, }); return `${extraMonCard}${extraCountryCard}${extraDurationCard}`; } function buildThumbnailLayout(titleHtml, thumbUrl, gridHtml, extras) { const extraCards = buildExtraCards(extras); const leftHtml = `
thumbnail
${extraCards}
`; return `${titleHtml}
${leftHtml}${gridHtml}
`; } function renderPageFallback(container, pageStats, id) { const cards = buildStatCards(pageStats); const gridHtml = `
${cards.join('')}
`; const title = (pageStats && pageStats.title) || document.title || ''; const titleHtml = title ? `
${title}
` : ''; const thumbUrl = getThumbnailUrl(id, pageStats); const extras = getVideoExtras(null, pageStats, id); if (thumbUrl) { container.innerHTML = buildThumbnailLayout(titleHtml, thumbUrl, gridHtml, extras); } else { container.innerHTML = `${titleHtml}${gridHtml}`; } } function formatNumber(num) { if (!num || isNaN(num)) return '0'; const absNum = Math.abs(num); if (absNum >= 1e9) { return `${(num / 1e9).toFixed(1)}B`; } if (absNum >= 1e6) { return `${(num / 1e6).toFixed(1)}M`; } if (absNum >= 1e3) { return `${(num / 1e3).toFixed(1)}K`; } return num.toLocaleString(); } function makeStatsCard(labelKey, value, exact, iconClass, iconSvg) { const display = value == null ? t('unknown') : formatNumber(value); const exactText = exact !== null && exact !== undefined ? exact.toLocaleString() : '—'; return `
${iconSvg}
${t(labelKey)}
${display}
${exactText}
`; } function getFirstAvailableField(stats, ...fields) { for (const field of fields) { if (stats?.[field] != null) return stats[field]; } return null; } function getThumbnailUrl(stats, id) { return stats?.thumbnail || (id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : ''); } function extractVideoFields(stats, id) { return { views: getFirstAvailableField(stats, 'liveViews', 'views', 'viewCount'), likes: getFirstAvailableField(stats, 'liveLikes', 'likes', 'likeCount'), dislikes: getFirstAvailableField(stats, 'dislikes', 'liveDislikes', 'dislikeCount'), comments: getFirstAvailableField(stats, 'liveComments', 'comments', 'commentCount'), liveViewer: getFirstAvailableField(stats, 'liveViewer', 'live_viewers'), title: stats?.title || document.title || '', thumbUrl: getThumbnailUrl(stats, id), country: getFirstAvailableField(stats, 'country'), monetized: stats?.monetized ?? null, duration: getFirstAvailableField(stats, 'duration'), }; } function mergeVideoStats(apiStats, pageStats) { if (!pageStats) return apiStats || {}; const getValue = (...fields) => { for (const field of fields) { if (apiStats?.[field] != null) return apiStats[field]; } for (const field of fields) { if (pageStats?.[field] != null) return pageStats[field]; } return null; }; return { ...apiStats, views: getValue('views', 'viewCount'), likes: getValue('likes', 'likeCount'), dislikes: getValue('dislikes'), comments: getValue('comments', 'commentCount'), thumbnail: getValue('thumbnail'), title: getValue('title'), liveViewer: getValue('liveViewer'), }; } function getVideoExtras(apiStats, pageStats) { const helpers = window.YouTubeStatsHelpers || {}; const duration = helpers.getDurationFromSources?.(apiStats) || null; const country = helpers.getCountryFromSources?.(apiStats, pageStats) || null; const monetization = helpers.getMonetizationFromSources?.(apiStats, pageStats, t) || null; return { duration, country, monetization }; } function createStatsModalCloseButton(overlay) { const closeBtn = document.createElement('button'); closeBtn.className = 'thumbnail-modal-close thumbnail-modal-action-btn'; closeBtn.innerHTML = ` `; closeBtn.title = t('close'); closeBtn.setAttribute('aria-label', t('close')); closeBtn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); overlay.remove(); }); return closeBtn; } function createLoadingSpinner() { const loader = document.createElement('div'); loader.className = 'stats-loader'; loader.innerHTML = `

${t('loadingStats')}

`; return loader; } function createStatsModalStructure(overlay) { const container = document.createElement('div'); container.className = 'stats-modal-container'; const content = document.createElement('div'); content.className = 'stats-modal-content'; const body = document.createElement('div'); body.className = 'stats-modal-body'; body.appendChild(createLoadingSpinner()); content.appendChild(body); const wrapper = document.createElement('div'); wrapper.className = 'thumbnail-modal-wrapper'; const actionsDiv = document.createElement('div'); actionsDiv.className = 'thumbnail-modal-actions'; actionsDiv.appendChild(createStatsModalCloseButton(overlay)); wrapper.appendChild(content); wrapper.appendChild(actionsDiv); container.appendChild(wrapper); return { body, container }; } function setupModalEventHandlers(overlay) { overlay.addEventListener('click', ({ target }) => { if (target === overlay) overlay.remove(); }); function escHandler(e) { if (e.key === 'Escape') { overlay.remove(); window.removeEventListener('keydown', escHandler, true); } } window.addEventListener('keydown', escHandler, true); } function renderErrorMessage(body, result) { const statusText = result?.status ? ` (${result.status})` : ''; const endpointHint = result?.url ? `
${result.url}
` : ''; body.innerHTML = `

${t('failedToLoadStats')}${statusText}

${endpointHint}
`; } function handleFailedFetch(body, result, id) { const pageStats = getPageVideoStats(); if (pageStats) { renderPageFallback(body, pageStats, id); } else { renderErrorMessage(body, result); } } function displayStatsBasedOnType(body, type, stats, id) { if (type === 'video') { try { const pageStats = getPageVideoStats(); const merged = mergeVideoStats(stats, pageStats); displayVideoStats(body, merged, id); } catch { displayVideoStats(body, stats, id); } } else { displayChannelStats(body, stats); } } async function openStatsModal(type, id) { if (!type || !id) { console.error('[YouTube+][Stats] Invalid parameters for modal'); return; } const existingOverlays = document.querySelectorAll('.stats-modal-overlay'); for (let i = 0; i < existingOverlays.length; i++) { try { existingOverlays[i].remove(); } catch { } } const overlay = document.createElement('div'); overlay.className = 'stats-modal-overlay'; const { body, container } = createStatsModalStructure(overlay); overlay.appendChild(container); setupModalEventHandlers(overlay); document.body.appendChild(overlay); const result = await fetchStats(type, id); if (!result?.ok) { handleFailedFetch(body, result, id); return; } displayStatsBasedOnType(body, type, result.data, id); } function getVideoStatDefinitions(fields) { const { views, likes, dislikes, comments } = fields; return [ { label: 'views', value: views, exact: views, iconClass: 'stats-icon-views', iconSvg: ``, }, { label: 'likes', value: likes, exact: likes, iconClass: 'stats-icon-likes', iconSvg: ``, }, { label: 'dislikes', value: dislikes, exact: dislikes, iconClass: 'stats-icon-dislikes', iconSvg: ``, }, { label: 'comments', value: comments, exact: comments, iconClass: 'stats-icon-comments', iconSvg: ``, }, ]; } function createLiveViewerCard(liveViewer) { if (liveViewer === undefined || liveViewer === null) return ''; return makeStatsCard( 'liveViewers', liveViewer, liveViewer, 'stats-icon-viewers', `` ); } function createMonetizationCard(extras, stats) { if (!extras.monetization) return ''; const isMonetized = extras.monetization === t('yes') || stats.monetized === true; const monIcon = isMonetized ? `` : ``; return `
${monIcon}
${t('monetization')}
${extras.monetization}
`; } function createCountryCard(extras) { if (!extras.country || extras.country === t('unknown')) return ''; const countryCode = extras.country.toUpperCase(); const flagUrl = `https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.3.2/flags/4x3/${countryCode.toLowerCase()}.svg`; return `
${countryCode}
${t('country')}
${countryCode}
`; } function createDurationCard(extras) { if (!extras.duration || extras.duration === t('unknown')) return ''; const durationIcon = ``; return `
${durationIcon}
${t('duration')}
${extras.duration}
`; } function buildMetaCardsHtml(stats, extras) { const cards = [ createMonetizationCard(extras, stats), createCountryCard(extras), createDurationCard(extras), ]; return cards.filter(Boolean).join(''); } function displayVideoStats(container, stats, id) { const fields = extractVideoFields(stats, id); const { liveViewer, title, thumbUrl } = fields; const titleHtml = title ? `
${title}
` : ''; const defs = getVideoStatDefinitions(fields); const parts = defs.map(d => makeStatsCard(d.label, d.value, d.exact, d.iconClass, d.iconSvg)); const liveViewerCard = createLiveViewerCard(liveViewer); if (liveViewerCard) parts.push(liveViewerCard); const gridHtml = `
${parts.join('')}
`; if (thumbUrl) { const extras = getVideoExtras(stats, null); const metaCardsHtml = buildMetaCardsHtml(stats, extras); const metaExtrasHtml = metaCardsHtml ? `
${metaCardsHtml}
` : ''; const leftHtml = `
thumbnail${metaExtrasHtml}
`; container.innerHTML = `${titleHtml}
${leftHtml}${gridHtml}
`; } else { container.innerHTML = `${titleHtml}${gridHtml}`; } } function displayChannelStats(container, stats) { const { liveSubscriber, liveViews, liveVideos } = stats; container.innerHTML = `
${t('subscribers')}
${formatNumber(liveSubscriber)}
${(liveSubscriber || 0).toLocaleString()}
${t('totalViews')}
${formatNumber(liveViews)}
${(liveViews || 0).toLocaleString()}
${t('totalVideos')}
${formatNumber(liveVideos)}
${(liveVideos || 0).toLocaleString()}
`; } function createStatsMenu() { if (!statsButtonEnabled) return undefined; if (document.querySelector('.stats-menu-container')) { return undefined; } const containerDiv = document.createElement('div'); containerDiv.className = 'yt-flexible-actions-view-model-wiz__action stats-menu-container'; const mainButtonViewModel = document.createElement('button-view-model'); mainButtonViewModel.className = 'yt-spec-button-view-model main-stats-view-model'; const mainButton = document.createElement('button'); mainButton.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--outline yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--enable-backdrop-filter-experiment main-stats-button'; mainButton.setAttribute('aria-disabled', 'false'); mainButton.setAttribute('aria-label', t('stats')); mainButton.style.display = 'flex'; mainButton.style.alignItems = 'center'; mainButton.style.justifyContent = 'center'; mainButton.style.gap = '8px'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 512 512'); svg.style.width = '20px'; svg.style.height = '20px'; svg.style.fill = 'currentColor'; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute( 'd', 'M500 89c13.8-11 16-31.2 5-45s-31.2-16-45-5L319.4 151.5 211.2 70.4c-11.7-8.8-27.8-8.5-39.2 .6L12 199c-13.8 11-16 31.2-5 45s31.2 16 45 5L192.6 136.5l108.2 81.1c11.7 8.8 27.8 8.5 39.2-.6L500 89zM160 256l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32zM32 352l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32zm288-64c-17.7 0-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32zm96-32l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32z' ); svg.appendChild(path); const buttonText = document.createElement('div'); buttonText.className = 'yt-spec-button-shape-next__button-text-content main-stats-text'; buttonText.textContent = t('stats'); buttonText.style.display = 'flex'; buttonText.style.alignItems = 'center'; const touchFeedback = document.createElement('yt-touch-feedback-shape'); touchFeedback.style.borderRadius = 'inherit'; const touchFeedbackDiv = document.createElement('div'); touchFeedbackDiv.className = 'yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response'; touchFeedbackDiv.setAttribute('aria-hidden', 'true'); const strokeDiv = document.createElement('div'); strokeDiv.className = 'yt-spec-touch-feedback-shape__stroke'; const fillDiv = document.createElement('div'); fillDiv.className = 'yt-spec-touch-feedback-shape__fill'; touchFeedbackDiv.appendChild(strokeDiv); touchFeedbackDiv.appendChild(fillDiv); touchFeedback.appendChild(touchFeedbackDiv); mainButton.appendChild(svg); mainButton.appendChild(buttonText); mainButton.appendChild(touchFeedback); mainButtonViewModel.appendChild(mainButton); containerDiv.appendChild(mainButtonViewModel); const horizontalMenu = document.createElement('div'); horizontalMenu.className = 'stats-horizontal-menu'; const channelButtonContainer = document.createElement('div'); channelButtonContainer.className = 'stats-menu-button channel-stats-container'; const channelButton = createButton( t('channel'), 'M64 48c-8.8 0-16 7.2-16 16l0 288c0 8.8 7.2 16 16 16l512 0c8.8 0 16-7.2 16-16l0-288c0-8.8-7.2-16-16-16L64 48zM0 64C0 28.7 28.7 0 64 0L576 0c35.3 0 64 28.7 64 64l0 288c0 35.3-28.7 64-64 64L64 416c-35.3 0-64-28.7-64-64L0 64zM120 464l400 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-400 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z', '0 0 640 512', 'channel-stats', () => { const channelId = getChannelIdentifier(); if (channelId) { openStatsModal('channel', channelId); } } ); channelButtonContainer.appendChild(channelButton); horizontalMenu.appendChild(channelButtonContainer); if (channelFeatures.hasStreams) { const liveButtonContainer = document.createElement('div'); liveButtonContainer.className = 'stats-menu-button live-stats-container'; const liveButton = createButton( t('live'), 'M99.8 69.4c10.2 8.4 11.6 23.6 3.2 33.8C68.6 144.7 48 197.9 48 256s20.6 111.3 55 152.8c8.4 10.2 7 25.3-3.2 33.8s-25.3 7-33.8-3.2C24.8 389.6 0 325.7 0 256S24.8 122.4 66 72.6c8.4-10.2 23.6-11.6 33.8-3.2zm376.5 0c10.2-8.4 25.3-7 33.8 3.2c41.2 49.8 66 113.8 66 183.4s-24.8 133.6-66 183.4c-8.4 10.2-23.6 11.6-33.8 3.2s-11.6-23.6-3.2-33.8c34.3-41.5 55-94.7 55-152.8s-20.6-111.3-55-152.8c-8.4-10.2-7-25.3 3.2-33.8zM248 256a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zm-61.1-78.5C170 199.2 160 226.4 160 256s10 56.8 26.9 78.5c8.1 10.5 6.3 25.5-4.2 33.7s-25.5 6.3-33.7-4.2c-23.2-29.8-37-67.3-37-108s13.8-78.2 37-108c8.1-10.5 23.2-12.3 33.7-4.2s12.3 23.2 4.2 33.7zM427 148c23.2 29.8 37 67.3 37 108s-13.8 78.2-37 108c-8.1 10.5-23.2 12.3-33.7 4.2s-12.3-23.2-4.2-33.7C406 312.8 416 285.6 416 256s-10-56.8-26.9-78.5c-8.1-10.5-6.3-25.5 4.2-33.7s25.5-6.3 33.7 4.2z', '0 0 576 512', 'live-stats', () => { const channelId = getChannelIdentifier(); if (channelId) { openStatsModal('channel', channelId); } } ); liveButtonContainer.appendChild(liveButton); horizontalMenu.appendChild(liveButtonContainer); } if (channelFeatures.hasShorts) { const shortsButtonContainer = document.createElement('div'); shortsButtonContainer.className = 'stats-menu-button shorts-stats-container'; const shortsButton = createButton( t('shorts'), 'M80 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16l224 0c8.8 0 16-7.2 16-16l0-384c0-8.8-7.2-16-16-16L80 48zM16 64C16 28.7 44.7 0 80 0L304 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L80 512c-35.3 0-64-28.7-64-64L16 64zM160 400l64 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-64 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z', '0 0 384 512', 'shorts-stats', () => { const channelId = getChannelIdentifier(); if (channelId) { openStatsModal('channel', channelId); } } ); shortsButtonContainer.appendChild(shortsButton); horizontalMenu.appendChild(shortsButtonContainer); } containerDiv.appendChild(horizontalMenu); const joinButton = document.querySelector( '.yt-flexible-actions-view-model-wiz__action:not(.stats-menu-container)' ); if (joinButton) { joinButton.parentNode.appendChild(containerDiv); } else { const buttonContainer = document.querySelector('#subscribe-button + #buttons'); if (buttonContainer) { buttonContainer.appendChild(containerDiv); } } return containerDiv; } function checkAndAddMenu() { if (!statsButtonEnabled) return; const joinButton = document.querySelector( '.yt-flexible-actions-view-model-wiz__action:not(.stats-menu-container)' ); const statsMenu = document.querySelector('.stats-menu-container'); if (joinButton && !statsMenu) { createStatsMenu(); } } function checkAndInsertIcon() { if (!statsButtonEnabled) return; insertUniversalIcon(); } function addSettingsUI() { const section = document.querySelector( '.ytp-plus-settings-section[data-section="experimental"]' ); if (!section || section.querySelector('.stats-button-settings-item')) return; const item = document.createElement('div'); item.className = 'ytp-plus-settings-item stats-button-settings-item'; item.innerHTML = `
${t('statisticsButtonDescription')}
`; section.appendChild(item); item.querySelector('input').addEventListener('change', e => { const { target } = e; const input = (target); statsButtonEnabled = input.checked; localStorage.setItem(SETTINGS_KEY, statsButtonEnabled ? 'true' : 'false'); document.querySelectorAll('.videoStats,.stats-menu-container').forEach(el => el.remove()); if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } }); } const settingsObserver = new MutationObserver(mutations => { for (const { addedNodes } of mutations) { for (const node of addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { setTimeout(addSettingsUI, 50); } } } if (document.querySelector('.ytp-plus-settings-nav-item[data-section="experimental"].active')) { setTimeout(addSettingsUI, 50); } }); YouTubeUtils.cleanupManager.registerObserver(settingsObserver); if (document.body) { settingsObserver.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { settingsObserver.observe(document.body, { childList: true, subtree: true }); }); } const handleExperimentalNavClick = e => { const { target } = e; const el = (target); if ( el.classList?.contains('ytp-plus-settings-nav-item') && el.dataset?.section === 'experimental' ) { setTimeout(addSettingsUI, 50); } }; if (!experimentalNavListenerKey) { experimentalNavListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', handleExperimentalNavClick, true ); } function init() { addStyles(); if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } history.pushState = (function (f) { return function (...args) { const fAny = (f); const result = fAny.call(this, ...args); checkUrlChange(); return result; }; })(history.pushState); history.replaceState = (function (f) { return function (...args) { const fAny = (f); const result = fAny.call(this, ...args); checkUrlChange(); return result; }; })(history.replaceState); window.addEventListener('popstate', checkUrlChange); if (isChannelPage(location.href)) { checkChannelTabs(location.href); } } const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'childList') { if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } } } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.addEventListener('yt-navigate-finish', () => { if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); if (isChannelPage(location.href)) { checkChannelTabs(location.href); } } }); document.addEventListener('yt-action', event => { const ev = (event); if (ev.detail && ev.detail.actionName === 'yt-reload-continuation-items-command') { if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } } }); })(); (function () { 'use strict'; const isStudioPageCount = () => { try { const host = location.hostname || ''; const href = location.href || ''; return ( host.includes('studio.youtube.com') || host.includes('studio.') || href.includes('studio.youtube.com') ); } catch { return false; } }; if (isStudioPageCount()) return; const _globalI18n_stats = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n_stats && typeof _globalI18n_stats.t === 'function') { return _globalI18n_stats.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const CONFIG = { OPTIONS: ['subscribers', 'views', 'videos'], FONT_LINK: 'https://fonts.googleapis.com/css2?family=Rubik:wght@400;700&display=swap', STATS_API_URL: 'https://api.livecounts.io/youtube-live-subscriber-counter/stats/', DEFAULT_UPDATE_INTERVAL: 2000, DEFAULT_OVERLAY_OPACITY: 0.75, MAX_RETRIES: 3, CACHE_DURATION: 300000, DEBOUNCE_DELAY: 100, STORAGE_KEY: 'youtube_channel_stats_settings', }; const state = { overlay: null, isUpdating: false, intervalId: null, currentChannelName: null, enabled: localStorage.getItem(CONFIG.STORAGE_KEY) !== 'false', updateInterval: parseInt(localStorage.getItem('youtubeEnhancerInterval'), 10) || CONFIG.DEFAULT_UPDATE_INTERVAL, overlayOpacity: parseFloat(localStorage.getItem('youtubeEnhancerOpacity')) || CONFIG.DEFAULT_OVERLAY_OPACITY, lastSuccessfulStats: new Map(), previousStats: new Map(), previousUrl: location.href, isChecking: false, documentListenerKeys: new Set(), }; const utils = { log: (message, ...args) => { console.log('[YouTube+][Stats]', message, ...args); }, warn: (message, ...args) => { console.warn('[YouTube+][Stats]', message, ...args); }, error: (message, ...args) => { console.error('[YouTube+][Stats]', message, ...args); }, debounce: window.YouTubeUtils?.debounce || ((func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }), }; const { OPTIONS } = CONFIG; const { FONT_LINK } = CONFIG; const { STATS_API_URL } = CONFIG; async function fetchChannel(url) { if (state.isChecking) return null; state.isChecking = true; try { const response = await fetch(url, { credentials: 'same-origin', }); if (!response.ok) return null; const html = await response.text(); const match = html.match(/var ytInitialData = (.+?);<\/script>/); return match && match[1] ? JSON.parse(match[1]) : null; } catch (error) { utils.warn('Failed to fetch channel data:', error); return null; } finally { state.isChecking = false; } } async function getChannelInfo(url) { const data = await fetchChannel(url); if (!data) return null; try { const channelName = data?.metadata?.channelMetadataRenderer?.title || t('unknown'); const channelId = data?.metadata?.channelMetadataRenderer?.externalId || null; return { channelName, channelId }; } catch { return null; } } function isChannelPageUrl(url) { return ( url.includes('youtube.com/') && (url.includes('/channel/') || url.includes('/@')) && !url.includes('/video/') && !url.includes('/watch') ); } function checkUrlChange() { const currentUrl = location.href; if (currentUrl !== state.previousUrl) { state.previousUrl = currentUrl; if (isChannelPageUrl(currentUrl)) { setTimeout(() => getChannelInfo(currentUrl), 500); } } } history.pushState = (function (f) { return function (...args) { f.call(this, ...args); checkUrlChange(); }; })(history.pushState); history.replaceState = (function (f) { return function (...args) { f.call(this, ...args); checkUrlChange(); }; })(history.replaceState); window.addEventListener('popstate', checkUrlChange); setInterval(checkUrlChange, 1000); function init() { try { utils.log('Initializing YouTube Enhancer v1.6'); loadFonts(); initializeLocalStorage(); addStyles(); if (state.enabled) { observePageChanges(); addNavigationListener(); if (isChannelPageUrl(location.href)) { getChannelInfo(location.href); } } utils.log('YouTube Enhancer initialized successfully'); } catch (error) { utils.error('Failed to initialize YouTube Enhancer:', error); } } function loadFonts() { const fontLink = document.createElement('link'); fontLink.rel = 'stylesheet'; fontLink.href = FONT_LINK; (document.head || document.documentElement).appendChild(fontLink); } function initializeLocalStorage() { OPTIONS.forEach(option => { if (localStorage.getItem(`show-${option}`) === null) { localStorage.setItem(`show-${option}`, 'true'); } }); } function addStyles() { const styles = ` .channel-banner-overlay{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:12px;z-index:10;display:flex;justify-content:space-around;align-items:center;color:#fff;font-family:var(--stats-font-family,'Rubik',sans-serif);font-size:var(--stats-font-size,24px);box-sizing:border-box;transition:background-color .3s ease;backdrop-filter:blur(2px)} .settings-button{position:absolute;top:8px;right:8px;width:24px;height:24px;cursor:pointer;z-index:2;transition:transform .2s;opacity:.7} .settings-button:hover{transform:scale(1.1);opacity:1} .settings-menu{position:absolute;top:35px;right:8px;background:rgba(0,0,0,.95);padding:12px;border-radius:8px;z-index:10;display:none;backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.1);min-width:320px} .settings-menu.show{display:block} .stat-container{display:flex;flex-direction:column;align-items:center;justify-content:center;visibility:hidden;width:33%;height:100%;padding:0 1rem} .number-container{display:flex;align-items:center;justify-content:center;font-weight:700;min-height:3rem} .label-container{display:flex;align-items:center;margin-top:.5rem;font-size:1.2rem;opacity:.9} .label-container svg{width:1.5rem;height:1.5rem;margin-right:.5rem} .difference{font-size:1.8rem;height:2rem;margin-bottom:.5rem;transition:opacity .3s} .spinner-container{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center} .loading-spinner{animation:spin 1s linear infinite} @keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} @media(max-width:768px){.channel-banner-overlay{flex-direction:column;padding:8px;min-height:160px}.settings-menu{width:280px;right:4px}} .setting-group{margin-bottom:12px} .setting-group:last-child{margin-bottom:0} .setting-group label{display:block;margin-bottom:4px;font-weight:600;color:#fff;font-size:14px} .setting-group input[type="range"]{width:100%;margin:4px 0} .setting-group input[type="checkbox"]{margin-right:8px} .setting-value{color:#aaa;font-size:12px;margin-top:2px} `; YouTubeUtils.StyleManager.add('channel-stats-overlay', styles); } function createSettingsButton() { const button = document.createElement('div'); button.className = 'settings-button'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svg.setAttribute('viewBox', '0 0 512 512'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill', 'white'); path.setAttribute( 'd', 'M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z' ); svg.appendChild(path); button.appendChild(svg); return button; } function createSettingsMenu() { const menu = document.createElement('div'); menu.className = 'settings-menu'; menu.style.gap = '15px'; menu.style.width = '360px'; menu.setAttribute('tabindex', '-1'); menu.setAttribute('aria-modal', 'true'); const displaySection = createDisplaySection(); const controlsSection = createControlsSection(); menu.appendChild(displaySection); menu.appendChild(controlsSection); return menu; } function createDisplaySection() { const displaySection = document.createElement('div'); displaySection.style.flex = '1'; const displayLabel = document.createElement('label'); displayLabel.textContent = t('displayOptions'); displayLabel.style.marginBottom = '10px'; displayLabel.style.display = 'block'; displayLabel.style.fontSize = '16px'; displayLabel.style.fontWeight = 'bold'; displaySection.appendChild(displayLabel); OPTIONS.forEach(option => { const checkboxContainer = document.createElement('div'); checkboxContainer.style.display = 'flex'; checkboxContainer.style.alignItems = 'center'; checkboxContainer.style.marginTop = '5px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `show-${option}`; checkbox.checked = localStorage.getItem(`show-${option}`) !== 'false'; checkbox.className = 'ytp-plus-settings-checkbox'; const checkboxLabel = document.createElement('label'); checkboxLabel.htmlFor = `show-${option}`; checkboxLabel.textContent = t(option); checkboxLabel.style.cursor = 'pointer'; checkboxLabel.style.color = 'white'; checkboxLabel.style.fontSize = '14px'; checkboxLabel.style.marginLeft = '8px'; checkbox.addEventListener('change', () => { localStorage.setItem(`show-${option}`, String(checkbox.checked)); updateDisplayState(); }); checkboxContainer.appendChild(checkbox); checkboxContainer.appendChild(checkboxLabel); displaySection.appendChild(checkboxContainer); }); return displaySection; } function createControlsSection() { const controlsSection = document.createElement('div'); controlsSection.style.flex = '1'; const fontLabel = document.createElement('label'); fontLabel.textContent = t('fontFamily'); fontLabel.style.display = 'block'; fontLabel.style.marginBottom = '5px'; fontLabel.style.fontSize = '16px'; fontLabel.style.fontWeight = 'bold'; const fontSelect = document.createElement('select'); fontSelect.className = 'font-family-select'; fontSelect.style.width = '100%'; fontSelect.style.marginBottom = '10px'; const fonts = [ { name: 'Rubik', value: 'Rubik, sans-serif' }, { name: 'Impact', value: 'Impact, Charcoal, sans-serif' }, { name: 'Verdana', value: 'Verdana, Geneva, sans-serif' }, { name: 'Tahoma', value: 'Tahoma, Geneva, sans-serif' }, ]; const savedFont = localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif'; fonts.forEach(f => { const opt = document.createElement('option'); opt.value = f.value; opt.textContent = f.name; if (f.value === savedFont) opt.selected = true; fontSelect.appendChild(opt); }); fontSelect.addEventListener('change', e => { const { target } = e; const select = (target); localStorage.setItem('youtubeEnhancerFontFamily', select.value); if (state.overlay) { state.overlay .querySelectorAll('.subscribers-number,.views-number,.videos-number') .forEach(el => { el.style.fontFamily = select.value; }); } }); const fontSizeLabel = document.createElement('label'); fontSizeLabel.textContent = t('fontSize'); fontSizeLabel.style.display = 'block'; fontSizeLabel.style.marginBottom = '5px'; fontSizeLabel.style.fontSize = '16px'; fontSizeLabel.style.fontWeight = 'bold'; const fontSizeSlider = document.createElement('input'); fontSizeSlider.type = 'range'; fontSizeSlider.min = '16'; fontSizeSlider.max = '72'; fontSizeSlider.value = localStorage.getItem('youtubeEnhancerFontSize') || '24'; fontSizeSlider.step = '1'; fontSizeSlider.className = 'font-size-slider'; const fontSizeValue = document.createElement('div'); fontSizeValue.className = 'font-size-value'; fontSizeValue.textContent = `${fontSizeSlider.value}px`; fontSizeValue.style.fontSize = '14px'; fontSizeValue.style.marginBottom = '15px'; fontSizeSlider.addEventListener('input', e => { const { target } = e; const input = (target); fontSizeValue.textContent = `${input.value}px`; localStorage.setItem('youtubeEnhancerFontSize', input.value); if (state.overlay) { state.overlay .querySelectorAll('.subscribers-number,.views-number,.videos-number') .forEach(el => { el.style.fontSize = `${input.value}px`; }); } }); const intervalLabel = document.createElement('label'); intervalLabel.textContent = t('updateInterval'); intervalLabel.style.display = 'block'; intervalLabel.style.marginBottom = '5px'; intervalLabel.style.fontSize = '16px'; intervalLabel.style.fontWeight = 'bold'; const intervalSlider = document.createElement('input'); intervalSlider.type = 'range'; intervalSlider.min = '2'; intervalSlider.max = '10'; intervalSlider.value = String(state.updateInterval / 1000); intervalSlider.step = '1'; intervalSlider.className = 'interval-slider'; const intervalValue = document.createElement('div'); intervalValue.className = 'interval-value'; intervalValue.textContent = `${intervalSlider.value}s`; intervalValue.style.marginBottom = '15px'; intervalValue.style.fontSize = '14px'; intervalSlider.addEventListener('input', e => { const { target } = e; const input = (target); const newInterval = parseInt(input.value, 10) * 1000; intervalValue.textContent = `${input.value}s`; state.updateInterval = newInterval; localStorage.setItem('youtubeEnhancerInterval', String(newInterval)); if (state.intervalId) { clearInterval(state.intervalId); state.intervalId = setInterval(() => { updateOverlayContent(state.overlay, state.currentChannelName); }, newInterval); YouTubeUtils.cleanupManager.registerInterval(state.intervalId); } }); const opacityLabel = document.createElement('label'); opacityLabel.textContent = t('backgroundOpacity'); opacityLabel.style.display = 'block'; opacityLabel.style.marginBottom = '5px'; opacityLabel.style.fontSize = '16px'; opacityLabel.style.fontWeight = 'bold'; const opacitySlider = document.createElement('input'); opacitySlider.type = 'range'; opacitySlider.min = '50'; opacitySlider.max = '90'; opacitySlider.value = String(state.overlayOpacity * 100); opacitySlider.step = '5'; opacitySlider.className = 'opacity-slider'; const opacityValue = document.createElement('div'); opacityValue.className = 'opacity-value'; opacityValue.textContent = `${opacitySlider.value}%`; opacityValue.style.fontSize = '14px'; opacitySlider.addEventListener('input', e => { const { target } = e; const input = (target); const newOpacity = parseInt(input.value, 10) / 100; opacityValue.textContent = `${input.value}%`; state.overlayOpacity = newOpacity; localStorage.setItem('youtubeEnhancerOpacity', String(newOpacity)); if (state.overlay) { state.overlay.style.backgroundColor = `rgba(0, 0, 0, ${newOpacity})`; } }); controlsSection.appendChild(fontLabel); controlsSection.appendChild(fontSelect); controlsSection.appendChild(fontSizeLabel); controlsSection.appendChild(fontSizeSlider); controlsSection.appendChild(fontSizeValue); controlsSection.appendChild(intervalLabel); controlsSection.appendChild(intervalSlider); controlsSection.appendChild(intervalValue); controlsSection.appendChild(opacityLabel); controlsSection.appendChild(opacitySlider); controlsSection.appendChild(opacityValue); return controlsSection; } function createSpinner() { const spinnerContainer = document.createElement('div'); spinnerContainer.style.position = 'absolute'; spinnerContainer.style.top = '0'; spinnerContainer.style.left = '0'; spinnerContainer.style.width = '100%'; spinnerContainer.style.height = '100%'; spinnerContainer.style.display = 'flex'; spinnerContainer.style.justifyContent = 'center'; spinnerContainer.style.alignItems = 'center'; spinnerContainer.classList.add('spinner-container'); const spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); spinner.setAttribute('viewBox', '0 0 512 512'); spinner.setAttribute('width', '64'); spinner.setAttribute('height', '64'); spinner.classList.add('loading-spinner'); spinner.style.animation = 'spin 1s linear infinite'; const secondaryPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); secondaryPath.setAttribute( 'd', 'M0 256C0 114.9 114.1 .5 255.1 0C237.9 .5 224 14.6 224 32c0 17.7 14.3 32 32 32C150 64 64 150 64 256s86 192 192 192c69.7 0 130.7-37.1 164.5-92.6c-3 6.6-3.3 14.8-1 22.2c1.2 3.7 3 7.2 5.4 10.3c1.2 1.5 2.6 3 4.1 4.3c.8 .7 1.6 1.3 2.4 1.9c.4 .3 .8 .6 1.3 .9s.9 .6 1.3 .8c5 2.9 10.6 4.3 16 4.3c11 0 21.8-5.7 27.7-16c-44.3 76.5-127 128-221.7 128C114.6 512 0 397.4 0 256z' ); secondaryPath.style.opacity = '0.4'; secondaryPath.style.fill = 'white'; const primaryPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); primaryPath.setAttribute( 'd', 'M224 32c0-17.7 14.3-32 32-32C397.4 0 512 114.6 512 256c0 46.6-12.5 90.4-34.3 128c-8.8 15.3-28.4 20.5-43.7 11.7s-20.5-28.4-11.7-43.7c16.3-28.2 25.7-61 25.7-96c0-106-86-192-192-192c-17.7 0-32-14.3-32-32z' ); primaryPath.style.fill = 'white'; spinner.appendChild(secondaryPath); spinner.appendChild(primaryPath); spinnerContainer.appendChild(spinner); return spinnerContainer; } function createSVGIcon(path) { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 640 512'); svg.setAttribute('width', '2rem'); svg.setAttribute('height', '2rem'); svg.style.marginRight = '0.5rem'; svg.style.display = 'none'; const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); svgPath.setAttribute('d', path); svgPath.setAttribute('fill', 'white'); svg.appendChild(svgPath); return svg; } function createStatContainer(className, iconPath) { const container = document.createElement('div'); Object.assign(container.style, { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', visibility: 'hidden', width: '33%', height: '100%', padding: '0 1rem', }); const numberContainer = document.createElement('div'); Object.assign(numberContainer.style, { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', }); const differenceElement = document.createElement('div'); differenceElement.classList.add(`${className}-difference`); Object.assign(differenceElement.style, { fontSize: '2.5rem', height: '2.5rem', marginBottom: '1rem', }); const digitContainer = createNumberContainer(); digitContainer.classList.add(`${className}-number`); Object.assign(digitContainer.style, { fontSize: `${localStorage.getItem('youtubeEnhancerFontSize') || '24'}px`, fontWeight: 'bold', lineHeight: '1', height: '4rem', fontFamily: localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif', letterSpacing: '0.025em', }); numberContainer.appendChild(differenceElement); numberContainer.appendChild(digitContainer); const labelContainer = document.createElement('div'); Object.assign(labelContainer.style, { display: 'flex', alignItems: 'center', marginTop: '0.5rem', }); const icon = createSVGIcon(iconPath); Object.assign(icon.style, { width: '2rem', height: '2rem', marginRight: '0.75rem', }); const labelElement = document.createElement('div'); labelElement.classList.add(`${className}-label`); labelElement.style.fontSize = '2rem'; labelContainer.appendChild(icon); labelContainer.appendChild(labelElement); container.appendChild(numberContainer); container.appendChild(labelContainer); return container; } function createOverlayElement() { const overlay = document.createElement('div'); overlay.classList.add('channel-banner-overlay'); Object.assign(overlay.style, { position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: `rgba(0, 0, 0, ${state.overlayOpacity})`, borderRadius: '15px', zIndex: '10', display: 'flex', justifyContent: 'space-around', alignItems: 'center', color: 'white', fontFamily: localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif', fontSize: `${localStorage.getItem('youtubeEnhancerFontSize') || '24'}px`, boxSizing: 'border-box', transition: 'background-color 0.3s ease', }); return overlay; } function applyOverlayAccessibility(overlay) { overlay.setAttribute('role', 'region'); overlay.setAttribute('aria-label', t('overlayAriaLabel')); overlay.setAttribute('tabindex', '-1'); } function applyMobileResponsiveness(overlay) { if (window.innerWidth <= 768) { overlay.style.flexDirection = 'column'; overlay.style.padding = '10px'; overlay.style.minHeight = '200px'; } } function setupSettingsButton() { const button = createSettingsButton(); button.setAttribute('tabindex', '0'); button.setAttribute('aria-label', t('settingsAriaLabel')); button.setAttribute('role', 'button'); return button; } function setupSettingsMenu() { const menu = createSettingsMenu(); menu.setAttribute('aria-label', t('settingsMenuAriaLabel')); menu.setAttribute('role', 'dialog'); return menu; } function attachMenuEventHandlers(settingsButton, settingsMenu) { const toggleMenu = show => { settingsMenu.classList.toggle('show', show); settingsButton.setAttribute('aria-expanded', show); if (show) settingsMenu.focus(); }; settingsButton.addEventListener('click', e => { e.stopPropagation(); toggleMenu(!settingsMenu.classList.contains('show')); }); settingsButton.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleMenu(!settingsMenu.classList.contains('show')); } }); const clickHandler = e => { const node = (e.target); if (!settingsMenu.contains(node) && !settingsButton.contains(node)) { toggleMenu(false); } }; const keyHandler = e => { if (e.key === 'Escape' && settingsMenu.classList.contains('show')) { toggleMenu(false); settingsButton.focus(); } }; const clickKey = YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler); const keyKey = YouTubeUtils.cleanupManager.registerListener(document, 'keydown', keyHandler); state.documentListenerKeys.add(clickKey); state.documentListenerKeys.add(keyKey); } function addStatContainers(overlay) { const subscribersElement = createStatContainer( 'subscribers', 'M144 160c-44.2 0-80-35.8-80-80S99.8 0 144 0s80 35.8 80 80s-35.8 80-80 80zm368 0c-44.2 0-80-35.8-80-80s35.8-80 80-80s80 35.8 80 80s-35.8 80-80 80zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM416 224c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z' ); const viewsElement = createStatContainer( 'views', 'M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z' ); const videosElement = createStatContainer( 'videos', 'M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z' ); overlay.appendChild(subscribersElement); overlay.appendChild(viewsElement); overlay.appendChild(videosElement); } function createOverlay(bannerElement) { clearExistingOverlay(); if (!bannerElement) return null; const overlay = createOverlayElement(); applyOverlayAccessibility(overlay); applyMobileResponsiveness(overlay); const settingsButton = setupSettingsButton(); const settingsMenu = setupSettingsMenu(); overlay.appendChild(settingsButton); overlay.appendChild(settingsMenu); attachMenuEventHandlers(settingsButton, settingsMenu); const spinner = createSpinner(); overlay.appendChild(spinner); addStatContainers(overlay); bannerElement.appendChild(overlay); updateDisplayState(); return overlay; } function fetchWithGM(url, headers = {}) { const requestHeaders = { Accept: 'application/json', ...headers, }; const gm = (window).GM_xmlhttpRequest; if (typeof gm === 'function') { return new Promise((resolve, reject) => { gm({ method: 'GET', url, headers: requestHeaders, timeout: 10000, onload: response => { if (response.status >= 200 && response.status < 300) { try { resolve(JSON.parse(response.responseText)); } catch (parseError) { reject(new Error(`Failed to parse response: ${parseError.message}`)); } } else { reject(new Error(`Failed to fetch: ${response.status}`)); } }, onerror: error => reject(error), ontimeout: () => reject(new Error('Request timed out')), }); }); } utils.warn('GM_xmlhttpRequest unavailable, falling back to fetch API'); return fetch(url, { method: 'GET', headers: requestHeaders, credentials: 'omit', mode: 'cors', }) .then(response => { if (!response.ok) { throw new Error(`Failed to fetch: ${response.status}`); } return response.json(); }) .catch(error => { utils.error('Fallback fetch failed:', error); throw error; }); } async function fetchChannelId(_channelName) { const metaTag = document.querySelector('meta[itemprop="channelId"]'); if (metaTag && metaTag.content) return metaTag.content; const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/); if (urlMatch && urlMatch[1]) return urlMatch[1]; const channelInfo = await getChannelInfo(window.location.href); if (channelInfo && channelInfo.channelId) return channelInfo.channelId; throw new Error('Could not determine channel ID'); } async function fetchChannelStats(channelId) { const helpers = typeof window !== 'undefined' && window.YouTubePlusChannelStatsHelpers ? window.YouTubePlusChannelStatsHelpers : null; if (!helpers) { utils.error('Channel stats helpers not loaded'); return { followerCount: 0, bottomOdos: [0, 0], error: true, timestamp: Date.now(), }; } try { const fetchFn = () => fetchWithGM(`${STATS_API_URL}${channelId}`, { origin: 'https://livecounts.io', referer: 'https://livecounts.io/', }); const stats = await helpers.fetchWithRetry(fetchFn, CONFIG.MAX_RETRIES, utils); if (stats) { helpers.cacheStats(state.lastSuccessfulStats, channelId, stats); return stats; } const cachedStats = helpers.getCachedStats( state.lastSuccessfulStats, channelId, CONFIG.CACHE_DURATION, utils ); if (cachedStats) { return cachedStats; } const fallbackCount = helpers.extractSubscriberCountFromPage(); if (fallbackCount > 0) { utils.log('Extracted fallback subscriber count:', fallbackCount); } return helpers.createFallbackStats(fallbackCount); } catch (error) { utils.error('Failed to fetch channel stats:', error); return helpers.createFallbackStats(0); } } function clearExistingOverlay() { const existingOverlay = document.querySelector('.channel-banner-overlay'); if (existingOverlay) { try { existingOverlay.remove(); } catch { console.warn('[YouTube+] Failed to remove overlay'); } } if (state.intervalId) { try { clearInterval(state.intervalId); YouTubeUtils.cleanupManager.unregisterInterval(state.intervalId); } catch { console.warn('[YouTube+] Failed to clear interval'); } state.intervalId = null; } if (state.documentListenerKeys && state.documentListenerKeys.size) { state.documentListenerKeys.forEach(key => { try { YouTubeUtils.cleanupManager.unregisterListener(key); } catch { console.warn('[YouTube+] Failed to unregister listener'); } }); state.documentListenerKeys.clear(); } if (state.lastSuccessfulStats) state.lastSuccessfulStats.clear(); if (state.previousStats) state.previousStats.clear(); state.isUpdating = false; state.overlay = null; utils.log('Cleared existing overlay'); } function createDigitElement() { const digit = document.createElement('span'); Object.assign(digit.style, { display: 'inline-block', width: '0.6em', textAlign: 'center', marginRight: '0.025em', marginLeft: '0.025em', }); return digit; } function createCommaElement() { const comma = document.createElement('span'); comma.textContent = ','; Object.assign(comma.style, { display: 'inline-block', width: '0.3em', textAlign: 'center', }); return comma; } function createNumberContainer() { const container = document.createElement('div'); Object.assign(container.style, { display: 'flex', justifyContent: 'center', alignItems: 'center', letterSpacing: '0.025em', }); return container; } function splitIntoDigitGroups(valueStr) { const digits = []; for (let i = valueStr.length - 1; i >= 0; i -= 3) { const start = Math.max(0, i - 2); digits.unshift(valueStr.slice(start, i + 1)); } return digits; } function clearContainer(container) { while (container.firstChild) { container.removeChild(container.firstChild); } } function renderDigitGroups(container, digitGroups) { for (let i = 0; i < digitGroups.length; i++) { const group = digitGroups[i]; for (let j = 0; j < group.length; j++) { const digitElement = createDigitElement(); digitElement.textContent = group[j]; container.appendChild(digitElement); } if (i < digitGroups.length - 1) { container.appendChild(createCommaElement()); } } } function animateDigitChanges(container, digitGroups) { let elementIndex = 0; for (let i = 0; i < digitGroups.length; i++) { const group = digitGroups[i]; for (let j = 0; j < group.length; j++) { const digitElement = container.children[elementIndex]; const newDigit = parseInt(group[j], 10); const currentDigit = parseInt(digitElement.textContent || '0', 10); if (currentDigit !== newDigit) { animateDigit(digitElement, currentDigit, newDigit); } elementIndex++; } if (i < digitGroups.length - 1) { elementIndex++; } } } function updateDigits(container, newValue) { const newValueStr = newValue.toString(); const digitGroups = splitIntoDigitGroups(newValueStr); clearContainer(container); renderDigitGroups(container, digitGroups); animateDigitChanges(container, digitGroups); } function animateDigit(element, start, end) { const duration = 1000; const startTime = performance.now(); function update(currentTime) { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const easeOutQuart = 1 - Math.pow(1 - progress, 4); const current = Math.round(start + (end - start) * easeOutQuart); element.textContent = current; if (progress < 1) { requestAnimationFrame(update); } } requestAnimationFrame(update); } function showContent(overlay) { const spinnerContainer = overlay.querySelector('.spinner-container'); if (spinnerContainer) { spinnerContainer.remove(); } const containers = overlay.querySelectorAll('div[style*="visibility: hidden"]'); containers.forEach(container => { container.style.visibility = 'visible'; }); const icons = overlay.querySelectorAll('svg[style*="display: none"]'); icons.forEach(icon => { icon.style.display = 'block'; }); } function updateDifferenceElement(element, currentValue, previousValue) { if (!previousValue) return; const difference = currentValue - previousValue; if (difference === 0) { element.textContent = ''; return; } const sign = difference > 0 ? '+' : ''; element.textContent = `${sign}${difference.toLocaleString()}`; element.style.color = difference > 0 ? '#1ed760' : '#f3727f'; setTimeout(() => { element.textContent = ''; }, 1000); } function updateDisplayState() { const overlay = document.querySelector('.channel-banner-overlay'); if (!overlay) return; const statContainers = overlay.querySelectorAll('div[style*="width"]'); if (!statContainers.length) return; let visibleCount = 0; const visibleContainers = []; statContainers.forEach(container => { const numberContainer = container.querySelector('[class$="-number"]'); if (!numberContainer) return; const type = numberContainer.className.replace('-number', ''); const isVisible = localStorage.getItem(`show-${type}`) !== 'false'; if (isVisible) { container.style.display = 'flex'; visibleCount++; visibleContainers.push(container); } else { container.style.display = 'none'; } }); visibleContainers.forEach(container => { container.style.width = ''; container.style.margin = ''; switch (visibleCount) { case 1: container.style.width = '100%'; break; case 2: container.style.width = '50%'; break; case 3: container.style.width = '33.33%'; break; default: container.style.display = 'none'; } }); const fontSize = localStorage.getItem('youtubeEnhancerFontSize') || '24'; const fontFamily = localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif'; overlay.querySelectorAll('.subscribers-number,.views-number,.videos-number').forEach(el => { el.style.fontSize = `${fontSize}px`; el.style.fontFamily = fontFamily; }); overlay.style.display = 'flex'; } function shouldUpdateOverlay(channelName) { return !state.isUpdating && channelName === state.currentChannelName; } function handleStatsError(overlay, stats) { const containers = overlay.querySelectorAll('[class$="-number"]'); containers.forEach(container => { if (container.classList.contains('subscribers-number') && stats.followerCount > 0) { updateDigits(container, stats.followerCount); } else { container.textContent = '---'; } }); utils.warn('Using fallback stats due to API error'); } function getPreviousStatValue(channelId, className) { const prevStats = state.previousStats.get(channelId); if (!prevStats) return null; if (className === 'subscribers') { return prevStats.followerCount; } const index = className === 'views' ? 0 : 1; return prevStats.bottomOdos[index]; } function updateStatElement(overlay, channelId, className, value, label) { const numberContainer = overlay.querySelector(`.${className}-number`); const differenceElement = overlay.querySelector(`.${className}-difference`); const labelElement = overlay.querySelector(`.${className}-label`); if (numberContainer) { updateDigits(numberContainer, value); } if (differenceElement && state.previousStats.has(channelId)) { const previousValue = getPreviousStatValue(channelId, className); if (previousValue !== null) { updateDifferenceElement(differenceElement, value, previousValue); } } if (labelElement) { labelElement.textContent = label; } } function updateAllStatElements(overlay, channelId, stats) { updateStatElement(overlay, channelId, 'subscribers', stats.followerCount, 'Subscribers'); updateStatElement(overlay, channelId, 'views', stats.bottomOdos[0], 'Views'); updateStatElement(overlay, channelId, 'videos', stats.bottomOdos[1], 'Videos'); } function showOverlayError(overlay) { const containers = overlay.querySelectorAll('[class$="-number"]'); containers.forEach(container => { container.textContent = '---'; }); } async function updateOverlayContent(overlay, channelName) { if (!shouldUpdateOverlay(channelName)) return; state.isUpdating = true; try { const channelId = await fetchChannelId(channelName); const stats = await fetchChannelStats(channelId); if (channelName !== state.currentChannelName) { return; } if (stats.error) { handleStatsError(overlay, stats); return; } updateAllStatElements(overlay, channelId, stats); if (!state.previousStats.has(channelId)) { showContent(overlay); utils.log('Displayed initial stats for channel:', channelName); } state.previousStats.set(channelId, stats); } catch (error) { utils.error('Failed to update overlay content:', error); showOverlayError(overlay); } finally { state.isUpdating = false; } } function addSettingsUI() { const section = document.querySelector( '.ytp-plus-settings-section[data-section="experimental"]' ); if (!section || section.querySelector('.count-settings-item')) return; const item = document.createElement('div'); item.className = 'ytp-plus-settings-item count-settings-item'; item.innerHTML = `
${t('channelStatsDescription')}
`; section.appendChild(item); item.querySelector('input').addEventListener('change', e => { const { target } = e; const input = (target); state.enabled = input.checked; localStorage.setItem(CONFIG.STORAGE_KEY, state.enabled ? 'true' : 'false'); if (state.enabled) { observePageChanges(); addNavigationListener(); setTimeout(() => { const bannerElement = document.getElementById('page-header-banner-sizer'); if (bannerElement && isChannelPage()) { addOverlay(bannerElement); } }, 100); } else { clearExistingOverlay(); } }); } const settingsObserver = new MutationObserver(mutations => { for (const { addedNodes } of mutations) { for (const node of addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { setTimeout(addSettingsUI, 100); return; } } } if (document.querySelector('.ytp-plus-settings-nav-item[data-section="experimental"].active')) { setTimeout(addSettingsUI, 50); } }); YouTubeUtils.cleanupManager.registerObserver(settingsObserver); if (document.body) { settingsObserver.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { settingsObserver.observe(document.body, { childList: true, subtree: true }); }); } const experimentalNavClickHandler = e => { const { target } = e; const el = (target); if ( el.classList?.contains('ytp-plus-settings-nav-item') && el.dataset?.section === 'experimental' ) { setTimeout(addSettingsUI, 50); } }; const listenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', experimentalNavClickHandler, true ); state.documentListenerKeys.add(listenerKey); function extractChannelName(pathname) { if (pathname.startsWith('/@')) { return pathname.split('/')[1].replace('@', ''); } if (pathname.startsWith('/channel/')) { return pathname.split('/')[2]; } if (pathname.startsWith('/c/')) { return pathname.split('/')[2]; } if (pathname.startsWith('/user/')) { return pathname.split('/')[2]; } return null; } function shouldSkipOverlay(channelName) { return !channelName || (channelName === state.currentChannelName && state.overlay); } function ensureBannerPosition(bannerElement) { if (bannerElement && !bannerElement.style.position) { bannerElement.style.position = 'relative'; } } function clearUpdateInterval() { if (state.intervalId) { clearInterval(state.intervalId); state.intervalId = null; } } function createDebouncedUpdate(overlay, channelName) { let lastUpdateTime = 0; return () => { const now = Date.now(); if (now - lastUpdateTime >= state.updateInterval - 100) { updateOverlayContent(overlay, channelName); lastUpdateTime = now; } }; } function setupUpdateInterval(overlay, channelName) { const debouncedUpdate = createDebouncedUpdate(overlay, channelName); state.intervalId = setInterval(debouncedUpdate, state.updateInterval); YouTubeUtils.cleanupManager.registerInterval(state.intervalId); } function addOverlay(bannerElement) { const channelName = extractChannelName(window.location.pathname); if (shouldSkipOverlay(channelName)) { return; } ensureBannerPosition(bannerElement); state.currentChannelName = channelName; state.overlay = createOverlay(bannerElement); if (state.overlay) { clearUpdateInterval(); setupUpdateInterval(state.overlay, channelName); updateOverlayContent(state.overlay, channelName); utils.log('Added overlay for channel:', channelName); } } function isChannelPage() { return ( window.location.pathname.startsWith('/@') || window.location.pathname.startsWith('/channel/') || window.location.pathname.startsWith('/c/') ); } function findBannerElement() { let bannerElement = document.getElementById('page-header-banner-sizer'); if (!bannerElement) { const alternatives = [ '[id*="banner"]', '.ytd-c4-tabbed-header-renderer', '#channel-header', '.channel-header', ]; for (const selector of alternatives) { bannerElement = document.querySelector(selector); if (bannerElement) break; } } return bannerElement; } function ensureBannerPositioning(bannerElement) { if (bannerElement.style.position !== 'relative') { bannerElement.style.position = 'relative'; } } function handleBannerUpdate() { const bannerElement = findBannerElement(); if (bannerElement && isChannelPage()) { ensureBannerPositioning(bannerElement); addOverlay(bannerElement); } else if (!isChannelPage()) { clearExistingOverlay(); state.currentChannelName = null; } } function clearObserverTimeout(observer) { if ( (observer)._timeout) { YouTubeUtils.cleanupManager.unregisterTimeout( (observer)._timeout); clearTimeout( (observer)._timeout); } } function setupObserver(observer) { const observerConfig = { childList: true, subtree: true, attributes: false, }; if (document.body) { observer.observe(document.body, observerConfig); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, observerConfig); }); } } function observePageChanges() { if (!state.enabled) return undefined; const observer = new MutationObserver(_mutations => { clearObserverTimeout(observer); (observer)._timeout = YouTubeUtils.cleanupManager.registerTimeout( setTimeout(handleBannerUpdate, 100) ); }); setupObserver(observer); (observer)._timeout = null; if (typeof state.observers === 'undefined') { state.observers = []; } state.observers.push(observer); return observer; } function addNavigationListener() { if (!state.enabled) return; window.addEventListener('yt-navigate-finish', () => { if (isChannelPage()) { const bannerElement = document.getElementById('page-header-banner-sizer'); if (bannerElement) { addOverlay(bannerElement); utils.log('Navigated to channel page'); } } else { clearExistingOverlay(); state.currentChannelName = null; utils.log('Navigated away from channel page'); } }); } function cleanup() { if (state.observers && Array.isArray(state.observers)) { state.observers.forEach(observer => { try { observer.disconnect(); } catch (e) { console.warn('[YouTube+] Failed to disconnect observer:', e); } }); state.observers = []; } clearExistingOverlay(); utils.log('Cleanup completed'); } window.addEventListener('beforeunload', cleanup); if (typeof window !== 'undefined') { window.YouTubeStats = { init, cleanup, version: '2.2', }; } init(); })(); (function () { 'use strict'; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') { return _globalI18n.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const CONFIG = { selectors: { deleteButtons: 'div[class^="VfPpkd-Bz112c-"]', menuButton: '[aria-haspopup="menu"]', }, classes: { checkbox: 'comment-checkbox', checkboxAnchor: 'comment-checkbox-anchor', checkboxFloating: 'comment-checkbox-floating', container: 'comment-controls-container', panel: 'comment-controls-panel', header: 'comment-controls-header', title: 'comment-controls-title', actions: 'comment-controls-actions', button: 'comment-controls-button', buttonDanger: 'comment-controls-button--danger', buttonPrimary: 'comment-controls-button--primary', buttonSuccess: 'comment-controls-button--success', close: 'comment-controls-close', deleteButton: 'comment-controls-button-delete', }, debounceDelay: 100, deleteDelay: 200, enabled: true, storageKey: 'youtube_comment_manager_settings', }; const state = { observer: null, isProcessing: false, settingsNavListenerKey: null, panelCollapsed: false, }; const settings = { load: () => { try { const saved = localStorage.getItem(CONFIG.storageKey); if (saved) CONFIG.enabled = JSON.parse(saved).enabled ?? true; } catch {} }, save: () => { try { localStorage.setItem(CONFIG.storageKey, JSON.stringify({ enabled: CONFIG.enabled })); } catch {} }, }; const debounce = (func, wait) => { try { const utilDebounce = window.YouTubeUtils && window.YouTubeUtils.debounce; if (typeof utilDebounce === 'function') { const debounced = utilDebounce(func, wait); if (typeof debounced === 'function') return debounced; } return ((f, w) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => f(...args), w); }; })(func, wait); } catch { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } }; const $ = selector => (document.querySelector(selector)); const $$ = selector => (document.querySelectorAll(selector)); const logError = (context, error) => { const errorObj = error instanceof Error ? error : new Error(String(error)); if (window.YouTubeErrorBoundary) { window.YouTubeErrorBoundary.logError(errorObj, { context }); } else { console.error(`[YouTube+][CommentManager] ${context}:`, error); } }; const withErrorBoundary = (fn, context) => { if (window.YouTubeErrorBoundary?.withErrorBoundary) { return (window.YouTubeErrorBoundary.withErrorBoundary(fn, 'CommentManager')); } return ( (...args) => { try { return fn(...args); } catch (error) { logError(context, error); return null; } } ); }; const addCheckboxes = withErrorBoundary(() => { if (!CONFIG.enabled || state.isProcessing) return; const deleteButtons = $$(CONFIG.selectors.deleteButtons); deleteButtons.forEach(button => { const parent = button.parentNode; if ( button.closest(CONFIG.selectors.menuButton) || (parent && parent.querySelector && parent.querySelector(`.${CONFIG.classes.checkbox}`)) ) { return; } const commentElement = button.closest('[class*="comment"]') || button.closest('[role="article"]') || parent; if (commentElement && commentElement instanceof Element) { if (!commentElement.hasAttribute('data-comment-text')) { commentElement.setAttribute( 'data-comment-text', (commentElement.textContent || '').toLowerCase() ); } } const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = `${CONFIG.classes.checkbox} ytp-plus-settings-checkbox`; checkbox.setAttribute('aria-label', t('selectComment')); checkbox.addEventListener('change', updateDeleteButtonState); checkbox.addEventListener('click', e => e.stopPropagation()); const dateElement = commentElement && commentElement.querySelector ? commentElement.querySelector( '[class*="date"],[class*="time"],time,[title*="20"],[aria-label*="ago"]' ) : null; if (dateElement && dateElement instanceof Element) { dateElement.classList.add(CONFIG.classes.checkboxAnchor); checkbox.classList.add(CONFIG.classes.checkboxFloating); dateElement.appendChild(checkbox); } else if (parent && parent.insertBefore) { parent.insertBefore(checkbox, button); } }); }, 'addCheckboxes'); const addControlButtons = withErrorBoundary(() => { if (!CONFIG.enabled || $(`.${CONFIG.classes.container}`)) return; const deleteButtons = $$(CONFIG.selectors.deleteButtons); if (!deleteButtons.length) return; const first = deleteButtons[0]; const container = first && first.parentNode && first.parentNode.parentNode; if (!container || !(container instanceof Element)) return; const panel = document.createElement('div'); panel.className = `${CONFIG.classes.container} ${CONFIG.classes.panel} glass-panel`; panel.setAttribute('role', 'region'); panel.setAttribute('aria-label', t('commentManagerControls')); const header = document.createElement('div'); header.className = CONFIG.classes.header; const title = document.createElement('div'); title.className = CONFIG.classes.title; title.textContent = t('commentManager'); const collapseButton = document.createElement('button'); collapseButton.className = `${CONFIG.classes.close} ytp-plus-settings-close`; collapseButton.setAttribute('type', 'button'); collapseButton.setAttribute('aria-expanded', String(!state.panelCollapsed)); collapseButton.setAttribute('aria-label', t('togglePanel')); collapseButton.innerHTML = ` `; const togglePanelState = collapsed => { state.panelCollapsed = collapsed; header.classList.toggle('is-collapsed', collapsed); actions.classList.toggle('is-hidden', collapsed); collapseButton.setAttribute('aria-expanded', String(!collapsed)); panel.classList.toggle('is-collapsed', collapsed); }; collapseButton.addEventListener('click', () => { state.panelCollapsed = !state.panelCollapsed; togglePanelState(state.panelCollapsed); }); header.append(title, collapseButton); const actions = document.createElement('div'); actions.className = CONFIG.classes.actions; const createActionButton = (label, className, onClick, options = {}) => { const button = document.createElement('button'); button.type = 'button'; button.textContent = label; button.className = `${CONFIG.classes.button} ${className}`; if (options.id) button.id = options.id; if (options.disabled) button.disabled = true; button.addEventListener('click', onClick); return button; }; const deleteAllButton = createActionButton( t('deleteSelected'), `${CONFIG.classes.buttonDanger} ${CONFIG.classes.deleteButton}`, deleteSelectedComments, { disabled: true } ); const selectAllButton = createActionButton(t('selectAll'), CONFIG.classes.buttonPrimary, () => { $$(`.${CONFIG.classes.checkbox}`).forEach(cb => { cb.checked = true; }); updateDeleteButtonState(); }); const clearAllButton = createActionButton(t('clearAll'), CONFIG.classes.buttonSuccess, () => { $$(`.${CONFIG.classes.checkbox}`).forEach(cb => { cb.checked = false; }); updateDeleteButtonState(); }); actions.append(deleteAllButton, selectAllButton, clearAllButton); togglePanelState(state.panelCollapsed); panel.append(header, actions); const refNode = deleteButtons[0] && deleteButtons[0].parentNode; if (refNode && refNode.parentNode) { container.insertBefore(panel, refNode); } else { container.appendChild(panel); } }, 'addControlButtons'); const updateDeleteButtonState = withErrorBoundary(() => { const deleteAllButton = $(`.${CONFIG.classes.deleteButton}`); if (!deleteAllButton) return; const hasChecked = Array.from($$(`.${CONFIG.classes.checkbox}`)).some(cb => cb.checked); deleteAllButton.disabled = !hasChecked; deleteAllButton.style.opacity = hasChecked ? '1' : '0.6'; }, 'updateDeleteButtonState'); const deleteSelectedComments = withErrorBoundary(() => { const checkedBoxes = Array.from($$(`.${CONFIG.classes.checkbox}`)).filter(cb => cb.checked); if (!checkedBoxes.length || !confirm(`Delete ${checkedBoxes.length} comment(s)?`)) return; state.isProcessing = true; checkedBoxes.forEach((checkbox, index) => { setTimeout(() => { const deleteButton = checkbox.nextElementSibling || checkbox.parentNode.querySelector(CONFIG.selectors.deleteButtons); deleteButton?.click(); }, index * CONFIG.deleteDelay); }); setTimeout( () => { state.isProcessing = false; }, checkedBoxes.length * CONFIG.deleteDelay + 1000 ); }, 'deleteSelectedComments'); const cleanup = withErrorBoundary(() => { $$(`.${CONFIG.classes.checkbox}`).forEach(el => el.remove()); $(`.${CONFIG.classes.container}`)?.remove(); }, 'cleanup'); const initializeScript = withErrorBoundary(() => { if (CONFIG.enabled) { addCheckboxes(); addControlButtons(); updateDeleteButtonState(); } else { cleanup(); } }, 'initializeScript'); const addStyles = withErrorBoundary(() => { if ($('#comment-delete-styles')) return; const styles = ` .${CONFIG.classes.checkboxAnchor}{position:relative;display:inline-flex;align-items:center;gap:8px;width:auto;} .${CONFIG.classes.checkboxFloating}{position:absolute;top:-4px;right:-32px;margin:0;} .${CONFIG.classes.panel}{position:fixed;top:50%;right:24px;transform:translateY(-50%);display:flex;flex-direction:column;gap:14px;z-index:10000;padding:16px 18px;background:var(--yt-glass-bg);border:1.5px solid var(--yt-glass-border);border-radius:20px;box-shadow:0 12px 40px rgba(0,0,0,0.45);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);min-width:220px;max-width:300px;color:var(--yt-text-primary);transition:transform .22s cubic-bezier(.4,0,.2,1),opacity .22s,box-shadow .2s} html:not([dark]) .${CONFIG.classes.panel}{background:var(--yt-glass-bg);} .${CONFIG.classes.header}{display:flex;align-items:center;justify-content:space-between;gap:12px;} .${CONFIG.classes.panel}.is-collapsed{padding:14px 18px;} .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.title}{font-weight:500;opacity:.85;} .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.close}{transform:rotate(45deg);} .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.actions}{display:none!important;} .${CONFIG.classes.title}{font-size:15px;font-weight:600;letter-spacing:.3px;} .${CONFIG.classes.close}{background:transparent;border:none;cursor:pointer;padding:6px;border-radius:12px;display:flex;align-items:center;justify-content:center;color:var(--yt-text-primary);transition:all .2s ease;} .${CONFIG.classes.close}:hover{transform:rotate(90deg) scale(1.05);color:var(--yt-accent);} .${CONFIG.classes.actions}{display:flex;flex-direction:column;gap:10px;} .${CONFIG.classes.actions}.is-hidden{display:none!important;} .${CONFIG.classes.button}{padding:12px 16px;border-radius:var(--yt-radius-md);border:1px solid var(--yt-glass-border);cursor:pointer;font-size:13px;font-weight:500;background:var(--yt-button-bg);color:var(--yt-text-primary);transition:all .2s ease;text-align:center;} .${CONFIG.classes.button}:disabled{opacity:.5;cursor:not-allowed;} .${CONFIG.classes.button}:not(:disabled):hover{transform:translateY(-1px);box-shadow:var(--yt-shadow);} .${CONFIG.classes.buttonDanger}{background:rgba(255,99,71,.12);border-color:rgba(255,99,71,.25);color:#ff5c5c;} .${CONFIG.classes.buttonPrimary}{background:rgba(33,150,243,.12);border-color:rgba(33,150,243,.25);color:#2196f3;} .${CONFIG.classes.buttonSuccess}{background:rgba(76,175,80,.12);border-color:rgba(76,175,80,.25);color:#4caf50;} .${CONFIG.classes.buttonDanger}:not(:disabled):hover{background:rgba(255,99,71,.22);} .${CONFIG.classes.buttonPrimary}:not(:disabled):hover{background:rgba(33,150,243,.22);} .${CONFIG.classes.buttonSuccess}:not(:disabled):hover{background:rgba(76,175,80,.22);} @media(max-width:1280px){ .${CONFIG.classes.panel}{top:auto;bottom:24px;transform:none;right:16px;} } @media(max-width:768px){ .${CONFIG.classes.panel}{position:fixed;left:16px;right:16px;bottom:16px;top:auto;transform:none;max-width:none;} .${CONFIG.classes.actions}{flex-direction:row;flex-wrap:wrap;} .${CONFIG.classes.button}{flex:1;min-width:140px;} } `; YouTubeUtils.StyleManager.add('comment-delete-styles', styles); }, 'addStyles'); const addCommentManagerSettings = withErrorBoundary(() => { const advancedSection = $('.ytp-plus-settings-section[data-section="advanced"]'); if (!advancedSection) return; const existing = $('.comment-manager-settings-item'); if (existing) { try { advancedSection.appendChild(existing); } catch { } return; } const settingsItem = document.createElement('div'); settingsItem.className = 'ytp-plus-settings-item comment-manager-settings-item'; settingsItem.innerHTML = `
${t('bulkDeleteDescription')}
`; advancedSection.appendChild(settingsItem); $('#open-comment-history-page').addEventListener('click', () => { window.open('https://www.youtube.com/feed/history/comment_history', '_blank'); }); }, 'addCommentManagerSettings'); const init = withErrorBoundary(() => { settings.load(); addStyles(); state.observer?.disconnect(); state.observer = new MutationObserver(debounce(initializeScript, CONFIG.debounceDelay)); YouTubeUtils.cleanupManager.registerObserver(state.observer); if (document.body) { state.observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { state.observer.observe(document.body, { childList: true, subtree: true }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeScript); } else { initializeScript(); } const settingsObserver = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { setTimeout(addCommentManagerSettings, 100); return; } } } }); YouTubeUtils.cleanupManager.registerObserver(settingsObserver); if (document.body) { settingsObserver.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { settingsObserver.observe(document.body, { childList: true, subtree: true }); }); } const handleAdvancedNavClick = e => { const { target } = e; const element = (target); if (element.dataset?.section === 'advanced') { setTimeout(addCommentManagerSettings, 50); } }; if (!state.settingsNavListenerKey) { state.settingsNavListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', handleAdvancedNavClick, { passive: true, capture: true } ); } }, 'init'); if (typeof window !== 'undefined') { window.YouTubeComments = { init, version: '2.2', }; } init(); })(); (function () { 'use strict'; const Y = (window).YouTubeUtils || {}; function gatherFormData(elements, t, validators) { const { typeSelect, titleInput, descInput, emailInput, debugCheckbox } = elements; const { validateTitle, validateDescription, isValidEmail } = validators; const type = typeSelect.value; const rawTitle = titleInput.value.trim(); const rawDescription = descInput.value.trim(); const rawEmail = emailInput.value.trim(); const includeDebug = debugCheckbox.checked; const errors = validateFormData({ rawTitle, rawDescription, rawEmail }, t, { validateTitle, validateDescription, isValidEmail, }); return { type, title: validateTitle(rawTitle), description: validateDescription(rawDescription), email: rawEmail && isValidEmail(rawEmail) ? rawEmail : '', includeDebug, errors, }; } function validateFormData(data, t, validators) { const { rawTitle, rawDescription, rawEmail } = data; const { isValidEmail } = validators; const errors = []; if (!rawTitle) { errors.push(t('titleRequired')); } else if (rawTitle.length < 5) { errors.push(t('titleMin')); } if (!rawDescription) { errors.push(t('descRequired')); } else if (rawDescription.length < 10) { errors.push(t('descMin')); } if (rawEmail && !isValidEmail(rawEmail)) { errors.push(t('invalidEmail')); } return errors; } function showValidationErrors(errors, t) { const errorMsg = t('fixErrorsPrefix') + errors.join('\n• '); if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') { Y.NotificationManager.show(errorMsg, { duration: 4000, type: 'error' }); } else { console.warn('[YouTube+][Report] Validation errors:', errors); } } function updateButtonState(button, disabled, text) { button.disabled = disabled; button.textContent = text; button.style.opacity = disabled ? '0.6' : '1'; } function resetButtonAfterDelay(button, originalText, delay = 2000) { setTimeout(() => { updateButtonState(button, false, originalText); }, delay); } function handleGitHubSubmit(e, elements, gather, buildPayload, openGitHub, t) { e.preventDefault(); const { submitBtn } = elements; if (submitBtn.disabled) return; try { const data = gather(); if (data.errors && data.errors.length > 0) { showValidationErrors(data.errors, t); return; } const originalText = submitBtn.textContent; updateButtonState(submitBtn, true, t('opening')); const payload = buildPayload(data); openGitHub(payload); if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') { Y.NotificationManager.show(t('openingGithubNotification'), { duration: 2500 }); } resetButtonAfterDelay(submitBtn, originalText); } catch (err) { if (Y.logError) Y.logError('Report', 'Failed to open GitHub issue', err); if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') { Y.NotificationManager.show(t('failedOpenGithub'), { duration: 3000, type: 'error' }); } updateButtonState(submitBtn, false, t('openGitHub')); } } function showNotification(message, options = {}) { if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') { Y.NotificationManager.show(message, options); } } function handleCopySuccess(copyBtn, originalText, t) { showNotification(t('reportCopied'), { duration: 2000 }); copyBtn.textContent = t('copied'); copyBtn.style.opacity = '1'; resetButtonAfterDelay(copyBtn, originalText); } function handleCopyError(err, copyBtn, originalText, t) { if (Y && typeof Y.logError === 'function') { Y.logError('Report', 'copy failed', err); } if (Y && Y.NotificationManager && typeof Y.NotificationManager.show === 'function') { Y.NotificationManager.show(t('copyFailed'), { duration: 3000, type: 'error' }); } else { console.warn('Copy failed; please copy manually', err); } updateButtonState(copyBtn, false, originalText); } function hasValidationErrors(data, t) { if (data.errors && data.errors.length > 0) { showValidationErrors(data.errors, t); return true; } return false; } function buildReportText(payload) { return `Title: ${payload.title}\n\n${payload.body}`; } function handleCopyReport(e, elements, gather, buildPayload, copyToClipboard, t) { e.preventDefault(); const { copyBtn } = elements; if (copyBtn.disabled) return; try { const data = gather(); if (hasValidationErrors(data, t)) return; const originalText = copyBtn.textContent; updateButtonState(copyBtn, true, t('copying')); const payload = buildPayload(data); const reportText = buildReportText(payload); copyToClipboard(reportText) .then(() => handleCopySuccess(copyBtn, originalText, t)) .catch(err => handleCopyError(err, copyBtn, originalText, t)); } catch (err) { if (Y.logError) Y.logError('Report', 'Failed to copy report', err); updateButtonState(copyBtn, false, t('copyReport')); } } function handleEmailReport(e, elements, gather, buildPayload, t) { e.preventDefault(); const { emailBtn } = elements; if (emailBtn.disabled) return; try { const data = gather(); if (data.errors && data.errors.length > 0) { showValidationErrors(data.errors, t); return; } const originalText = emailBtn.textContent; updateButtonState(emailBtn, true, t('opening')); const payload = buildPayload(data); const subject = payload.title; const mailto = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent( payload.body )}`; window.location.href = mailto; resetButtonAfterDelay(emailBtn, originalText); } catch (err) { if (Y.logError) Y.logError('Report', 'Failed to prepare email', err); updateButtonState(emailBtn, false, t('prepareEmail')); } } function updateDebugPreview(debugCheckbox, debugPreview, getDebugInfo, mk) { try { if (debugCheckbox.checked) { const d = getDebugInfo(); debugPreview.innerHTML = ''; const header = createDebugHeader(d, mk); debugPreview.appendChild(header); if (d.settings) { debugPreview.appendChild(createSettingsSection(d.settings, mk)); } debugPreview.appendChild(createFullDebugSection(d, mk)); debugPreview.style.display = 'block'; } else { debugPreview.innerHTML = ''; debugPreview.style.display = 'none'; } } catch (err) { if (Y && typeof Y.logError === 'function') { Y.logError('Report', 'updateDebugPreview failed', err); } } } function createDebugHeader(d, mk) { const header = mk( 'div', { style: 'display:flex;flex-direction:column;gap:6px;margin-bottom:6px;' }, [] ); header.appendChild( mk('div', {}, ['Version: ', mk('strong', {}, [String(d.version || 'unknown')])]) ); header.appendChild( mk('div', {}, [ 'User agent: ', mk('code', { style: 'font-size:11px;color:var(--yt-text-secondary);' }, [ String(d.userAgent || ''), ]), ]) ); const urlStr = String(d.url || 'unknown'); const urlEl = createUrlElement(urlStr, mk); header.appendChild(mk('div', {}, ['URL: ', urlEl])); header.appendChild(mk('div', {}, ['Language: ', mk('code', {}, [String(d.language || '')])])); return header; } function createUrlElement(urlStr, mk) { try { if (/^https?:\/\//i.test(urlStr)) { return mk( 'a', { href: urlStr, target: '_blank', rel: 'noopener noreferrer', style: 'color:var(--yt-accent);word-break:break-all;', }, [urlStr] ); } } catch (e) { if (Y && typeof Y.logError === 'function') { Y.logError('Report', 'URL link creation failed', e); } } return mk('span', {}, [String(urlStr)]); } function createSettingsSection(settings, mk) { const settingsDetails = mk('details', {}, [mk('summary', {}, ['Settings'])]); settingsDetails.appendChild( mk('pre', { style: 'white-space:pre-wrap;margin:6px 0 0 0;font-size:11px;' }, [ JSON.stringify(settings, null, 2), ]) ); return settingsDetails; } function createFullDebugSection(d, mk) { const fullDetails = mk('details', {}, [mk('summary', {}, ['Full debug JSON'])]); fullDetails.appendChild( mk('pre', { style: 'white-space:pre-wrap;margin:6px 0 0 0;font-size:11px;' }, [ JSON.stringify(d, null, 2), ]) ); return fullDetails; } if (typeof window !== 'undefined') { window.YouTubeReportHandlers = { gatherFormData, validateFormData, showValidationErrors, updateButtonState, resetButtonAfterDelay, handleGitHubSubmit, handleCopyReport, handleEmailReport, updateDebugPreview, createDebugHeader, createUrlElement, createSettingsSection, createFullDebugSection, }; } })(); (function () { 'use strict'; const Y = (window).YouTubeUtils || {}; const _globalI18n_report = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; function t(key, params = {}) { try { if (_globalI18n_report && typeof _globalI18n_report.t === 'function') { return _globalI18n_report.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; const FALLBACK_EN = { shortTitle: 'Short title (one line)', emailOptional: 'Your email (optional)', descriptionPlaceholder: 'Describe the issue, steps to reproduce, expected vs actual', includeDebug: 'Include debug info (version, URL, settings)', openGitHub: 'Open GitHub Issue', copyReport: 'Copy Report', prepareEmail: 'Prepare Email', privacy: 'By submitting you agree to include the provided information. Do not include passwords or personal tokens.', typeBug: 'Bug / Error', typeFeature: 'Feature Request', typeOther: 'Other', titleRequired: 'Title is required', titleMin: 'Title must be at least 5 characters', descRequired: 'Description is required', descMin: 'Description must be at least 10 characters', invalidEmail: 'Invalid email format', fixErrorsPrefix: 'Please fix the following errors:\n• ', opening: 'Opening...', copying: 'Copying...', copied: 'Copied!', openingGithubNotification: 'Opening GitHub in a new tab', failedOpenGithub: 'Failed to open GitHub issue', reportCopied: 'Report copied to clipboard', copyFailed: 'Copy failed — please copy manually', }; let template = FALLBACK_EN[key] || key; if (Object.keys(params).length === 0) return template; for (const [k, v] of Object.entries(params)) { template = template.split(`{${k}}`).join(String(v)); } return template; } function mk(tag, props = {}, children = []) { const el = document.createElement(tag); Object.entries(props).forEach(([k, v]) => { if (k === 'class') { el.className = (v); } else if (k === 'html') { el.innerHTML = (v); } else if (k.startsWith('on') && typeof v === 'function') { el.addEventListener(k.substring(2).toLowerCase(), (v)); } else { el.setAttribute(k, String(v)); } }); children.forEach(c => typeof c === 'string' ? el.appendChild(document.createTextNode(c)) : el.appendChild(c) ); return el; } function sanitizeHTML(html) { if (Y.sanitizeHTML && typeof Y.sanitizeHTML === 'function') { return Y.sanitizeHTML(html); } if (typeof html !== 'string') return ''; const map = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', }; return html.replace(/[<>&"'\/`=]/g, char => map[char] || char); } function isValidEmail(email) { if (!email || typeof email !== 'string') return false; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email) && email.length <= 254; } function validateTitle(title) { if (!title || typeof title !== 'string') return ''; return sanitizeHTML(title.trim().substring(0, 200)); } function validateDescription(description) { if (!description || typeof description !== 'string') return ''; return sanitizeHTML(description.trim().substring(0, 5000)); } function getFallbackDebugInfo() { return { version: 'unknown', userAgent: 'unknown', url: 'unknown', language: 'unknown', settings: null, error: 'Failed to collect debug info', }; } function getVersion() { return (window.YouTubePlusDebug || {}).version || 'unknown'; } function getSettings() { return typeof Y.SettingsManager === 'object' ? Y.SettingsManager.load() : null; } function getLanguage() { return document.documentElement.lang || navigator.language || 'unknown'; } function getDebugInfo() { try { return { version: getVersion(), userAgent: navigator.userAgent || 'unknown', url: location.href || 'unknown', language: getLanguage(), settings: getSettings(), }; } catch (err) { if (Y && typeof Y.logError === 'function') { Y.logError('Report', 'Failed to collect debug info', err); } return getFallbackDebugInfo(); } } function getTypeLabel(type) { const typeMap = { bug: t('typeBug'), feature: t('typeFeature'), other: t('typeOther'), }; return typeMap[type] || typeMap.other; } function getTitlePrefix(type) { const prefixMap = { bug: '[Bug]', feature: '[Feature]', other: '[YouTube+][Report]', }; return prefixMap[type] || prefixMap.other; } function createMinimalDebugJson(debug) { const minimalDebug = { version: debug.version || 'unknown', userAgent: debug.userAgent || 'unknown', url: debug.url || 'unknown', }; try { return JSON.stringify(minimalDebug, null, 2); } catch { return '{ "error": "Failed to stringify debug info" }'; } } function stringifyDebugInfo(debug) { try { return JSON.stringify(debug, null, 2); } catch (err) { if (Y && typeof Y.logError === 'function') { Y.logError('Report', 'Failed to stringify debug info', err); } return createMinimalDebugJson(debug); } } function addDebugSection(lines, debug) { lines.push('\n---\n**Debug info**\n'); lines.push('```json'); lines.push(stringifyDebugInfo(debug)); lines.push('```'); lines.push('\n_Please do not include sensitive personal data._'); } function buildBodyLines({ type, description, email, includeDebug }) { const debug = includeDebug ? getDebugInfo() : null; const lines = []; lines.push(`**Type:** ${getTypeLabel(type)}`); if (email) lines.push(`**Reporter email (optional):** ${email}`); lines.push('\n**Description:**\n'); lines.push(description || '(no description)'); if (debug) { addDebugSection(lines, debug); } return lines; } function buildIssuePayload(params) { const { type, title } = params; const lines = buildBodyLines(params); const body = lines.join('\n'); const issueTitle = `${getTitlePrefix(type)} ${title || ''}`.trim(); return { title: issueTitle, body }; } function openGitHubIssue(payload) { try { const repoOwner = 'diorhc'; const repo = 'YTP'; const url = `https://github.com/${repoOwner}/${repo}/issues/new?title=${encodeURIComponent( payload.title )}&body=${encodeURIComponent(payload.body)}`; window.open(url, '_blank'); } catch (err) { if (Y && typeof Y.logError === 'function') { Y.logError('Report', 'Failed to open GitHub issue', err); } throw err; } } function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text); } return new Promise((resolve, reject) => { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.style.opacity = '0'; document.body.appendChild(ta); try { ta.select(); ta.setSelectionRange(0, text.length); const success = document.execCommand('copy'); document.body.removeChild(ta); if (success) { resolve(); } else { reject(new Error('execCommand failed')); } } catch (err) { document.body.removeChild(ta); reject(err); } }); } function createTypeSelect() { const typeSelect = mk( 'select', { style: 'padding:var(--yt-space-sm);border-radius:var(--yt-radius-sm);background:var(--yt-input-bg);color:var(--yt-text-primary);border:1px solid var(--yt-glass-border);backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);font-size:14px;cursor:pointer;transition:var(--yt-transition);', }, [] ); [ { v: 'bug', l: t('typeBug') }, { v: 'feature', l: t('typeFeature') }, { v: 'other', l: t('typeOther') }, ].forEach(opt => { const o = mk('option', { value: opt.v }, [opt.l]); typeSelect.appendChild(o); }); return typeSelect; } function createFormInputs() { const inputStyle = 'padding:var(--yt-space-sm);border-radius:var(--yt-radius-sm);background:var(--yt-input-bg);color:var(--yt-text-primary);border:1px solid var(--yt-glass-border);backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);font-size:14px;transition:var(--yt-transition);box-sizing:border-box;'; const titleInput = mk('input', { placeholder: t('shortTitle'), style: inputStyle, }); const emailInput = mk('input', { placeholder: t('emailOptional'), type: 'email', style: inputStyle, }); const descInput = mk('textarea', { placeholder: t('descriptionPlaceholder'), rows: 6, style: `${inputStyle}resize:vertical;font-family:inherit;`, }); return { titleInput, emailInput, descInput }; } function createDebugCheckbox() { const debugCheckboxInput = mk('input', { type: 'checkbox', class: 'ytp-plus-settings-checkbox', }); const includeDebug = mk( 'label', { style: 'font-size:13px;display:flex;gap:var(--yt-space-sm);align-items:center;color:var(--yt-text-primary);cursor:pointer;align-self:center;', }, [debugCheckboxInput, ` ${t('includeDebug')}`] ); return { includeDebug, debugCheckboxInput }; } function createActionButtons() { const actions = mk('div', { style: 'display:flex;gap:var(--yt-space-sm);margin-top:var(--yt-space-sm);flex-wrap:wrap;', }); const submitBtn = mk('button', { class: 'glass-button' }, [t('openGitHub')]); const copyBtn = mk('button', { class: 'glass-button' }, [t('copyReport')]); const emailBtn = mk('button', { class: 'glass-button' }, [t('prepareEmail')]); actions.appendChild(submitBtn); actions.appendChild(copyBtn); actions.appendChild(emailBtn); return { actions, submitBtn, copyBtn, emailBtn }; } function createDebugPreview() { return mk( 'div', { class: 'glass-card', style: 'overflow:auto;max-height:240px;font-size:11px;display:none;margin-top:var(--yt-space-sm);padding:8px;box-sizing:border-box;', }, [] ); } function renderReportSection(modal) { if (!modal || !modal.querySelector) return; const section = modal.querySelector('.ytp-plus-settings-section[data-section="report"]'); if (!section) return; let handlers = window.YouTubeReportHandlers || {}; if (!handlers.gatherFormData) { try { require('./report-handlers.js'); } catch (err) { if (err) { } } handlers = window.YouTubeReportHandlers || {}; if (!handlers.gatherFormData) { if (Y.logError) { Y.logError('Report', 'YouTubeReportHandlers not loaded', new Error('Missing handlers')); } return; } } section.innerHTML = ''; const form = mk('div', { style: 'display:flex;flex-direction:column;gap:var(--yt-space-sm);margin-top:var(--yt-space-md);', }); const typeSelect = createTypeSelect(); const { titleInput, emailInput, descInput } = createFormInputs(); const { includeDebug, debugCheckboxInput } = createDebugCheckbox(); const { actions, submitBtn, copyBtn, emailBtn } = createActionButtons(); const debugPreview = createDebugPreview(); form.appendChild(typeSelect); form.appendChild(titleInput); form.appendChild(emailInput); form.appendChild(descInput); form.appendChild(includeDebug); form.appendChild(debugPreview); form.appendChild(actions); const privacy = mk( 'div', { class: 'ytp-plus-settings-item-description', style: 'margin-top:var(--yt-space-sm);font-size:12px;color:var(--yt-text-secondary);', }, [t('privacy')] ); section.appendChild(form); section.appendChild(privacy); const gather = () => handlers.gatherFormData( { typeSelect, titleInput, descInput, emailInput, debugCheckbox: debugCheckboxInput }, t, { validateTitle, validateDescription, isValidEmail } ); debugCheckboxInput.addEventListener('change', () => handlers.updateDebugPreview(debugCheckboxInput, debugPreview, getDebugInfo, mk) ); submitBtn.addEventListener('click', e => handlers.handleGitHubSubmit(e, { submitBtn }, gather, buildIssuePayload, openGitHubIssue, t) ); copyBtn.addEventListener('click', e => handlers.handleCopyReport(e, { copyBtn }, gather, buildIssuePayload, copyToClipboard, t) ); emailBtn.addEventListener('click', e => handlers.handleEmailReport(e, { emailBtn }, gather, buildIssuePayload, t) ); } try { (window).youtubePlusReport = (window).youtubePlusReport || {}; (window).youtubePlusReport.render = renderReportSection; } catch (e) { if (Y.logError) Y.logError('Report', 'Failed to attach report module to window', e); } })(); (function () { 'use strict'; const _globalI18n = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n && typeof _globalI18n.t === 'function') { return _globalI18n.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; const getLanguage = () => { try { if (_globalI18n && typeof _globalI18n.getLanguage === 'function') { return _globalI18n.getLanguage(); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.getLanguage === 'function' ) { return window.YouTubeUtils.getLanguage(); } } catch { } const lang = document.documentElement.lang || navigator.language || 'en'; return lang.startsWith('ru') ? 'ru' : 'en'; }; const UPDATE_CONFIG = { enabled: true, checkInterval: 24 * 60 * 60 * 1000, updateUrl: 'https://update.greasyfork.icu/scripts/537017/YouTube%20%2B.meta.js', currentVersion: '2.2', storageKey: 'youtube_plus_update_check', notificationDuration: 8000, autoInstallUrl: 'https://update.greasyfork.icu/scripts/537017/YouTube%20%2B.user.js', }; const windowRef = typeof window === 'undefined' ? null : window; const GM_namespace = windowRef?.GM || null; const GM_info_safe = windowRef?.GM_info || null; const GM_openInTab_safe = (() => { if (windowRef) { if (typeof windowRef.GM_openInTab === 'function') { return windowRef.GM_openInTab.bind(windowRef); } if (GM_namespace?.openInTab) { return GM_namespace.openInTab.bind(GM_namespace); } } return null; })(); if (GM_info_safe?.script?.version) { UPDATE_CONFIG.currentVersion = GM_info_safe.script.version; } const updateState = { lastCheck: 0, lastVersion: UPDATE_CONFIG.currentVersion, updateAvailable: true, checkInProgress: false, updateDetails: null, }; function getRussianPluralIndex(num) { const mod10 = num % 10; const mod100 = num % 100; if (mod10 === 1 && mod100 !== 11) return 0; if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) return 1; return 2; } function getRussianForms(unit) { return { day: ['день', 'дня', 'дней'], hour: ['час', 'часа', 'часов'], minute: ['минута', 'минуты', 'минут'], }[unit]; } function getEnglishForms(unit) { return { day: ['day', 'days'], hour: ['hour', 'hours'], minute: ['minute', 'minutes'], }[unit]; } function pluralizeTime(n, unit) { const lang = getLanguage(); const num = Math.abs(Number(n)) || 0; if (lang === 'ru') { const forms = getRussianForms(unit); const idx = getRussianPluralIndex(num); return `${num} ${forms[idx]}`; } const enForms = getEnglishForms(unit); return `${num} ${num === 1 ? enForms[0] : enForms[1]}`; } const utils = { loadSettings: () => { try { const saved = localStorage.getItem(UPDATE_CONFIG.storageKey); if (!saved) { return; } const parsed = JSON.parse(saved); if (typeof parsed !== 'object' || parsed === null) { console.error('[YouTube+][Update]', 'Invalid settings structure'); return; } if (typeof parsed.lastCheck === 'number' && parsed.lastCheck >= 0) { updateState.lastCheck = parsed.lastCheck; } if (typeof parsed.lastVersion === 'string') { const ver = parsed.lastVersion.replace(/^v/i, ''); if (/^\d+(?:\.\d+){0,2}$/.test(ver)) { updateState.lastVersion = ver; } } if (typeof parsed.updateAvailable === 'boolean') { updateState.updateAvailable = parsed.updateAvailable; } if (parsed.updateDetails && typeof parsed.updateDetails === 'object') { if ( typeof parsed.updateDetails.version === 'string' && /^\d+\.\d+\.\d+/.test(parsed.updateDetails.version) ) { updateState.updateDetails = parsed.updateDetails; } } } catch (e) { console.error('[YouTube+][Update]', 'Failed to load update settings:', e); } }, saveSettings: () => { try { const dataToSave = { lastCheck: updateState.lastCheck, lastVersion: updateState.lastVersion, updateAvailable: updateState.updateAvailable, updateDetails: updateState.updateDetails, }; localStorage.setItem(UPDATE_CONFIG.storageKey, JSON.stringify(dataToSave)); } catch (e) { console.error('[YouTube+][Update]', 'Failed to save update settings:', e); } }, compareVersions: (v1, v2) => { if (typeof v1 !== 'string' || typeof v2 !== 'string') { console.error('[YouTube+][Update]', 'Invalid version format - must be strings'); return 0; } const normalize = v => v .replace(/[^\d.]/g, '') .split('.') .map(n => parseInt(n, 10) || 0); const [parts1, parts2] = [normalize(v1), normalize(v2)]; const maxLength = Math.max(parts1.length, parts2.length); for (let i = 0; i < maxLength; i++) { const diff = (parts1[i] || 0) - (parts2[i] || 0); if (diff !== 0) { return diff; } } return 0; }, parseMetadata: text => { if (typeof text !== 'string' || text.length > 100000) { console.error('[YouTube+][Update]', 'Invalid metadata text'); return { version: null, description: '', downloadUrl: UPDATE_CONFIG.autoInstallUrl }; } const extractField = field => text.match(new RegExp(`@${field}\\s+([^\\r\\n]+)`))?.[1]?.trim(); let version = extractField('version'); const description = extractField('description') || ''; const downloadUrl = extractField('downloadURL') || UPDATE_CONFIG.autoInstallUrl; if (version) { version = version.replace(/^v/i, '').trim(); if (!/^\d+(?:\.\d+){0,2}$/.test(version)) { console.error('[YouTube+][Update]', 'Invalid version format in metadata:', version); return { version: null, description: '', downloadUrl: UPDATE_CONFIG.autoInstallUrl }; } } return { version, description: description.substring(0, 500), downloadUrl, }; }, formatTimeAgo: timestamp => { if (!timestamp) return t('never'); const diffMs = Date.now() - timestamp; const diffDays = Math.floor(diffMs / 86400000); const diffHours = Math.floor(diffMs / 3600000); const diffMinutes = Math.floor(diffMs / 60000); if (diffDays > 0) return pluralizeTime(diffDays, 'day'); if (diffHours > 0) return pluralizeTime(diffHours, 'hour'); if (diffMinutes > 0) return pluralizeTime(diffMinutes, 'minute'); return t('justNow'); }, showNotification: (text, type = 'info', duration = 3000) => { try { YouTubeUtils.NotificationManager.show(text, { type, duration }); } catch (error) { console.log(`[YouTube+] ${type.toUpperCase()}:`, text, error); } }, }; const validateDownloadUrl = downloadUrl => { if (!downloadUrl || typeof downloadUrl !== 'string') { return { valid: false, error: 'Invalid download URL for installation' }; } try { const parsedUrl = new URL(downloadUrl); const allowedDomains = ['update.greasyfork.org', 'greasyfork.org']; if (parsedUrl.protocol !== 'https:') { return { valid: false, error: 'Only HTTPS URLs allowed for updates' }; } if (!allowedDomains.includes(parsedUrl.hostname)) { return { valid: false, error: `Update URL domain not in allowlist: ${parsedUrl.hostname}` }; } return { valid: true, error: null }; } catch (error) { return { valid: false, error: `Invalid URL format: ${error.message}` }; } }; const markUpdateDismissed = details => { if (details?.version && typeof details.version === 'string') { try { sessionStorage.setItem('update_dismissed', details.version); } catch (err) { console.error('[YouTube+][Update]', 'Failed to persist dismissal state:', err); } } }; const tryOpenUpdateUrl = url => { if (GM_openInTab_safe) { try { GM_openInTab_safe(url, { active: true, insert: true, setParent: true }); return true; } catch (gmError) { console.error('[YouTube+] GM_openInTab update install failed:', gmError); } } try { const popup = window.open(url, '_blank', 'noopener'); if (popup) return true; } catch (popupError) { console.error('[YouTube+] window.open update install failed:', popupError); } try { window.location.assign(url); return true; } catch (navigationError) { console.error('[YouTube+] Navigation to update URL failed:', navigationError); } return false; }; const installUpdate = (details = updateState.updateDetails) => { const downloadUrl = details?.downloadUrl || UPDATE_CONFIG.autoInstallUrl; const validation = validateDownloadUrl(downloadUrl); if (!validation.valid) { console.error('[YouTube+][Update]', validation.error); return false; } const success = tryOpenUpdateUrl(downloadUrl); if (success) { markUpdateDismissed(details); } return success; }; const showUpdateNotification = updateDetails => { const notification = document.createElement('div'); notification.className = 'youtube-enhancer-notification update-notification'; notification.style.cssText = ` z-index: 10001; max-width: 350px; background: linear-gradient(135deg, rgba(255, 69, 0, 0.95), rgba(255, 140, 0, 0.95)); color: white; padding: 16px 20px; border-radius: 12px; box-shadow: 0 8px 32px rgba(255, 69, 0, 0.4); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.2); animation: slideInFromBottom 0.4s ease-out; `; notification.innerHTML = `
${t('updateAvailableTitle')}
${t('version')} ${updateDetails.version} • ${updateDetails.description || t('newFeatures')}
`; const _containerId = 'youtube-enhancer-notification-container'; let _container = document.getElementById(_containerId); if (!_container) { _container = document.createElement('div'); _container.id = _containerId; _container.className = 'youtube-enhancer-notification-container'; try { document.body.appendChild(_container); } catch { document.body.appendChild(notification); } } try { _container.insertBefore(notification, _container.firstChild); } catch { document.body.appendChild(notification); } const removeNotification = () => { notification.style.animation = 'slideOutToBottom 0.35s ease-in forwards'; setTimeout(() => notification.remove(), 360); }; const installBtn = notification.querySelector('#update-install-btn'); if (installBtn) { installBtn.addEventListener('click', () => { const success = installUpdate(updateDetails); if (success) { removeNotification(); setTimeout(() => utils.showNotification(t('installing')), 500); } else { utils.showNotification(t('manualInstallHint'), 'error', 5000); window.open('https://greasyfork.org/en/scripts/537017-youtube', '_blank'); } }); } const dismissBtn = notification.querySelector('#update-dismiss-btn'); if (dismissBtn) { dismissBtn.addEventListener('click', () => { if (updateDetails?.version) { sessionStorage.setItem('update_dismissed', updateDetails.version); } removeNotification(); }); } const closeBtn = notification.querySelector('#update-close-btn'); if (closeBtn) { closeBtn.addEventListener('click', () => { if (updateDetails?.version) { sessionStorage.setItem('update_dismissed', updateDetails.version); } removeNotification(); }); } setTimeout(() => { if (notification.isConnected) removeNotification(); }, UPDATE_CONFIG.notificationDuration); }; const validateUpdateUrl = url => { const parsedUrl = new URL(url); if (parsedUrl.protocol !== 'https:') { throw new Error('Update URL must use HTTPS'); } if (!parsedUrl.hostname.includes('greasyfork.org')) { throw new Error('Update URL must be from greasyfork.org'); } }; const fetchUpdateMetadata = async (url = UPDATE_CONFIG.updateUrl) => { const fetchMeta = async requestUrl => { if (typeof GM_xmlhttpRequest !== 'undefined') { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => reject(new Error('Update check timeout')), 10000); GM_xmlhttpRequest({ method: 'GET', url: requestUrl, timeout: 10000, headers: { Accept: 'text/plain', 'User-Agent': 'YouTube+ UpdateChecker' }, onload: response => { clearTimeout(timeoutId); if (response.status >= 200 && response.status < 300) resolve(response.responseText); else reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); }, onerror: e => { clearTimeout(timeoutId); reject(new Error(`Network error: ${e}`)); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error('Update check timeout')); }, }); }); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const res = await fetch(requestUrl, { method: 'GET', cache: 'no-cache', signal: controller.signal, headers: { Accept: 'text/plain', 'User-Agent': 'YouTube+ UpdateChecker' }, }); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return await res.text(); } finally { clearTimeout(timeoutId); } }; return await fetchMeta(url); }; const handleUpdateResult = (updateDetails, force) => { const shouldShowNotification = updateState.updateAvailable && (force || sessionStorage.getItem('update_dismissed') !== updateDetails.version); if (shouldShowNotification) { showUpdateNotification(updateDetails); console.log(`YouTube + Update available: ${updateDetails.version}`); return; } if (force) { const message = updateState.updateAvailable ? t('updateAvailableMsg').replace('{version}', updateDetails.version) : t('upToDateMsg').replace('{version}', UPDATE_CONFIG.currentVersion); utils.showNotification(message); } }; const isTransientError = error => { return ( error.name === 'AbortError' || error.name === 'NetworkError' || (error.message && error.message.includes('fetch')) || (error.message && error.message.includes('network')) ); }; const retrieveUpdateDetails = async () => { let metaText = await fetchUpdateMetadata(UPDATE_CONFIG.updateUrl); let details = utils.parseMetadata(metaText); if (!details.version) { try { const fallbackText = await fetchUpdateMetadata(UPDATE_CONFIG.autoInstallUrl); const fallbackDetails = utils.parseMetadata(fallbackText); if (fallbackDetails.version) { details = fallbackDetails; metaText = fallbackText; } } catch (fallbackErr) { if (typeof console !== 'undefined' && console.warn) { console.warn('[YouTube+][Update] Fallback metadata fetch failed:', fallbackErr.message); } } } return details; }; const shouldCheckForUpdates = (force, now) => { if (!UPDATE_CONFIG.enabled || updateState.checkInProgress) { return false; } return force || now - updateState.lastCheck >= UPDATE_CONFIG.checkInterval; }; const validateUpdateConfiguration = () => { try { validateUpdateUrl(UPDATE_CONFIG.updateUrl); return true; } catch (urlError) { console.error('[YouTube+][Update]', 'Invalid update URL configuration:', urlError); throw urlError; } }; const processUpdateDetails = (updateDetails, force, now) => { updateState.lastCheck = now; updateState.lastVersion = updateDetails.version; updateState.updateDetails = updateDetails; const comparison = utils.compareVersions(UPDATE_CONFIG.currentVersion, updateDetails.version); updateState.updateAvailable = comparison < 0; handleUpdateResult(updateDetails, force); utils.saveSettings(); }; const handleMissingUpdateInfo = force => { updateState.updateAvailable = false; if (force) { utils.showNotification( t('updateCheckFailed').replace('{msg}', t('noUpdateInfo')), 'error', 4000 ); } }; const handleUpdateRetry = async (error, force, retryCount) => { const MAX_RETRIES = 2; const RETRY_DELAY = 2000; if (isTransientError(error) && retryCount < MAX_RETRIES) { console.warn( `[YouTube+][Update] Retry ${retryCount + 1}/${MAX_RETRIES} after error:`, error.message ); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, retryCount))); return checkForUpdates(force, retryCount + 1); } console.error('[YouTube+][Update] Check failed after retries:', error); if (force) { utils.showNotification(t('updateCheckFailed').replace('{msg}', error.message), 'error', 4000); } }; const checkForUpdates = async (force = false, retryCount = 0) => { const now = Date.now(); if (!shouldCheckForUpdates(force, now)) { return; } updateState.checkInProgress = true; try { validateUpdateConfiguration(); const updateDetails = await retrieveUpdateDetails(); if (updateDetails.version) { processUpdateDetails(updateDetails, force, now); } else { handleMissingUpdateInfo(force); } } catch (error) { await handleUpdateRetry(error, force, retryCount); } finally { updateState.checkInProgress = false; } }; const addUpdateSettings = () => { const aboutSection = YouTubeUtils.querySelector( '.ytp-plus-settings-section[data-section="about"]' ); if (!aboutSection || YouTubeUtils.querySelector('.update-settings-container')) return; const updateContainer = document.createElement('div'); updateContainer.className = 'update-settings-container'; updateContainer.style.cssText = ` padding: 16px; margin-top: 20px; border-radius: 12px; background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02)); border: 1px solid var(--yt-glass-border); backdrop-filter: blur(8px); `; const lastCheckTime = utils.formatTimeAgo(updateState.lastCheck); updateContainer.innerHTML = `

${t('enhancedExperience')}

${t('currentVersion')} ${UPDATE_CONFIG.currentVersion}
${t('lastChecked')}: ${lastCheckTime} ${ updateState.lastVersion && updateState.lastVersion !== UPDATE_CONFIG.currentVersion ? `
${t('latestAvailable')}: ${updateState.lastVersion}` : '' }
${ updateState.updateAvailable ? `
${t('updateAvailable')}
` : `
${t('upToDate')}
` }
`; aboutSection.appendChild(updateContainer); const attachClickHandler = (id, handler) => { const element = document.getElementById(id); if (element) YouTubeUtils.cleanupManager.registerListener(element, 'click', handler); }; attachClickHandler('manual-update-check', async ({ target }) => { const button = (target); const originalHTML = button.innerHTML; button.innerHTML = ` ${t('checkingForUpdates')} `; button.disabled = true; await checkForUpdates(true); setTimeout(() => { button.innerHTML = originalHTML; button.disabled = false; }, 1000); }); attachClickHandler('install-update-btn', () => { const success = installUpdate(); if (success) { utils.showNotification(t('installing')); } else { utils.showNotification(t('manualInstallHint'), 'error', 5000); window.open('https://greasyfork.org/en/scripts/537017-youtube', '_blank'); } }); attachClickHandler('open-update-page', () => { utils.showNotification(t('updatePageFallback')); window.open('https://greasyfork.org/en/scripts/537017-youtube', '_blank'); }); }; const setupUpdateChecks = () => { setTimeout(() => checkForUpdates(), 3000); const intervalId = setInterval(() => checkForUpdates(), UPDATE_CONFIG.checkInterval); YouTubeUtils.cleanupManager.registerInterval(intervalId); window.addEventListener('beforeunload', () => clearInterval(intervalId)); }; const handleSettingsModalMutation = (mutation, state) => { for (const node of mutation.addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { state.settingsObserved = true; setTimeout(addUpdateSettings, 100); return true; } } return false; }; const handleAboutNavItemMutation = () => { const aboutNavItem = YouTubeUtils.querySelector( '.ytp-plus-settings-nav-item[data-section="about"].active:not([data-observed])' ); if (aboutNavItem) { aboutNavItem.setAttribute('data-observed', ''); setTimeout(addUpdateSettings, 50); } }; const createSettingsObserver = () => { const state = { settingsObserved: false }; const observer = new MutationObserver(mutations => { if (state.settingsObserved) return; for (const mutation of mutations) { if (handleSettingsModalMutation(mutation, state)) { return; } } handleAboutNavItemMutation(); }); return observer; }; const setupSettingsObserver = () => { const observer = createSettingsObserver(); YouTubeUtils.cleanupManager.registerObserver(observer); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); } }; const setupAboutClickHandler = () => { const clickHandler = ({ target }) => { const el = (target); if (el.classList?.contains('ytp-plus-settings-nav-item') && el.dataset?.section === 'about') { setTimeout(addUpdateSettings, 50); } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, { passive: true, capture: true, }); }; const logInitialization = () => { if (typeof console !== 'undefined' && console.log) { console.log('YouTube + Update Checker initialized', { version: UPDATE_CONFIG.currentVersion, enabled: UPDATE_CONFIG.enabled, lastCheck: new Date(updateState.lastCheck).toLocaleString(), updateAvailable: updateState.updateAvailable, }); } }; const init = () => { utils.loadSettings(); setupUpdateChecks(); setupSettingsObserver(); setupAboutClickHandler(); logInitialization(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); (function () { try { const host = typeof location === 'undefined' ? '' : location.hostname; if (!host) return; if (!/(^|\.)youtube\.com$/.test(host) && !/\.youtube\.google/.test(host)) return; const css = ` #inline-preview-player {transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) 1s !important; transform: scale(1) !important;} #video-preview-container:has(#inline-preview-player) {transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; border-radius: 1.2em !important; overflow: hidden !important; transform: scale(1) !important;} #video-preview-container:has(#inline-preview-player):hover {transform: scale(1.25) !important; box-shadow: #0008 0px 0px 60px !important; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s !important;} ytd-app #content {opacity: 1 !important; transition: opacity 0.3s ease-in-out !important;} ytd-app:has(#video-preview-container:hover) #content {opacity: 0.5 !important; transition: opacity 4s ease-in-out 1s !important;} #page-manager, yt-searchbox {transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.35) !important;} #masthead yt-searchbox button[aria-label="Search"] {display: none !important;} .ytSearchboxComponentInputBox {border-radius: 2em !important;} yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) {position: relative !important; left: 0vw !important; top: -30vh !important; height: 40px !important; max-width: 600px !important; transform: scale(1) !important;} @media only screen and (min-width: 1400px) {yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) { height: 60px !important; max-width: 700px !important; transform: scale(1.1) !important;} } yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) .ytSearchboxComponentInputBox, yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {background-color: #fffb !important; box-shadow: black 0 0 30px !important;} @media (prefers-color-scheme: dark) {yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) .ytSearchboxComponentInputBox, yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {background-color: #000b !important;}} yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {margin-top: 10px !important;} @media only screen and (min-width: 1400px) {yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {margin-top: 30px !important;}} .ytd-masthead #center:has(.ytSearchboxComponentInputBoxHasFocus) {height: 100vh !important; width: 100vw !important; left: 0 !important; top: 0 !important; position: fixed !important; justify-content: center !important; align-items: center !important;} #content:has(.ytSearchboxComponentInputBoxHasFocus) #page-manager {filter: blur(20px) !important; transform: scale(1.05) !important;} #voice-search-button {display: none !important;} #masthead-container {#background.ytd-masthead {background-color: #00000000 !important;}} ytd-mini-guide-renderer, [theater=""] #contentContainer::after {display: none !important;} tp-yt-app-drawer > #contentContainer:not([opened=""]), #contentContainer:not([opened=""]) #guide-content, ytd-mini-guide-renderer, ytd-mini-guide-entry-renderer {background-color: var(--yt-spec-text-primary-inverse) !important; background: var(--yt-spec-text-primary-inverse) !important;} #content:not(:has(#contentContainer[opened=""])) #page-manager {margin-left: 0 !important;} ytd-app:not([guide-persistent-and-visible=""]) tp-yt-app-drawer > #contentContainer {background-color: var(--yt-spec-text-primary-inverse) !important;} ytd-alert-with-button-renderer {align-items: center !important; justify-content: center !important;} ytd-guide-section-renderer:has([title="YouTube Premium"]), ytd-guide-renderer #footer {display: none !important;} ytd-guide-section-renderer, ytd-guide-collapsible-section-entry-renderer {border: none !important;} `; const ID = 'ytp-zen-features-style'; if (document.getElementById(ID)) return; const style = document.createElement('style'); style.id = ID; style.appendChild(document.createTextNode(css)); (document.head || document.documentElement).appendChild(style); } catch (err) { console.error('zen-youtube-features injection failed', err); } })(); window.YouTubePlusMusicUtils = (() => { 'use strict'; function isScrollable(el) { if (!el || el.scrollHeight <= el.clientHeight + 10) return false; try { const style = window.getComputedStyle(el); const overflowY = style && style.overflowY; return overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay'; } catch { return false; } } function findScrollContainer(root) { if (!root) return null; const contents = root.querySelector('#contents'); if (contents && contents.scrollHeight > contents.clientHeight) return contents; const all = root.querySelectorAll('*'); for (let i = 0; i < all.length; i++) { if (isScrollable(all[i])) return all[i]; } if (isScrollable(root)) return root; return null; } function setupButtonStyles(button, container) { if (!button || !container) return; container.style.position = container.style.position || 'relative'; button.style.position = 'absolute'; button.style.bottom = '16px'; button.style.right = '16px'; button.style.zIndex = '1000'; } function debounce(fn, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } function setupScrollVisibility(button, scrollContainer, threshold = 100) { if (!button || !scrollContainer) return () => {}; const scrollHandler = debounce(() => { button.classList.toggle('visible', scrollContainer.scrollTop > threshold); }, 100); scrollContainer.addEventListener('scroll', scrollHandler, { passive: true }); button.classList.toggle('visible', scrollContainer.scrollTop > threshold); return () => { scrollContainer.removeEventListener('scroll', scrollHandler); }; } function setupScrollToTop(button, scrollContainer) { if (!button || !scrollContainer) return; button.addEventListener('click', () => { try { scrollContainer.scrollTo({ top: 0, behavior: 'smooth' }); } catch (err) { console.error('[YouTube+][Music] Error scrolling to top:', err); } }); } return { isScrollable, findScrollContainer, setupButtonStyles, debounce, setupScrollVisibility, setupScrollToTop, }; })(); (function () { 'use strict'; const enhancedStyles = ` ytmusic-app-layout[is-bauhaus-sidenav-enabled] #nav-bar-background.ytmusic-app-layout { border-bottom: none !important; box-shadow: none !important; } ytmusic-app-layout[is-bauhaus-sidenav-enabled] #nav-bar-divider.ytmusic-app-layout { border-top: none !important; } ytmusic-app-layout[is-bauhaus-sidenav-enabled] #mini-guide-background.ytmusic-app-layout { border-right: 0 !important; } ytmusic-nav-bar, ytmusic-app-layout[is-bauhaus-sidenav-enabled] .ytmusic-nav-bar { border: none !important; box-shadow: none !important; } ytmusic-settings-button.style-scope.ytmusic-nav-bar, ytmusic-nav-bar ytmusic-settings-button.style-scope.ytmusic-nav-bar {position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; bottom: auto !important; margin: 0 !important; z-index: 1000 !important;} ytmusic-search-box, ytmusic-nav-bar ytmusic-search-box, ytmusic-searchbox, ytmusic-nav-bar ytmusic-searchbox {position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; margin: 0 !important; max-width: 75% !important; width: auto !important; z-index: 900 !important;} ytmusic-search-box:has(input:focus), ytmusic-searchbox:has(input:focus), ytmusic-search-box:focus-within, ytmusic-searchbox:focus-within {position: fixed !important; left: 50% !important; top: 12vh !important; transform: translateX(-50%) !important; height: auto !important; max-width: 900px !important; width: min(90vw, 900px) !important; z-index: 1200 !important; display: block !important;} @media only screen and (min-width: 1400px) {ytmusic-search-box:has(input:focus), ytmusic-searchbox:has(input:focus) {top: 10vh !important; max-width: 1000px !important; transform: translateX(-50%) scale(1.05) !important;}} ytmusic-search-box:has(input:focus) input, ytmusic-searchbox:has(input:focus) input, ytmusic-search-box:focus-within input, ytmusic-searchbox:focus-within input {background-color: #fffb !important; box-shadow: black 0 0 30px !important;} @media (prefers-color-scheme: dark) {ytmusic-search-box:has(input:focus) input, ytmusic-searchbox:has(input:focus) input {background-color: #000b !important;}} ytmusic-app-layout:has(ytmusic-search-box:has(input:focus)) #main-panel, ytmusic-app-layout:has(ytmusic-searchbox:has(input:focus)) #main-panel {filter: blur(18px) !important; transform: scale(1.03) !important;} `; const hoverStyles = ` .ytmusic-guide-renderer {opacity: 0.01 !important; transition: opacity 0.5s ease-in-out !important;} .ytmusic-guide-renderer:hover { opacity: 1 !important;} ytmusic-app[is-bauhaus-sidenav-enabled] #guide-wrapper.ytmusic-app {background-color: transparent !important; border: none !important;} `; const playerSidebarStyles = ` #side-panel {width: 40em !important; height: 80vh !important; padding: 0 2em !important; right: -30em !important; top: 10vh !important; opacity: 0 !important; position: absolute !important; transition: all 0.3s ease-in-out !important; backdrop-filter: blur(5px) !important; background-color: #0005 !important; border-radius: 1em !important; box-shadow: rgba(0, 0, 0, 0.15) 0px -36px 30px inset, rgba(0, 0, 0, 0.1) 0px -79px 40px inset, rgba(0, 0, 0, 0.06) 0px 2px 1px, rgba(0, 0, 0, 0.09) 0px 4px 2px, rgba(0, 0, 0, 0.09) 0px 8px 4px, rgba(0, 0, 0, 0.09) 0px 16px 8px, rgba(0, 0, 0, 0.09) 0px 32px 16px !important;} #side-panel tp-yt-paper-tabs {transition: height 0.3s ease-in-out !important; height: 0 !important;} #side-panel:hover {right: 0 !important; opacity: 1 !important;} #side-panel:hover tp-yt-paper-tabs {height: 4em !important;} #side-panel:has(ytmusic-tab-renderer[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"]):not(:has(ytmusic-message-renderer:not([style="display: none;"]))) {right: 0 !important; opacity: 1 !important;} #side-panel {min-width: auto !important;} `; const centeredPlayerStyles = ` ytmusic-app-layout:not([player-ui-state="FULLSCREEN"]) #main-panel {position: absolute !important; height: 70vh !important; max-width: 70vw !important; aspect-ratio: 1 !important; top: 50vh !important; left: 50vw !important; transform: translate(-50%, -50%) !important;} #player-page {padding: 0 !important; margin: 0 !important; left: 0 !important; top: 0 !important; height: 100% !important; width: 100% !important;} `; const playerBarStyles = ` ytmusic-player-bar, #player-bar-background {margin: 1vw !important; width: 98vw !important; border-radius: 1em !important; overflow: hidden !important; transition: all 0.5s ease-in-out !important; background-color: #0002 !important; box-shadow: rgba(0, 0, 0, 0.15) 0px -36px 30px inset, rgba(0, 0, 0, 0.1) 0px -79px 40px inset, rgba(0, 0, 0, 0.06) 0px 2px 1px, rgba(0, 0, 0, 0.09) 0px 4px 2px, rgba(0, 0, 0, 0.09) 0px 8px 4px, rgba(0, 0, 0, 0.09) 0px 16px 8px, rgba(0, 0, 0, 0.09) 0px 32px 16px !important;} #layout:not([player-ui-state="PLAYER_PAGE_OPEN"]) #player-bar-background {background-color: #0005 !important;} `; const centeredPlayerBarStyles = ` #left-controls {position: absolute !important; left: 49vw !important; bottom: 15px !important; transform: translateX(-50%) !important; width: fit-content !important; order: 1 !important;} .time-info {position: absolute !important; bottom: -10px !important; left: 0 !important; width: 100% !important; text-align: center !important; padding: 0 !important; margin: 0 !important;} .middle-controls {position: absolute !important; left: 1vw !important; bottom: 15px !important; max-width: 30vw !important; order: 0 !important;} `; const miniPlayerStyles = ` #main-panel:has(ytmusic-player[player-ui-state="MINIPLAYER"]) {position: fixed !important; width: 100vw !important; height: 100vh !important; top: -100vh !important; left: 0 !important; margin: 0 !important; padding: 0 !important; transform: none !important; max-width: 100vw !important;} ytmusic-player[player-ui-state="MINIPLAYER"] {position: fixed !important; bottom: calc(100vh + 120px) !important; right: 30px !important; width: 350px !important; height: fit-content !important;} #av-id:has(ytmusic-av-toggle) {position: absolute !important; left: 50% !important; transform: translateX(-50%) !important; top: -4em !important; opacity: 0 !important; transition: all 0.3s ease-in-out !important;} #av-id:has(ytmusic-av-toggle):hover {opacity: 1 !important;} #player[player-ui-state="MINIPLAYER"] {display: none !important;} `; const scrollToTopStyles = ` .ytmusic-top-button {position: absolute; bottom: 16px; right: 16px; width: 40px; height: 40px; background: rgba(255,255,255,.12); color: #fff; border: none; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; visibility: hidden; transition: all .3s; backdrop-filter: blur(12px) saturate(180%); -webkit-backdrop-filter: blur(12px) saturate(180%); border: 1px solid rgba(255,255,255,.18); box-shadow: 0 8px 32px 0 rgba(31,38,135,.18);} .ytmusic-top-button:hover {background: rgba(255,255,255,.18); transform: translateY(-2px) scale(1.07); box-shadow: 0 8px 32px rgba(0,0,0,.25);} .ytmusic-top-button.visible {opacity: 1; visibility: visible;} .ytmusic-top-button svg {transition: transform .2s;} .ytmusic-top-button:hover svg {transform: translateY(-1px) scale(1.1);} .ytmusic-top-button:focus {outline: none; box-shadow: 0 8px 32px rgba(0,0,0,.25);} .ytmusic-top-button:active {transform: translateY(0) scale(0.98);} .ytmusic-top-button.top-button { } `; function applyStyles() { if (window.location.hostname !== 'music.youtube.com') { return; } const allStyles = ` ${enhancedStyles} ${hoverStyles} ${playerSidebarStyles} ${centeredPlayerStyles} ${playerBarStyles} ${centeredPlayerBarStyles} ${miniPlayerStyles} ${scrollToTopStyles} `; if (typeof GM_addStyle === 'undefined') { const style = document.createElement('style'); style.textContent = allStyles; document.head.appendChild(style); } else { GM_addStyle(allStyles); } console.log('[YouTube+][Music]', 'Стили применены'); } const _globalI18n_music = typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null; const t = (key, params = {}) => { try { if (_globalI18n_music && typeof _globalI18n_music.t === 'function') { return _globalI18n_music.t(key, params); } if ( typeof window !== 'undefined' && window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function' ) { return window.YouTubeUtils.t(key, params); } } catch { } if (!key || typeof key !== 'string') return ''; if (Object.keys(params).length === 0) return key; let result = key; for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v)); return result; }; function createButton() { const button = document.createElement('button'); button.id = 'ytmusic-side-panel-top-button'; button.className = 'ytmusic-top-button top-button'; button.title = t('scrollToTop'); button.setAttribute('aria-label', t('scrollToTop')); button.innerHTML = ''; return button; } function findScrollContainer(sidePanel, MusicUtils) { const findContainer = MusicUtils.findScrollContainer || (root => { const contents = root?.querySelector('#contents'); if (contents && contents.scrollHeight > contents.clientHeight) return contents; if (root && root.scrollHeight > root.clientHeight + 10) return root; return null; }); return findContainer(sidePanel); } function setupScrollBehavior(button, sc, MusicUtils) { if (MusicUtils.setupScrollToTop) { MusicUtils.setupScrollToTop(button, sc); } else { button.addEventListener('click', () => { sc.scrollTo({ top: 0, behavior: 'smooth' }); }); } } function setupButtonPosition(button, sidePanel, MusicUtils) { if (MusicUtils.setupButtonStyles) { MusicUtils.setupButtonStyles(button, sidePanel); } else { sidePanel.style.position = sidePanel.style.position || 'relative'; button.style.position = 'absolute'; button.style.bottom = '16px'; button.style.right = '16px'; button.style.zIndex = '1000'; } } function setupScrollVisibility(button, sc, MusicUtils) { if (MusicUtils.setupScrollVisibility) { MusicUtils.setupScrollVisibility(button, sc, 100); } else { const debounce = (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounce(() => { button.classList.toggle('visible', sc.scrollTop > 100); }, 100); sc.addEventListener('scroll', scrollHandler, { passive: true }); button.classList.toggle('visible', sc.scrollTop > 100); } } function attachButtonToContainer(button, sidePanel, sc, MusicUtils) { try { setupScrollBehavior(button, sc, MusicUtils); setupButtonPosition(button, sidePanel, MusicUtils); sidePanel.appendChild(button); setupScrollVisibility(button, sc, MusicUtils); console.log('[YouTube+][Music]', 'Кнопка scroll to top создана'); } catch (err) { console.error('[YouTube+][Music] attachButton error:', err); } } function createScrollToTopButton() { try { if (window.location.hostname !== 'music.youtube.com') return; const sidePanel = document.querySelector('#side-panel'); if (!sidePanel || document.getElementById('ytmusic-side-panel-top-button')) return; const MusicUtils = window.YouTubePlusMusicUtils || {}; const button = createButton(); const scrollContainer = findScrollContainer(sidePanel, MusicUtils); if (!scrollContainer) { setTimeout(() => { const sc = findScrollContainer(sidePanel, MusicUtils); if (sc) attachButtonToContainer(button, sidePanel, sc, MusicUtils); }, 400); return; } attachButtonToContainer(button, sidePanel, scrollContainer, MusicUtils); } catch (error) { console.error('[YouTube+][Music] Error creating scroll to top button:', error); } } function checkAndCreateButton() { const sidePanel = document.querySelector('#side-panel'); if (sidePanel && !document.getElementById('ytmusic-side-panel-top-button')) { setTimeout(createScrollToTopButton, 500); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { applyStyles(); checkAndCreateButton(); }); } else { applyStyles(); checkAndCreateButton(); } const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function (...args) { originalPushState.call(this, ...args); setTimeout(() => { applyStyles(); checkAndCreateButton(); }, 100); }; history.replaceState = function (...args) { originalReplaceState.call(this, ...args); setTimeout(() => { applyStyles(); checkAndCreateButton(); }, 100); }; window.addEventListener('popstate', () => { setTimeout(() => { applyStyles(); checkAndCreateButton(); }, 100); }); const observer = new MutationObserver(() => { checkAndCreateButton(); }); const observeDocumentBodySafely = () => { if (document.body) { try { observer.observe(document.body, { childList: true, subtree: true, }); } catch (observeError) { console.error('[YouTube+][Music] Failed to observe document.body:', observeError); } } else { document.addEventListener( 'DOMContentLoaded', () => { try { if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } } catch (observeError) { console.error( '[YouTube+][Music] Failed to observe document.body after DOMContentLoaded:', observeError ); } }, { once: true } ); } }; if (typeof window !== 'undefined') { window.YouTubeMusic = { observeDocumentBodySafely, version: '2.2', }; } observeDocumentBodySafely(); console.log('[YouTube+][Music]', 'Модуль загружен'); })();