// ==UserScript== // @name YouTube + // @name:en YouTube + // @namespace by // @version 2.0 // @author diorhc // @description Вкладки для информации, комментариев, видео, плейлиста и скачивание видео и другие функции ↴ // @description:en Tabview YouTube and Download and others features ↴ // @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 // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect api.livecounts.io // @connect livecounts.io // @run-at document-start // @homepageURL https://github.com/diorhc/YoutubePlus // @supportURL https://github.com/diorhc/YoutubePlus/issues // @downloadURL none // ==/UserScript== // --- MODULE: utils.js --- // Shared utilities for YouTube+ modules (function () { 'use strict'; /** * Logs an error message with module context * @param {string} module - The module name where the error occurred * @param {string} message - Description of the error * @param {Error|*} error - The error object or value */ const logError = (module, message, error) => { try { console.error(`[YouTube+][${module}] ${message}:`, error); } catch { } }; /** * Creates a debounced function that delays invoking func until after wait milliseconds * @template {Function} T * @param {T} fn - The function to debounce * @param {number} ms - The number of milliseconds to delay * @param {{leading?: boolean}} [options={}] - Options object * @returns {T & {cancel: () => void}} The debounced function with a cancel method */ const debounce = (fn, ms, options = {}) => { let timeout = null; let lastArgs = null; let lastThis = null; /** @this {any} */ const debounced = function (...args) { lastArgs = args; lastThis = this; clearTimeout(timeout); if (options.leading && !timeout) { /** @type {Function} */ (fn).apply(this, args); } timeout = setTimeout(() => { if (!options.leading) /** @type {Function} */ (fn).apply(lastThis, lastArgs); timeout = null; lastArgs = null; lastThis = null; }, ms); }; debounced.cancel = () => { clearTimeout(timeout); timeout = null; lastArgs = null; lastThis = null; }; return /** @type {any} */ (debounced); }; /** * Creates a throttled function that only invokes func at most once per limit milliseconds * @template {Function} T * @param {T} fn - The function to throttle * @param {number} limit - The number of milliseconds to throttle invocations to * @returns {T} The throttled function */ const throttle = (fn, limit) => { let inThrottle = false; let lastResult; /** @this {any} */ const throttled = function (...args) { if (!inThrottle) { lastResult = /** @type {Function} */ (fn).apply(this, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } return lastResult; }; return /** @type {any} */ (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); } }, // expose for debug observers, listeners, intervals, timeouts, animationFrames, }; })(); 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 waitForElement = (selector, timeout = 5000, parent = document.body) => new Promise((resolve, reject) => { if (!selector || typeof selector !== 'string') return reject(new Error('Invalid selector')); try { const el = parent.querySelector(selector); if (el) return resolve(el); } catch (e) { return reject(e); } const obs = new MutationObserver(() => { const el = parent.querySelector(selector); if (el) { try { obs.disconnect(); } catch { } resolve(el); } }); obs.observe(parent, { childList: true, subtree: true }); const id = setTimeout(() => { try { obs.disconnect(); } catch { } reject(new Error('timeout')); }, timeout); cleanupManager.registerTimeout(id); }); // Minimal storage wrapper const storage = { get(key, def = null) { try { const v = localStorage.getItem(key); return v === null ? def : JSON.parse(v); } catch { return def; } }, set(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); return true; } catch { return false; } }, remove(key) { try { localStorage.removeItem(key); } catch { } }, }; // Expose a global YouTubeUtils if not present (non-destructive) if (typeof window !== 'undefined') { /** @type {any} */ (window).YouTubeUtils = /** @type {any} */ (window).YouTubeUtils || {}; const U = /** @type {any} */ (window).YouTubeUtils; U.logError = U.logError || logError; U.debounce = U.debounce || debounce; U.throttle = U.throttle || throttle; 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; } })(); // --- MODULE: error-boundary.js --- // Global error boundary for YouTube+ userscript (function () { 'use strict'; /** * Error boundary configuration */ const ErrorBoundaryConfig = { maxErrors: 10, errorWindow: 60000, // 1 minute enableLogging: true, enableRecovery: true, storageKey: 'youtube_plus_errors', }; /** * Error tracking state */ const errorState = { errors: [], errorCount: 0, lastErrorTime: 0, isRecovering: false, }; /** * Error severity levels */ const ErrorSeverity = { LOW: 'low', MEDIUM: 'medium', HIGH: 'high', CRITICAL: 'critical', }; /** * Categorize error severity * @param {Error} error - The error object * @returns {string} Severity level */ 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; }; /** * Log error with context * @param {Error} error - The error object * @param {Object} context - Additional context information */ const logError = (error, context = {}) => { if (!ErrorBoundaryConfig.enableLogging) return; const fallbackMessage = error.message?.trim() || '(no message)'; const errorInfo = { timestamp: new Date().toISOString(), message: fallbackMessage, stack: error.stack, severity: categorizeSeverity(error), context: { url: window.location.href, userAgent: navigator.userAgent, ...context, }, }; console.error(`[YouTube+ Error Boundary] ${errorInfo.message}`, errorInfo); // Store error for analysis errorState.errors.push(errorInfo); if (errorState.errors.length > 50) { errorState.errors.shift(); // Keep only last 50 errors } // Persist to localStorage for debugging 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 { } }; /** * Check if error rate is too high * @returns {boolean} True if error rate exceeded */ const isErrorRateExceeded = () => { const now = Date.now(); const windowStart = now - ErrorBoundaryConfig.errorWindow; // Count errors in the time window const recentErrors = errorState.errors.filter( e => new Date(e.timestamp).getTime() > windowStart ); return recentErrors.length >= ErrorBoundaryConfig.maxErrors; }; /** * Attempt to recover from error * @param {Error} error - The error that occurred * @param {Object} context - Error context */ 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.'); return; } errorState.isRecovering = true; try { // Attempt recovery based on error type if (context.module && window.YouTubeUtils?.cleanupManager) { console.log(`[YouTube+] Attempting recovery for module: ${context.module}`); // Could implement module-specific recovery here } setTimeout(() => { errorState.isRecovering = false; }, 5000); } catch (recoveryError) { console.error('[YouTube+] Recovery attempt failed:', recoveryError); errorState.isRecovering = false; } }; /** * Global error handler * @param {ErrorEvent} event - The error event */ 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) { // Ignore opaque cross-origin errors we can't introspect return false; } // Track error errorState.errorCount++; errorState.lastErrorTime = Date.now(); // Log error logError(error, { type: 'uncaught', filename: event.filename, lineno: event.lineno, colno: event.colno, }); // Check error rate if (isErrorRateExceeded()) { console.error( '[YouTube+] Error rate exceeded! Too many errors in short period. Some features may be disabled.' ); return; } // Attempt recovery attemptRecovery(error, { type: 'uncaught' }); // Don't prevent default error handling return false; }; /** * Unhandled promise rejection handler * @param {PromiseRejectionEvent} event - The rejection event */ const handleUnhandledRejection = event => { const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason)); logError(error, { type: 'unhandledRejection', promise: event.promise, }); // Check error rate if (isErrorRateExceeded()) { console.error('[YouTube+] Promise rejection rate exceeded!'); return; } // Attempt recovery attemptRecovery(error, { type: 'unhandledRejection' }); }; /** * Safe function wrapper with error boundary * @param {Function} fn - Function to wrap * @param {string} context - Context identifier * @returns {Function} Wrapped function */ const withErrorBoundary = (fn, context = 'unknown') => { /** @this {any} */ return function (...args) { try { const fnAny = /** @type {any} */ (fn); return /** @this {any} */ fnAny.apply(this, args); } catch (error) { logError(error, { module: context, args }); attemptRecovery(error, { module: context }); return null; } }; }; /** * Safe async function wrapper with error boundary * @param {Function} fn - Async function to wrap * @param {string} context - Context identifier * @returns {Function} Wrapped async function */ const withAsyncErrorBoundary = (fn, context = 'unknown') => { /** @this {any} */ return async function (...args) { try { const fnAny = /** @type {any} */ (fn); return /** @this {any} */ await fnAny.apply(this, args); } catch (error) { logError(error, { module: context, args }); attemptRecovery(error, { module: context }); return null; } }; }; /** * Get error statistics * @returns {Object} Error statistics */ 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; }, {}), }; }; /** * Clear stored errors */ const clearErrors = () => { errorState.errors = []; try { localStorage.removeItem(ErrorBoundaryConfig.storageKey); } catch { } }; // Install global error handlers if (typeof window !== 'undefined') { window.addEventListener('error', handleError, true); window.addEventListener('unhandledrejection', handleUnhandledRejection, true); // Expose error boundary utilities window.YouTubeErrorBoundary = { withErrorBoundary, withAsyncErrorBoundary, getErrorStats, clearErrors, logError, }; console.log('[YouTube+] Error boundary initialized'); } })(); // --- MODULE: performance.js --- // Performance monitoring for YouTube+ userscript (function () { 'use strict'; /** * Performance monitoring configuration */ const PerformanceConfig = { enabled: true, sampleRate: 1.0, // 100% sampling storageKey: 'youtube_plus_performance', metricsRetention: 100, // Keep last 100 metrics enableConsoleOutput: false, }; /** * Performance metrics storage */ const metrics = { timings: new Map(), marks: new Map(), measures: [], resources: [], }; /** * Create a performance mark * @param {string} name - Mark name */ const mark = name => { if (!PerformanceConfig.enabled) return; try { if (typeof performance !== 'undefined' && performance.mark) { performance.mark(name); } metrics.marks.set(name, Date.now()); } catch (e) { console.warn('[YouTube+ Perf] Failed to create mark:', e); } }; /** * Measure time between two marks * @param {string} name - Measure name * @param {string} startMark - Start mark name * @param {string} endMark - End mark name (optional, defaults to now) * @returns {number} Duration in milliseconds */ const measure = (name, startMark, endMark) => { if (!PerformanceConfig.enabled) return 0; try { const startTime = metrics.marks.get(startMark); if (!startTime) { console.warn(`[YouTube+ Perf] Start mark "${startMark}" not found`); return 0; } const endTime = endMark ? metrics.marks.get(endMark) : Date.now(); const duration = endTime - startTime; const measureData = { name, startMark, endMark: endMark || 'now', duration, timestamp: Date.now(), }; metrics.measures.push(measureData); // Keep only recent measures if (metrics.measures.length > PerformanceConfig.metricsRetention) { metrics.measures.shift(); } if (PerformanceConfig.enableConsoleOutput) { console.log(`[YouTube+ Perf] ${name}: ${duration.toFixed(2)}ms`); } // Try native performance API if (typeof performance !== 'undefined' && performance.measure) { try { performance.measure(name, startMark, endMark); } catch { } } return duration; } catch (e) { console.warn('[YouTube+ Perf] Failed to measure:', e); return 0; } }; /** * Time a function execution * @param {string} name - Timer name * @param {Function} fn - Function to time * @returns {Function} Wrapped function */ const timeFunction = (name, fn) => { if (!PerformanceConfig.enabled) return fn; return /** @this {any} */ function (...args) { const startMark = `${name}-start-${Date.now()}`; mark(startMark); try { const fnAny = /** @type {any} */ (fn); const result = fnAny.apply(this, args); // Handle promises 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; } }; }; /** * Time an async function execution * @param {string} name - Timer name * @param {Function} fn - Async function to time * @returns {Function} Wrapped async function */ const timeAsyncFunction = (name, fn) => { if (!PerformanceConfig.enabled) return fn; return /** @this {any} */ async function (...args) { const startMark = `${name}-start-${Date.now()}`; mark(startMark); try { const fnAny = /** @type {any} */ (fn); const result = await fnAny.apply(this, args); measure(name, startMark, undefined); return result; } catch (error) { measure(name, startMark, undefined); throw error; } }; }; /** * Record custom metric * @param {string} name - Metric name * @param {number} value - Metric value * @param {Object} metadata - Additional metadata */ 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+ Perf] ${name}: ${value}`, metadata); } }; /** * Get performance statistics * @param {string} metricName - Optional metric name filter * @returns {Object} Performance statistics */ 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], }; } // Get all stats 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), }; }; /** * Export metrics to JSON * @returns {string} JSON string of metrics */ const exportMetrics = () => { const data = { timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href, stats: getStats(undefined), measures: metrics.measures, customMetrics: Object.fromEntries(metrics.timings), }; return JSON.stringify(data, null, 2); }; /** * Clear all performance metrics */ 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 { } } }; /** * Monitor DOM mutations performance * @param {Element} element - Element to monitor * @param {string} name - Monitor name * @returns {MutationObserver} The observer instance */ 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; }; /** * Get browser performance entries * @param {string} type - Entry type filter * @returns {Array} Performance entries */ const getPerformanceEntries = type => { if (typeof performance === 'undefined' || !performance.getEntriesByType) { return []; } try { return performance.getEntriesByType(type); } catch { return []; } }; /** * Log page load performance */ 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+ Perf] Failed to log page metrics:', e); } }; // Auto-log page load metrics if (typeof window !== 'undefined') { if (document.readyState === 'complete') { logPageLoadMetrics(); } else { window.addEventListener('load', logPageLoadMetrics, { once: true }); } // Expose performance monitoring API window.YouTubePerformance = { mark, measure, timeFunction, timeAsyncFunction, recordMetric, getStats, exportMetrics, clearMetrics, monitorMutations, getPerformanceEntries, config: PerformanceConfig, }; console.log('[YouTube+] Performance monitoring initialized'); } })(); // --- MODULE: main.js --- /** * Identity function that returns the input value unchanged * @param {*} value - The value to return * @returns {*} The same value */ // @ts-nocheck const identityFn = value => value; /** * Ensure TrustedTypes policy exists for secure HTML handling * @returns {{createHTML: Function, error: Error|null}} Policy object with createHTML function and error status */ 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; // Validate policy works 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 }; } } /** * Create browser tick scheduler for microtask execution * @param {Function} existing - Existing scheduler to reuse if version compatible * @returns {Function} Scheduler function with version property */ 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'); } // Export createHTML for use in modules if needed 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; } // ----------------------------------------------------------------------------------------------------------------------------- /** * Main execution script for YouTube tab view * @param {string} _communicationKey - Unique key for cross-context communication (reserved for future use) */ const executionScript = _communicationKey => { /** @const {boolean} Debug flag for attachment/detachment events */ const DEBUG_5084 = false; /** @const {boolean} Debug flag for tab operations */ const DEBUG_5085 = false; /** @const {boolean} Auto-switch to comments tab when available */ const TAB_AUTO_SWITCH_TO_COMMENTS = false; // Configuration validation /** @const {number} Maximum value for attributes before overflow reset */ const MAX_ATTRIBUTE_VALUE = 1e9; /** @const {number} Reset value when attribute exceeds max */ const ATTRIBUTE_RESET_VALUE = 9; // Validate configuration 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' ); } // Reuse utility functions from parent scope const identityFn = value => value; const ensureTrustedTypesPolicyLocal = () => { 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?.createHTML?.bind?.(policy) ?? identityFn; // Validate policy works const testDiv = document.createElement('div'); testDiv.innerHTML = createHTML('1'); return { createHTML, error: null }; } catch (error) { console.error('[YouTube+] TrustedTypes local policy failed:', error); return { createHTML: identityFn, error }; } }; /** * Create browser tick scheduler for microtask execution * @param {Function} existing - Existing scheduler to reuse if version compatible * @returns {Function} Scheduler function */ 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, error: trustHTMLErr } = ensureTrustedTypesPolicyLocal(); if (trustHTMLErr) { console.error( '[YouTube+] TrustedHTML Error: Script cannot run due to CSP restrictions', trustHTMLErr ); return; // Exit execution script gracefully } const nextBrowserTick = createNextBrowserTickLocal( (typeof window !== 'undefined' && window.nextBrowserTick) || undefined ); if ( typeof window !== 'undefined' && (!window.nextBrowserTick || window.nextBrowserTick.version < 2) ) { window.nextBrowserTick = nextBrowserTick; } try { let executionFinished = 0; if (typeof CustomElementRegistry === 'undefined') return; if (CustomElementRegistry.prototype.define000) return; if (typeof CustomElementRegistry.prototype.define !== 'function') return; /** @type {HTMLElement} HTMLElement constructor reference */ const HTMLElement_ = HTMLElement.prototype.constructor; /** * Simple cache for frequently used querySelector results * Helps reduce DOM traversal overhead */ const selectorCache = new Map(); // eslint-disable-next-line no-unused-vars const _CACHE_MAX_SIZE = 50; // Reserved for future cache implementation const CACHE_TTL = 5000; // 5 seconds /** * Clear expired cache entries */ const clearExpiredCache = () => { const now = Date.now(); for (const [key, value] of selectorCache.entries()) { if (now - value.timestamp > CACHE_TTL) { selectorCache.delete(key); } } }; // Periodically clear expired cache setInterval(clearExpiredCache, CACHE_TTL); /** * Query single element from a specific parent * @param {Element} elm - Parent element to query from * @param {string} selector - CSS selector string * @returns {Element | null} Found element or null */ const qsOne = (elm, selector) => { return HTMLElement_.prototype.querySelector.call(elm, selector); }; /** * Query all matching elements from a specific parent * @param {Element} elm - Parent element to query from * @param {string} selector - CSS selector string * @returns {NodeListOf} NodeList of found elements */ // eslint-disable-next-line no-unused-vars const _qsAll = (elm, selector) => { return HTMLElement_.prototype.querySelectorAll.call(elm, selector); }; const pdsBaseDF = Object.getOwnPropertyDescriptors(DocumentFragment.prototype); Object.defineProperties(DocumentFragment.prototype, { replaceChildren000: pdsBaseDF.replaceChildren, }); const pdsBaseNode = Object.getOwnPropertyDescriptors(Node.prototype); Object.defineProperties(Node.prototype, { appendChild000: pdsBaseNode.appendChild, insertBefore000: pdsBaseNode.insertBefore, }); const pdsBaseElement = Object.getOwnPropertyDescriptors(Element.prototype); Object.defineProperties(Element.prototype, { setAttribute000: pdsBaseElement.setAttribute, getAttribute000: pdsBaseElement.getAttribute, hasAttribute000: pdsBaseElement.hasAttribute, removeAttribute000: pdsBaseElement.removeAttribute, querySelector000: pdsBaseElement.querySelector, replaceChildren000: pdsBaseElement.replaceChildren, }); /** * Set attribute only if value has changed (optimization to reduce DOM operations) * @param {string} p - Attribute name * @param {*} v - Attribute value */ Element.prototype.setAttribute111 = function (p, v) { if (!p || typeof p !== 'string') { console.warn('[YouTube+] setAttribute111: invalid attribute name', p); return; } try { v = `${v}`; if (this.getAttribute000(p) === v) return; this.setAttribute000(p, v); } catch (error) { console.warn('[YouTube+] setAttribute111 failed:', error, p, v); } }; /** * Increment attribute value (with overflow protection) * @param {string} p - Attribute name * @returns {number} New attribute value */ 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; } }; /** * Assign children elements in specific order while managing DOM efficiently * @param {Array|null} previousSiblings - Nodes to place before target node * @param {Node} node - Target node (required) * @param {Array|null} nextSiblings - Nodes to place after target node */ Element.prototype.assignChildren111 = function (previousSiblings, node, nextSiblings) { if (!node) { console.warn('[YouTube+] assignChildren111: node is required'); return; } try { // Collect all child nodes except the target node 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) { // Node is already a child, rearrange efficiently 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 { // Node is not a child yet, replace all children this.replaceChildren000(...(previousSiblings || []), node, ...(nextSiblings || [])); } inPageRearrange = false; // Cleanup disconnected nodes if (nodeList.length > 0) { for (const t of nodeList) { if (t instanceof Element && t.isConnected === false) { t.remove(); // Trigger removal events } } } nodeList.length = 0; nodeList = null; } catch (error) { inPageRearrange = false; console.error('[YouTube+] assignChildren111 failed:', error); } }; // ============================================================================================================================================================================================================================================================================== const DISABLE_FLAGS_SHADYDOM_FREE = true; /** * * Minified Code from https://greasyfork.org/en/scripts/475632-ytconfighacks/code (ytConfigHacks) * Date: 2024.04.17 * Minifier: https://www.toptal.com/developers/javascript-minifier * */ (() => { const e = 'undefined' != typeof unsafeWindow ? unsafeWindow : this instanceof Window ? this : window; if (!e._ytConfigHacks) { let t = 4; class n extends Set { add(e) { if (t <= 0) return console.warn('yt.config_ is already applied on the page.'); 'function' == typeof e && super.add(e); } } const a = (async () => { })().constructor, i = (e._ytConfigHacks = new n()); let l = () => { const t = e.ytcsi.originalYtcsi; t && ((e.ytcsi = t), (l = null)); }; let c = null; const o = () => { if (t >= 1) { const n = (e.yt || 0).config_ || (e.ytcfg || 0).data_ || 0; if ('string' == typeof n.INNERTUBE_API_KEY && 'object' == typeof n.EXPERIMENT_FLAGS) { for (const a of (--t <= 0 && l && l(), (c = !0), i)) a(n); } } }; let f = 1; const d = t => { if ((t = t || e.ytcsi)) { return ( (e.ytcsi = new Proxy(t, { get: (e, t) => ('originalYtcsi' === t ? e : (o(), c && --f <= 0 && l && l(), e[t])), })), !0 ); } }; d() || Object.defineProperty(e, 'ytcsi', { get() { }, set: t => (t && (delete e.ytcsi, d(t)), !0), enumerable: !1, configurable: !0, }); const { addEventListener: s, removeEventListener: y } = Document.prototype; function r(t) { (o(), t && e.removeEventListener('DOMContentLoaded', r, !1)); } (new a(e => { if ('undefined' != typeof AbortSignal) { (s.call(document, 'yt-page-data-fetched', e, { once: !0 }), s.call(document, 'yt-navigate-finish', e, { once: !0 }), s.call(document, 'spfdone', e, { once: !0 })); } else { const t = () => { (e(), y.call(document, 'yt-page-data-fetched', t, !1), y.call(document, 'yt-navigate-finish', t, !1), y.call(document, 'spfdone', t, !1)); }; (s.call(document, 'yt-page-data-fetched', t, !1), s.call(document, 'yt-navigate-finish', t, !1), s.call(document, 'spfdone', t, !1)); } }).then(o), new a(e => { if ('undefined' != typeof AbortSignal) { s.call(document, 'yt-action', e, { once: !0, capture: !0 }); } else { const t = () => { (e(), y.call(document, 'yt-action', t, !0)); }; s.call(document, 'yt-action', t, !0); } }).then(o), a.resolve().then(() => { 'loading' !== document.readyState ? r() : e.addEventListener('DOMContentLoaded', r, !1); })); } })(); 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.kevlar_watch_metadata_refresh_no_old_secondary_data = false; // flags.live_chat_overflow_hide_chat = false; flags.web_watch_chat_hide_button_killswitch = false; flags.web_watch_theater_chat = false; // for re-openable chat (ytd-watch-flexy's liveChatCollapsed is always undefined) flags.suppress_error_204_logging = true; flags.kevlar_watch_grid = false; // A/B testing for watch grid 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; } } } }); // =================================================================================================================================================================================================================================== /* globals WeakRef:false */ /** @type {(o: Object | null) => WeakRef | null} */ const mWeakRef = typeof WeakRef === 'function' ? o => (o ? new WeakRef(o) : null) : o => o || null; // typeof InvalidVar == 'undefined' /** @type {(wr: Object | null) => Object | null} */ const kRef = wr => (wr && wr.deref ? wr.deref() : wr); /** @type {globalThis.PromiseConstructor} */ /** @type {PromiseConstructor} Safe Promise constructor (YouTube hacks Promise in WaterFox Classic) */ const Promise = (async () => { })().constructor; /** * Create a promise that resolves after a delay * @param {number} delay - Delay in milliseconds * @returns {Promise} Promise that resolves after the delay */ const delayPn = delay => new Promise(fn => setTimeout(fn, delay)); /** * Get polymer controller or instance from element * @param {*} o - Element or object to inspect * @returns {*} Polymer controller, instance, or the object itself */ const insp = o => (o ? o.polymerController || o.inst || o || 0 : o || 0); /** @type {Function} Bound setTimeout to ensure correct context */ const setTimeout_ = setTimeout.bind(window); /** * Error handler for promises - logs errors with context * @param {Error} error - The error that occurred * @param {string} context - Context information about where the error occurred */ const handlePromiseError = (error, context = 'Unknown') => { if (error) { console.error(`[YouTube+] Promise error in ${context}:`, error); } }; /** * Promise class with external resolve/reject methods * Useful for creating deferred promises that can be resolved externally */ const PromiseExternal = ((resolve_, reject_) => { const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject; }; return class PromiseExternal extends Promise { constructor(cb = h) { super(cb); if (cb === h) { /** @type {(value: any) => void} */ this.resolve = resolve_; /** @type {(reason?: any) => void} */ this.reject = reject_; } } }; })(); // ------------------------------------------------------------------------ Event Listener Options ------------------------------------------------------------------------ /** @const {boolean} Check if passive event listeners are supported */ const isPassiveArgSupport = typeof IntersectionObserver === 'function'; /** @const {Object|boolean} Event listener options for bubble phase with passive */ // eslint-disable-next-line no-unused-vars const _bubblePassive = isPassiveArgSupport ? { capture: false, passive: true } : false; /** @const {Object|boolean} Event listener options for capture phase with passive */ const capturePassive = isPassiveArgSupport ? { capture: true, passive: true } : true; /** * Helper class to manage binary flags as string attributes */ class Attributer { /** * @param {string} list - String where each character represents a flag */ constructor(list) { this.list = list; this.flag = 0; } /** * Convert active flags to string representation * @returns {string} String with characters for active flags */ 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; } } /** @type {Attributer} Module loaded state tracker */ const mLoaded = new Attributer('icp'); /** @type {WeakMap} WeakMap for self-referencing objects */ const wrSelfMap = new WeakMap(); /** * Elements cache using Proxy with WeakRef for memory efficiency * Automatically manages element references and prevents memory leaks * @type {Object.} */ 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; }, } ); /** * Get the main info element from the infoExpander * @returns {Element|null} The main info element or null */ const getMainInfo = () => { const infoExpander = elements.infoExpander; if (!infoExpander) return null; const mainInfo = infoExpander.matches('[tyt-main-info]') ? infoExpander : infoExpander.querySelector000('[tyt-main-info]'); return mainInfo || null; }; /** * Wrap async function to execute in next microtask * @param {Function} asyncFn - Async function to wrap * @returns {Function} Wrapped function */ // eslint-disable-next-line no-unused-vars const _asyncWrap = asyncFn => { return () => { Promise.resolve().then(asyncFn); }; }; let pageType = null; let pageLang = 'en'; /** * Localized strings for different languages * @type {Object.>} */ 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: 'Плейлист', }, }; const svgComments = ``.trim(); const svgVideos = ``.trim(); const svgInfo = ``.trim(); const svgPlayList = ``.trim(); // eslint-disable-next-line no-unused-vars const svgDiag1 = ``; // eslint-disable-next-line no-unused-vars const svgDiag2 = ``; /** * Get GMT offset for the current timezone * @returns {string} GMT offset string (e.g., "+9" or "-5") */ // eslint-disable-next-line no-unused-vars const getGMT = () => { const m = new Date('2023-01-01T00:00:00Z'); return m.getDate() === 1 ? `+${m.getHours()}` : `-${24 - m.getHours()}`; }; /** * Get localized word based on current page language * @param {string} tag - Word identifier * @returns {string} Localized word or empty string */ function getWord(tag) { return langWords[pageLang]?.[tag] || langWords['en']?.[tag] || ''; } /** * Create SVG element string * @param {number} w - Width * @param {number} h - Height * @param {number} vw - ViewBox width * @param {number} vh - ViewBox height * @param {string} p - Path data * @param {string} m - Optional class name * @returns {string} SVG element string */ const svgElm = (w, h, vw, vh, p, m) => `${p}`; const hiddenTabsByUserCSS = 0; /** * Generate HTML for tab buttons * @returns {string} HTML string for tabs */ 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 || ''; // Language mapping with optimized lookup 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'; } /** @type {Object.} */ 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; }, }); // note: xxxxxxxxxAsyncLock is not expected for calling multiple time in a short period. // it is just to split the process into microTasks. const videosElementProvidedPromise = new PromiseExternal(); const navigateFinishedPromise = new PromiseExternal(); let isRightTabsInserted = false; const rightTabsProvidedPromise = new PromiseExternal(); const infoExpanderElementProvidedPromise = new PromiseExternal(); const cmAttr = document.createComment('1'); const cmAttrStack = []; /** * Add function to attribute change stack * @param {Function} f - Function to execute on attribute change */ // eslint-disable-next-line no-unused-vars const cmAttrStackPush = f => { cmAttrStack.push(f); cmAttr.data = `${(cmAttr.data & 7) + 1}`; }; const cmAttrObs = new MutationObserver(() => { cmAttrStack.forEach(fn => fn()); }); cmAttrObs.observe(cmAttr, { characterData: true }); /** * Function to calculate if element can collapse * @param {*} _s - Parameter (unused but kept for compatibility) */ const funcCanCollapse = function (_s) { const content = this.content || this.$.content; this.canToggle = this.shouldUseNumberOfLines && (this.alwaysCollapsed || this.collapsed || this.isToggled === false) ? this.alwaysToggleable || this.isToggled || (content && content.offsetHeight < content.scrollHeight) : this.alwaysToggleable || this.isToggled || (content && content.scrollHeight > this.collapsedHeight); }; const aoChatAttrChangeFn = async lockId => { if (lockGet['aoChatAttrAsyncLock'] !== lockId) return; const chatElm = elements.chat; const ytdFlexyElm = elements.flexy; if (chatElm && ytdFlexyElm) { const isChatCollapsed = chatElm.hasAttribute000('collapsed'); if (isChatCollapsed) { ytdFlexyElm.setAttribute111('tyt-chat-collapsed', ''); } else { ytdFlexyElm.removeAttribute000('tyt-chat-collapsed'); } ytdFlexyElm.setAttribute111('tyt-chat', isChatCollapsed ? '-' : '+'); } }; const aoPlayListAttrChangeFn = async lockId => { if (lockGet['aoPlayListAttrAsyncLock'] !== lockId) return; const playlistElm = elements.playlist; const ytdFlexyElm = elements.flexy; if (playlistElm && ytdFlexyElm) { if (playlistElm.hasAttribute000('collapsed')) { ytdFlexyElm.removeAttribute000('tyt-playlist-expanded'); } else { ytdFlexyElm.setAttribute111('tyt-playlist-expanded', ''); } } else if (ytdFlexyElm) { ytdFlexyElm.removeAttribute000('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; //tyt-comments-video-id //tyt-comments-data-status // hidden 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 = +commentsArea.getAttribute000('tyt-comments-data-status'); if (commentsDataStatus === 2) { ytdFlexyElm.setAttribute111('tyt-comment-disabled', ''); } else if (commentsDataStatus === 1) { ytdFlexyElm.removeAttribute000('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; // console.log(7932, 'comments'); 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.target; const cnt = insp(target); if ( entry.isIntersecting && target instanceof HTMLElement_ && typeof cnt.calculateCanCollapse === 'function' ) { lockSet['removeKeepCommentsScrollerLock']; cnt.calculateCanCollapse(true); target.setAttribute111('io-intersected', ''); const ytdFlexyElm = elements.flexy; if (ytdFlexyElm && !ytdFlexyElm.hasAttribute000('keep-comments-scroller')) { ytdFlexyElm.setAttribute111('keep-comments-scroller', ''); } } else if (target.hasAttribute000('io-intersected')) { target.removeAttribute000('io-intersected'); } } }, { threshold: [0], rootMargin: '32px', // enlarging viewport for getting intersection earlier } ); 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; } } }); /** * Switch to specified tab * @param {string|Element} activeLink - Tab link selector or element */ const switchToTab = activeLink => { if (typeof activeLink === 'string') { activeLink = document.querySelector(`a[tyt-tab-content="${activeLink}"]`) || null; } const ytdFlexyElm = elements.flexy; const links = document.querySelectorAll('#material-tabs a[tyt-tab-content]'); for (const link of links) { const content = document.querySelector(link.getAttribute000('tyt-tab-content')); if (!link || !content) continue; const isActive = link === activeLink; link.classList.toggle('active', isActive); content.classList.toggle('tab-content-hidden', !isActive); if (isActive) { content.removeAttribute000('tyt-hidden'); } else if (!content.hasAttribute000('tyt-hidden')) { content.setAttribute111('tyt-hidden', ''); } } const switchingTo = activeLink ? activeLink.getAttribute000('tyt-tab-content') : ''; if (switchingTo) { lastTab = lastPanel = switchingTo; } if (ytdFlexyElm?.getAttribute000('tyt-chat') === '') { ytdFlexyElm.removeAttribute000('tyt-chat'); } ytdFlexyElm?.setAttribute111('tyt-tab', switchingTo); if (switchingTo) { bFixForResizedTabLater = false; Promise.resolve(0).then(eventMap['fixForTabDisplay']); } }; let tabAStatus = 0; /** * Calculate status flags based on element attributes * @param {number} r - Initial result value * @param {number} flag - Flags to check (bitwise) * @returns {number} Calculated status flags */ const calculationFn = (r = 0, flag) => { const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) return r; if (flag & 1) { r |= 1; if (!ytdFlexyElm.hasAttribute000('theater')) r -= 1; } if (flag & 2) { r |= 2; if (!ytdFlexyElm.getAttribute000('tyt-tab')) r -= 2; } if (flag & 4) { r |= 4; if (ytdFlexyElm.getAttribute000('tyt-chat') !== '-') r -= 4; } if (flag & 8) { r |= 8; if (ytdFlexyElm.getAttribute000('tyt-chat') !== '+') r -= 8; } if (flag & 16) { r |= 16; if (!ytdFlexyElm.hasAttribute000('is-two-columns_')) r -= 16; } if (flag & 32) { r |= 32; if (!ytdFlexyElm.hasAttribute000('tyt-egm-panel_')) r -= 32; } if (flag & 64) { r |= 64; if (!document.fullscreenElement) r -= 64; } if (flag & 128) { r |= 128; if (!ytdFlexyElm.hasAttribute000('tyt-playlist-expanded')) r -= 128; } return r; }; /** * Check if theater mode is active * @returns {boolean} True if theater mode is active */ function isTheater() { return Boolean(elements.flexy?.hasAttribute000('theater')); } /** * Get theater mode toggle button * @returns {HTMLButtonElement|null} Theater button or null */ function getTheaterButton() { return document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button'); } /** * Enable theater mode * @internal Reserved for future use */ // eslint-disable-next-line no-unused-vars function ytBtnSetTheater() { if (!isTheater()) { getTheaterButton()?.click(); } } /** * Disable theater mode */ function ytBtnCancelTheater() { if (isTheater()) { getTheaterButton()?.click(); } } /** * Get element with most nested children (best match) * @param {string} selector - CSS selector * @returns {Element|null} Element with most children or null */ function getSuitableElement(selector) { const elements = document.querySelectorAll(selector); let bestIndex = -1; let maxDepth = -1; for (let i = 0; i < elements.length; i++) { const depth = elements[i].getElementsByTagName('*').length; if (depth > maxDepth) { maxDepth = depth; bestIndex = i; } } return bestIndex >= 0 ? elements[bestIndex] : null; } /** * Expand YouTube live chat */ function ytBtnExpandChat() { const dom = getSuitableElement('ytd-live-chat-frame#chat'); const cnt = insp(dom); if (cnt && typeof cnt.collapsed === 'boolean') { if (typeof cnt.setCollapsedState === 'function') { cnt.setCollapsedState({ setLiveChatCollapsedStateAction: { collapsed: false, }, }); if (cnt.collapsed === false) return; } cnt.collapsed = false; if (cnt.collapsed === false) return; } 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(); } } /** * Collapse YouTube live chat */ /** * Collapse YouTube live chat panel */ function ytBtnCollapseChat() { const dom = getSuitableElement('ytd-live-chat-frame#chat'); const cnt = insp(dom); if (cnt && typeof cnt.collapsed === 'boolean') { if (typeof cnt.setCollapsedState === 'function') { cnt.setCollapsedState({ setLiveChatCollapsedStateAction: { collapsed: true, }, }); if (cnt.collapsed === true) return; } cnt.collapsed = true; if (cnt.collapsed === true) return; } 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(); } } /** * Control YouTube engagement panels (show/hide) * @param {Array|Object} arr - Array of panel actions or single action object */ function ytBtnEgmPanelCore(arr) { if (!arr) return; if (!('length' in arr)) arr = [arr]; const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) return; const actions = []; for (const entry of arr) { if (!entry) continue; const { panelId, toHide, toShow } = entry; if (toHide === true && !toShow) { actions.push({ changeEngagementPanelVisibilityAction: { targetId: panelId, visibility: 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN', }, }); } else if (toShow === true && !toHide) { actions.push({ showEngagementPanelEndpoint: { panelIdentifier: panelId, }, }); } } if (actions.length > 0) { const cnt = insp(ytdFlexyElm); cnt.resolveCommand( { signalServiceEndpoint: { signal: 'CLIENT_SIGNAL', actions: actions, }, }, {}, false ); } } /* function ytBtnCloseEngagementPanel( s) { //ePanel.setAttribute('visibility',"ENGAGEMENT_PANEL_VISIBILITY_HIDDEN"); let panelId = s.getAttribute('target-id') scriptletDeferred.debounce(() => { document.dispatchEvent(new CustomEvent('tyt-engagement-panel-visibility-change', { detail: { panelId, toHide: true } })) }) } /** * Close all expanded YouTube engagement panels */ function ytBtnCloseEngagementPanels() { const actions = []; for (const panelElm of document.querySelectorAll( `ytd-watch-flexy[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); } /** * Open YouTube playlist panel */ function ytBtnOpenPlaylist() { const cnt = insp(elements.playlist); if (cnt && typeof cnt.collapsed === 'boolean') { cnt.collapsed = false; } } /** * Close YouTube playlist panel */ function ytBtnClosePlaylist() { const cnt = insp(elements.playlist); if (cnt && typeof cnt.collapsed === 'boolean') { cnt.collapsed = true; } } const updateChatLocation498 = function () { /* updateChatLocation: function() { if (this.is !== "ytd-watch-grid" && y("web_watch_theater_chat")) { var a = T(this.hostElement).querySelector("#chat-container") , b = this.theater && (!this.fullscreen || y("web_watch_fullscreen_panels")); this.watchWhileWindowSizeSufficient && this.liveChatPresentAndExpanded && b ? y("web_watch_theater_chat_beside_player") ? (b = T(this.hostElement).querySelector("#panels-full-bleed-container"), (a == null ? void 0 : a.parentElement) !== b && b.append(a), this.panelsBesidePlayer = !0) : y("web_watch_theater_fixed_chat") && (b = T(this.hostElement).querySelector("#columns"), (a == null ? void 0 : a.parentElement) !== b && b.append(a), this.fixedPanels = !0) : (y("web_watch_theater_chat_beside_player") ? this.panelsBesidePlayer = !1 : y("web_watch_theater_fixed_chat") && (this.fixedPanels = !1), b = T(this.hostElement).querySelector("#playlist"), a && b ? Fh(a, b) : Gm(new zk("Missing element when updating chat location",{ "chatContainer defined": !!a, "playlist defined": !!b }))); this.updatePageMediaQueries(); this.schedulePlayerSizeUpdate_() } }, */ // console.log('updateChatLocation498') if (this.is !== 'ytd-watch-grid') { this.updatePageMediaQueries(); this.schedulePlayerSizeUpdate_(); } }; const mirrorNodeWS = new WeakMap(); /* const infoFix = () => { const infoExpander = elements.infoExpander; const ytdFlexyElm = elements.flexy; if (!infoExpander || !ytdFlexyElm) return; console.log(386, infoExpander, infoExpander.matches('#tab-info > [class]')) if (!infoExpander.matches('#tab-info > [class]')) return; // const elms = [...document.querySelectorAll('ytd-watch-metadata.ytd-watch-flexy div[slot="extra-content"], ytd-watch-metadata.ytd-watch-flexy ytd-metadata-row-container-renderer')].filter(elm=>{ // if(elm.parentNode.closest('div[slot="extra-content"], ytd-metadata-row-container-renderer')) return false; // return true; // }); 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(elm => { const is = elm.is; 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'); console.log(requireElements) const source = requireElements.map(entry=>({ data: insp(entry).data, tag: insp(entry).is, elm: entry })) if (!document.querySelector('noscript#aythl')) { const noscript = document.createElement('noscript') noscript.id = 'aythl'; ytdFlexyElm.insertBefore000(noscript, ytdFlexyElm.firstChild); } const noscript = document.querySelector('noscript#aythl'); const clones = new Set(); for (const {data, tag, elm} of source) { // const cloneNode = document.createElement(tag); let cloneNode = elm.cloneNode(true); // noscript.appendChild(cloneNode); // insp(cloneNode).data = null; insp(cloneNode).data = data; source.clone = cloneNode; clones.add(cloneNode); } // const elms = [...document.querySelectorAll('ytd-watch-metadata.ytd-watch-flexy div[slot="extra-content"]')].filter(elm => { // if (elm.parentNode.closest('div[slot="extra-content"], ytd-metadata-row-container-renderer')) return false; // return true; // }); // let arr = []; // for(const elm of elms){ // if(elm.hasAttribute('slot')) arr.push(...elm.childNodes); // else arr.push(elm); // } // arr = arr.filter(e=>e && e.nodeType === 1); // console.log(386,arr) // const clones = arr.map(e=>e.cloneNode(true)); // for(let node = infoExpander.nextSibling; node instanceof Node; node = node.nextSibling) node.remove(); // infoExpander.parentNode.assignChildren111(null, infoExpander, [...clones]); let removal = []; for(let node = infoExpander.nextSibling; node instanceof Node; node = node.nextSibling)removal.push(node); for(const node of removal) node.remove(); for(const node of clones) infoExpander.parentNode.appendChild(node); for (const {data, tag, elm, clone} of source) { insp(clone).data = null; insp(clone).data = data; } // console.log(infoExpander.parentNode.childNodes) } */ const dummyNode = document.createElement('noscript'); // const __j4838__ = Symbol(); const __j4836__ = Symbol(); const __j5744__ = Symbol(); // original element const __j5733__ = Symbol(); // __lastChanged__ 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(); // required for making sufficient delay for data rendering attributeInc(node, 'tyt-data-change-counter'); // next macro task } }; 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 = Object.assign({}, oriCnt.data); dummyNode.replaceWith(node); } } }; /* const moChangeReflection = async 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-data-change-counter' && mutation.target === originElement) { bfDataChangeCounter = true; } } if(bfDataChangeCounter && oriCnt.data){ node.replaceWith(dummyNode); cnt.data = Object.assign({}, oriCnt.data); dummyNode.replaceWith(node); } } // console.log(8348, originElement) if (cnt.isAttached === false) { // do nothing // don't call infoFix() as it shall be only called in ytd-expander::attached and yt-navigate-finish } else if (oriCnt.isAttached === false && cnt.isAttached === true) { if (node.isConnected && node.parentNode instanceof HTMLElement_) { node.parentNode.removeChild(node); } else { node.remove(); } if (oriCnt.data !== null) { cnt.data = null; } } else if (oriCnt.isAttached === true && cnt.isAttached === true) { if (!oriCnt.data) { if(cnt.data){ cnt.data = null; } } else if (!cnt.data || oriCnt.data[__j4838__] !== cnt.data[__j4838__]) { oriCnt.data[__j4838__] = Date.now(); await Promise.resolve(); // required for making sufficient delay for data rendering attributeInc(originElement, 'tyt-data-change-counter'); // next macro task } } }; */ /** * Increment attribute value with overflow protection * @param {Element} elm - Element to modify * @param {string} prop - Attribute name * @returns {number} New attribute value */ 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; }; /** * Validates if a string is a valid YouTube channel ID * Format: UC[-_a-zA-Z0-9+=.]{22} * @see https://support.google.com/youtube/answer/6070344?hl=en * @param {string} x - The string to validate * @returns {boolean} True if valid channel ID */ const isChannelId = x => { return typeof x === 'string' && x.length === 24 && /^UC[-_a-zA-Z0-9+=.]{22}$/.test(x); }; /** * Fix and organize info panel layout * @param {number|null} lockId - Lock identifier for concurrent execution control */ const infoFix = lockId => { if (lockId !== null && lockGet['infoFixLock'] !== lockId) return; const infoExpander = elements.infoExpander; 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(elm => { const is = elm.is; 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); new MutationObserver(moChangeReflection.bind(nodeWR)).observe(s, { attributes: true, attributeFilter: ['tyt-clone-refresh-count', 'tyt-data-change-counter'], }); s.jy8432 = 1; if ( !(cProto instanceof Node) && !cProto._dataChanged496 && typeof cProto._createPropertyObserver === 'function' ) { cProto._dataChanged496 = function () { const cnt = this; const node = cnt.hostElement || cnt; 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 () { const cnt = kRef(this.controller573 || null) || null; cnt && typeof cnt._dataChanged496k === 'function' && Promise.resolve(cnt) .then(cnt._dataChanged496k) .catch(err => handlePromiseError(err, 'setWithPath_dataChanged496k')); return this.setWithPath573(...arguments); }; cProto._dataChanged496 = function () { const cnt = this; const node = cnt.hostElement || cnt; if (node.jy8432) { attributeInc(node, 'tyt-data-change-counter'); } }; cProto._dataChanged496k = cnt => cnt._dataChanged496(); } } if (!cProto._dataChanged496) { new MutationObserver( monitorDataChangedByDOMMutation.bind(mirrorNode[__j5744__]) ).observe(s, { attributes: true, childList: true, subtree: true }); } mirrorNodeWS.set(s, nodeWR); requiredUpdate = true; } else { if (mirrorNode.parentNode !== targetParent) { requiredUpdate = true; } } if (!requiredUpdate) { const cloneNodeCnt = insp(mirrorNode); if (cloneNodeCnt.data !== data) { // if(mirrorNode.parentNode !== noscript){ // noscript.appendChild(mirrorNode); // } // mirrorNode.replaceWith(dummyNode); // cloneNodeCnt.data = data; // dummyNode.replaceWith(mirrorNode); requiredUpdate = true; } } mirrorElmSet.add(mirrorNode); source.mirrored = mirrorNode; } const mirroElmArr = [...mirrorElmSet]; mirrorElmSet.clear(); if (!requiredUpdate) { let e = infoExpander ? -1 : 0; // DOM Tree Check for (let n = targetParent.firstChild; n instanceof Node; n = n.nextSibling) { const target = e < 0 ? infoExpander : mirroElmArr[e]; e++; if (n !== target) { // target can be undefined if index overflow 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) { // trigger data assignment and record refresh count by manual update 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; }; /** * Fix and optimize secondary layout structure * @param {number} lockId - Lock identifier for concurrent execution control */ const layoutFix = lockId => { if (lockGet['layoutFixLock'] !== lockId) return; // console.log('((layoutFix))') const secondaryWrapper = document.querySelector( '#secondary-inner.style-scope.ytd-watch-flexy > secondary-wrapper' ); // console.log(3838, !!chatContainer, !!(secondaryWrapper && secondaryInner), secondaryInner?.firstChild, secondaryInner?.lastChild , secondaryWrapper?.parentNode === secondaryInner) 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')) ) { // console.log(38381) 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); } } // console.log('qww', w, w2) 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) ) { // setTimeout(() => chatCnt.urlChanged, 136); 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 = ''; // let fixInitialTabState = 0; const aoEgmPanels = new MutationObserver(() => { // console.log(5094,3); 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; // console.log('updateEgmPanels::called'); 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 findLcComment(lc) { if (arguments.length === 1) { const element = document.querySelector( `#tab-comments ytd-comments ytd-comment-renderer #header-author a[href*="lc=${lc}"]` ); if (element) { const commentRendererElm = closestFromAnchor.call(element, 'ytd-comment-renderer'); if (commentRendererElm && lc) { return { lc, commentRendererElm, }; } } } else if (arguments.length === 0) { const element = document.querySelector( `#tab-comments ytd-comments ytd-comment-renderer > #linked-comment-badge span:not(:empty)` ); if (element) { const commentRendererElm = closestFromAnchor.call(element, 'ytd-comment-renderer'); if (commentRendererElm) { const header = _querySelector.call(commentRendererElm, '#header-author'); if (header) { const anchor = _querySelector.call(header, 'a[href*="lc="]'); if (anchor) { const href = anchor.getAttribute('href') || ''; const m = /[&?]lc=([\w_.-]+)/.exec(href); // dot = sub-comment if (m) { lc = m[1]; } } } } if (commentRendererElm && lc) { return { lc, commentRendererElm, }; } } } return null; } function lcSwapFuncA(targetLcId, currentLcId) { let done = 0; try { // console.log(currentLcId, targetLcId) const r1 = findLcComment(currentLcId).commentRendererElm; const r2 = findLcComment(targetLcId).commentRendererElm; if ( typeof insp(r1).data.linkedCommentBadge === 'object' && typeof insp(r2).data.linkedCommentBadge === 'undefined' ) { const p = Object.assign({}, insp(r1).data.linkedCommentBadge); if (((p || 0).metadataBadgeRenderer || 0).trackingParams) { delete p.metadataBadgeRenderer.trackingParams; } const v1 = findContentsRenderer(r1); const v2 = findContentsRenderer(r2); if ( v1.parent === v2.parent && (v2.parent.nodeName === 'YTD-COMMENTS' || v2.parent.nodeName === 'YTD-ITEM-SECTION-RENDERER') ) { } else { // currently not supported return false; } if (v2.index >= 0) { if (v2.parent.nodeName === 'YTD-COMMENT-REPLIES-RENDERER') { if (lcSwapFuncB(targetLcId, currentLcId, p)) { done = 1; } done = 1; } else { const v2pCnt = insp(v2.parent); const v2Conents = (v2pCnt.data || 0).contents || 0; if (!v2Conents) console.warn('v2Conents is not found'); v2pCnt.data = Object.assign({}, v2pCnt.data, { contents: [].concat( [v2Conents[v2.index]], v2Conents.slice(0, v2.index), v2Conents.slice(v2.index + 1) ), }); if (lcSwapFuncB(targetLcId, currentLcId, p)) { done = 1; } } } } } catch (e) { console.warn(e); } return done === 1; } 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 = Object.assign({}, _p); r1d.linkedCommentBadge = null; delete r1d.linkedCommentBadge; const q = Object.assign({}, r1d); q.linkedCommentBadge = null; delete q.linkedCommentBadge; r1cnt.data = Object.assign({}, q); r2cnt.data = Object.assign({}, 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); // document.querySelector('#movie_player video[src]'); const media2 = common.getMediaElements(2); // document.querySelectorAll('ytd-browse[role="main"] video[src]'); 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(s => s.paused === false && s.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.videoId; const url = endpoint.commandMetadata.webCommandMetadata.url; if (typeof videoId === 'string' && typeof url === 'string' && url.indexOf('lc=') > 0) { const m = /^\/watch\?v=([\w_-]+)&lc=([\w_.-]+)$/.exec(url); // dot = sub-comment if (m && m[1] === videoId) { /* { "style": "BADGE_STYLE_TYPE_SIMPLE", "label": "注目のコメント", "trackingParams": "XXXXXX" } */ const targetLc = findLcComment(m[2]); const currentLc = targetLc ? findLcComment() : null; if (targetLc && currentLc) { const done = targetLc.lc === currentLc.lc ? 1 : lcSwapFuncA(targetLc.lc, currentLc.lc) ? 1 : 0; if (done === 1) { common.xReplaceState(history.state, url); return; } } } } } /* { "type": 0, "command": endpoint, "form": { "tempData": {}, "reload": false } } */ 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') { // valid = false; const playerMedia = common.getMediaElement(1); if (playerMedia && playerMedia.paused === false) valid = true; // home page } else if (endpoint.commandMetadata && endpoint.commandMetadata.webCommandMetadata) { const meta = endpoint.commandMetadata.webCommandMetadata; if (meta && /*meta.apiUrl &&*/ 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; // return true; // return !!document.querySelector('ytd-page-manager#page-manager > ytd-browse[page-subtype]'); }; const conditionFulfillment = req => { const endpoint = req ? req.command : null; if (!endpoint) return; if ( endpoint && (endpoint.commandMetadata || 0).webCommandMetadata && endpoint.watchEndpoint ) { } else if ( endpoint && (endpoint.commandMetadata || 0).webCommandMetadata && endpoint.browseEndpoint && isChannelId(endpoint.browseEndpoint.browseId) ) { } else if ( endpoint && (endpoint.browseEndpoint || endpoint.searchEndpoint) && !endpoint.urlEndpoint && !endpoint.watchEndpoint ) { } else { return false; } if (!shouldUseMiniPlayer()) return false; /* // user would like to switch page immediately without playing the video; // attribute appear after playing video for more than 2s if (!document.head.dataset.viTime) return false; else { let currentVideo = common.getMediaElement(0); if (currentVideo && currentVideo.readyState > currentVideo.HAVE_CURRENT_DATA && currentVideo.currentTime > 2.2 && currentVideo.duration - 2.2 < currentVideo.currentTime) { // disable miniview browsing if the media is near to the end return false; } } */ if (pageType !== 'watch') return false; if ( !checkElementExist( 'ytd-watch-flexy #player button.ytp-miniplayer-button.ytp-button', '[hidden]' ) ) { 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) okay = true; else { 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 = false; } } 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 handleNavigateFactory = handleNavigate => { return function (req) { if (u38 > MAX_ATTRIBUTE_VALUE) u38 = ATTRIBUTE_RESET_VALUE; const t38 = ++u38; const $this = this; const $arguments = arguments; let endpoint = null; if (conditionFulfillment(req)) { endpoint = getBrowsableEndPoint(req); } if (!endpoint || !shouldUseMiniPlayer()) return handleNavigate.apply($this, $arguments); // console.log('tabview-script-handleNavigate') const ytdAppElm = document.querySelector('ytd-app'); const ytdAppCnt = insp(ytdAppElm); let object = null; try { object = ytdAppCnt.data.response.currentVideoEndpoint.watchEndpoint || null; } catch { object = null; } if (typeof object !== 'object') object = null; const once = { once: true }; // browsers supporting async function can also use once option. if (object !== null && !('playlistId' in object)) { let wObject = mWeakRef(object); const N = 3; let count = 0; /* rcb(b) => a = playlistId = undefinded var scb = function(a, b, c, d) { a.isInitialized() && (B("kevlar_miniplayer_navigate_to_shorts_killswitch") ? c || d ? ("watch" !== Xu(b) && "shorts" !== Xu(b) && os(a.miniplayerEl, "yt-cache-miniplayer-page-action", [b]), qs(a.miniplayerEl, "yt-deactivate-miniplayer-action")) : "watch" === Xu(b) && rcb(b) && (qt.getInstance().playlistWatchPageActivation = !0, a.activateMiniplayer(b)) : c ? ("watch" !== Xu(b) && os(a.miniplayerEl, "yt-cache-miniplayer-page-action", [b]), qs(a.miniplayerEl, "yt-deactivate-miniplayer-action")) : d ? qs(a.miniplayerEl, "yt-pause-miniplayer-action") : "watch" === Xu(b) && rcb(b) && (qt.getInstance().playlistWatchPageActivation = !0, a.activateMiniplayer(b))) }; */ Object.defineProperty(kRef(wObject) || {}, 'playlistId', { get() { count++; if (count === N) { delete this.playlistId; } return '*'; }, set(value) { delete this.playlistId; // remove property definition this.playlistId = value; // assign as normal property }, 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 object = kRef(wObject); wObject = null; return object ? object.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.apply($this, $arguments); }; }; return { handleNavigateFactory }; })(); const common = (() => { let mediaModeLock = 0; const _getMediaElement = i => { if (mediaModeLock === 0) { const e = document.querySelector('.video-stream.html5-main-video') || document.querySelector('#movie_player video, #movie_player audio') || document.querySelector('body video[src], body audio[src]'); if (e) { if (e.nodeName === 'VIDEO') mediaModeLock = 1; else if (e.nodeName === 'AUDIO') mediaModeLock = 2; } } if (!mediaModeLock) return null; if (mediaModeLock === 1) { switch (i) { case 1: return 'ytd-player#ytd-player video[src]'; case 2: return 'ytd-browse[role="main"] video[src]'; case 0: default: return '#movie_player video[src]'; } } else if (mediaModeLock === 2) { switch (i) { case 1: return 'ytd-player#ytd-player audio.video-stream.html5-main-video[src]'; case 2: return 'ytd-browse[role="main"] audio.video-stream.html5-main-video[src]'; case 0: default: return '#movie_player audio.video-stream.html5-main-video[src]'; } } return null; }; return { xReplaceState(s, u) { try { history.replaceState(s, '', u); } catch { // in case error occurs if replaceState is replaced by any external script / extension } if (s.endpoint) { try { const ytdAppElm = document.querySelector('ytd-app'); const ytdAppCnt = insp(ytdAppElm); ytdAppCnt.replaceState(s.endpoint, '', u); } catch { } } }, getMediaElement(i) { const s = _getMediaElement(i) || ''; if (s) return document.querySelector(s); return null; }, getMediaElements(i) { const s = _getMediaElement(i) || ''; if (s) return document.querySelectorAll(s); return []; }, }; })(); let inPageRearrange = false; let tmpLastVideoId = ''; // const nsMap = new Map(); 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 ''; }; // eslint-disable-next-line no-unused-vars 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.hasAttributedStringText = true; inlineExpanderCnt.isResetMutation = true; fixInlineExpanderDisplay(inlineExpanderCnt); // do the initial fix } }; const fixInlineExpanderContent = () => { // console.log(21886,1) const mainInfo = getMainInfo(); if (!mainInfo) return; // console.log(21886,2) const inlineExpanderElm = mainInfo.querySelector('ytd-text-inline-expander'); const inlineExpanderCnt = insp(inlineExpanderElm); fixInlineExpanderMethods(inlineExpanderCnt); // console.log(21886, 3) // if (inlineExpanderCnt && inlineExpanderCnt.isExpanded === true && plugin.autoExpandInfoDesc.activated) { // // inlineExpanderCnt.isExpandedChanged(); // // holdInlineExpanderAlwaysExpanded(inlineExpanderCnt); // } // if(inlineExpanderCnt){ // // console.log(21886,4, inlineExpanderCnt.isExpanded, inlineExpanderCnt.isTruncated) // if (inlineExpanderCnt.isExpanded === false && inlineExpanderCnt.isTruncated === true) { // // console.log(21881) // inlineExpanderCnt.isTruncated = false; // } // } }; const plugin = { minibrowser: { activated: false, toUse: true, // depends on shouldUseMiniPlayer() activate() { if (this.activated) return; // Use global isPassiveArgSupport constant // https://caniuse.com/?search=observer // https://caniuse.com/?search=addEventListener%20passive 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, // false by default; once the expand is clicked, maintain the feature until the browser is closed. /** @type { MutationObserver | null } */ 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(); // holdInlineExpanderAlwaysExpanded(inlineExpanderCnt); } 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.nodeName.toLowerCase() === 'ytd-expander') { this.mo.observe(mainInfo, { attributes: true, attributeFilter: ['collapsed', 'attr-8ifv7'], }); } else { this.mo.observe(mainInfo, { attributes: true, attributeFilter: ['attr-8ifv7'] }); } mainInfo.incAttribute111('attr-8ifv7'); }, }, fullChannelNameOnHover: { activated: false, toUse: true, /** @type { MutationObserver | null } */ mo: null, /** @type { ResizeObserver | 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'); // console.log('mouseEnter') }, 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'); // console.log('mouseLeaveFn') }, 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; this.mo.observe(uploadInfo, { attributes: true, attributeFilter: ['hidden', 'attr-3wb0k'], }); uploadInfo.incAttribute111('attr-3wb0k'); this.ro.observe(uploadInfo); }, activate() { if (this.activated) return; // Use global isPassiveArgSupport constant // https://caniuse.com/?search=observer // https://caniuse.com/?search=addEventListener%20passive 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(); }, }, }; if (sessionStorage.__$$tmp_UseAutoExpandInfoDesc$$__) plugin.autoExpandInfoDesc.toUse = true; // let shouldFixInfo = false; 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) { // try later 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'); // eg short if (!hasFlexyParent) return true; const currentFlexy = elements.flexy; if (currentFlexy && currentFlexy !== hasFlexyParent) return true; } return false; }; // const mutationComment = document.createComment('1'); // let mutationPromise = new PromiseExternal(); // const mutationPromiseObs = new MutationObserver(()=>{ // mutationPromise.resolve(); // mutationPromise = new PromiseExternal(); // }); // mutationPromiseObs.observe(mutationComment, {characterData: true}); 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 => { // isResize is true if the layout is resized (not due to tab switching) // youtube components shall handle the resize issue. can skip some checkings. 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') { // #tab-info is now shown. // to fix the sizing issue (description info cards in 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); } } } // to fix expand/collapse sizing issue (inline-expander in tab info) // for example, expand button is required but not shown as it was rendered in the hidden state 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); // reflow due to offsetWidth calling } fixInlineExpanderDisplay(cnt); // just in case } } 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; // if (inPageRearrange) 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.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; 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', ''); } // console.log('ytd-watch-next-secondary-results-renderer::attached', hostElement); }, 'ytd-watch-next-secondary-results-renderer::detached': hostElement => { // if (inPageRearrange) return; 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.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; 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 commentsArea = elements.comments; if ( commentsArea !== hostElement || hostElement.isConnected !== true || cnt.isAttached !== true || !cnt.data || cnt.hidden !== false ) { return; } const ytdFlexyElm = elements.flexy; 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; // commentsArea's attribute: tyt-comments-video-id // ytdFlexyElm's attribute: video-id const commentsArea = elements.comments; const ytdFlexyElm = elements.flexy; 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', ''); // removeKeepCommentsScroller(); } } } }, '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); } // Promise.resolve(this.hostElement).then(eventMap['ytd-comments::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::detached']) .catch(console.warn); } // Promise.resolve(this.hostElement).then(eventMap['ytd-comments::dataChanged_']).catch(console.warn); return this.detached498(); }; } cProto._createPropertyObserver('data', '_dataChanged498', undefined); cProto._dataChanged498 = function () { // console.log('_dataChanged498', this.hostElement) Promise.resolve(this.hostElement) .then(eventMap['ytd-comments::_dataChanged498']) .catch(console.warn); }; // if (!cProto.dataChanged498_ && typeof cProto.dataChanged_ === 'function') { // cProto.dataChanged498_ = cProto.dataChanged_; // cProto.dataChanged_ = function () { // Promise.resolve(this.hostElement).then(eventMap['ytd-comments::dataChanged_']).catch(console.warn); // return this.dataChanged498_(); // } // } makeInitAttached('ytd-comments'); }, 'ytd-comments::_dataChanged498': hostElement => { // console.log(18984, hostElement.hasAttribute('tyt-comments-area')) 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); // ytdFlexyElm.setAttribute111('tyt-comment-disabled', '') } else { // ytdFlexyElm.removeAttribute000('tyt-comment-disabled') hostElement.removeAttribute000('tyt-comments-data-status'); } Promise.resolve(hostElement).then(eventMap['settingCommentsVideoId']).catch(console.warn); }, 'ytd-comments::attached': async hostElement => { if (invalidFlexyParent(hostElement)) return; // if (inPageRearrange) 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.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; if (!hostElement || hostElement.id !== 'comments') return; // if (!hostElement || hostElement.closest('[hidden]')) return; elements.comments = hostElement; console.log('ytd-comments::attached'); Promise.resolve(hostElement).then(eventMap['settingCommentsVideoId']).catch(console.warn); aoComment.observe(hostElement, { attributes: true }); 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(!elements.comments || elements.comments.isConnected === false) return; 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); // document.querySelector('#tab-comments').classList.remove('tab-content-hidden') // document.querySelector('[tyt-tab-content="#tab-comments"]').classList.remove('tab-btn-hidden') Promise.resolve(lockSet['removeKeepCommentsScrollerLock']) .then(removeKeepCommentsScroller) .catch(console.warn); } TAB_AUTO_SWITCH_TO_COMMENTS && switchToTab('#tab-comments'); }, 'ytd-comments::detached': hostElement => { // if (inPageRearrange) return; DEBUG_5084 && console.log(5084, 'ytd-comments::detached'); // console.log(858, hostElement) if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; // if (hostElement.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; if (hostElement.hasAttribute000('tyt-comments-area')) { // foComments.disconnect(); // foComments.takeRecords(); hostElement.removeAttribute000('tyt-comments-area'); // document.querySelector('#tab-comments').classList.add('tab-content-hidden') // document.querySelector('[tyt-tab-content="#tab-comments"]').classList.add('tab-btn-hidden') 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); // force dataChanged on attached 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; // if (inPageRearrange) 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.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; if (!hostElement || !hostElement.classList.contains('ytd-item-section-renderer')) return; // console.log(12991, 'ytd-comments-header-renderer::attached') const targetElement = document.querySelector( '[tyt-comments-area] ytd-comments-header-renderer' ); if (hostElement === targetElement) { hostElement.setAttribute111('tyt-comments-header-field', ''); } else { const parentNode = hostElement.parentNode; if ( parentNode instanceof HTMLElement_ && parentNode.querySelector('[tyt-comments-header-field]') ) { hostElement.setAttribute111('tyt-comments-header-field', ''); } } }, 'ytd-comments-header-renderer::detached': hostElement => { // if (inPageRearrange) return; 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.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; // console.log(12992, 'ytd-comments-header-renderer::detached') 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'] ); } headerMutationObserver.observe(hostElement.parentNode, { subtree: false, childList: true, }); 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.data; 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(); }; } /* console.log('ytd-expander::defined 01'); CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.connectedCallback = connectedCallbackY(CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.connectedCallback) CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.disconnectedCallback = disconnectedCallbackY(CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.disconnectedCallback) console.log('ytd-expander::defined 02'); */ 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); // console.log(1299, hostElement.parentNode, isRightTabsInserted) 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'); // add a classname for it const infoExpander = elements.infoExpander; // const infoExpanderBack = elements.infoExpanderBack; // console.log(5438,infoExpander, qt); // const dummy = document.createElement('noscript'); // dummy.setAttribute000('id', 'info-expander-vid'); // dummy.setAttribute000('video-id', getCurrentVideoId()); // infoExpander.insertBefore000(dummy, infoExpander.firstChild); // aoInfo.observe(infoExpander, { attributes: true, attributeFilter: ['tyt-display-for', 'tyt-video-id'] }); // zoInfo.observe(infoExpanderBack, { attributes: true, attributeFilter: ['hidden', 'attr-w20ts'], childList: true, subtree: true}); // new MutationObserver(()=>{ // console.log(591499) // }).observe(infoExpanderBack, {childList: true, subtree: true}) 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, }); // hidden + subtree to trigger the fn by delayedUpdate 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); // required when the page is switched from channel to watch // if (infoExpander && infoExpander.closest('#right-tabs')) Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); // infoExpanderBack.incAttribute111('attr-w20ts'); // return; } 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]')) { // const cnt = insp(hostElement); // if(cnt.data){ // cnt.data = Object.assign({}, cnt.data); // } } 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); } // document.querySelector('#tab-info').assignChildren111(null, bodyRendererNew, null); const cnt = insp(bodyRendererNew); cnt.data = Object.assign({}, insp(bodyRenderer).data); const inlineExpanderElm = bodyRendererNew.querySelector('ytd-text-inline-expander'); const inlineExpanderCnt = insp(inlineExpanderElm); fixInlineExpanderMethods(inlineExpanderCnt); // insp(bodyRendererNew).data = insp(bodyRenderer).data; // if((bodyRendererNew.hasAttribute('hidden')?1:0)^(bodyRenderer.hasAttribute('hidden')?1:0)){ // if(bodyRenderer.hasAttribute('hidden')) bodyRendererNew.setAttribute('hidden', ''); // else bodyRendererNew.removeAttribute('hidden'); // } elements.infoExpanderRendererBack = bodyRenderer; elements.infoExpanderRendererFront = bodyRendererNew; bodyRenderer.setAttribute('tyt-info-renderer-back', ''); bodyRendererNew.setAttribute('tyt-info-renderer-front', ''); // elements.infoExpanderBack = {{ytd-expander}}; } }, 'ytd-expandable-video-description-body-renderer::detached': async hostElement => { if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; // if (hostElement.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; // console.log(5992, hostElement) 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 (inPageRearrange) 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.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; // console.log(4959, hostElement) 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', ''); ioComment.observe(hostElement); } // -------------- // else if (hostElement instanceof HTMLElement_ && hostElement.matches('ytd-expander#expander.style-scope.ytd-expandable-video-description-body-renderer')) { // // && !hostElement.matches('#right-tabs ytd-expander#expander, [hidden] ytd-expander#expander') // console.log(5084, 'ytd-expander::attached'); // const bodyRenderer = hostElement.closest('ytd-expandable-video-description-body-renderer'); // 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); // } // // document.querySelector('#tab-info').assignChildren111(null, bodyRendererNew, null); // insp(bodyRendererNew).data = insp(bodyRenderer).data; // // if((bodyRendererNew.hasAttribute('hidden')?1:0)^(bodyRenderer.hasAttribute('hidden')?1:0)){ // // if(bodyRenderer.hasAttribute('hidden')) bodyRendererNew.setAttribute('hidden', ''); // // else bodyRendererNew.removeAttribute('hidden'); // // } // elements.infoExpanderRendererBack = bodyRenderer; // elements.infoExpanderRendererFront = bodyRendererNew; // bodyRenderer.setAttribute('tyt-info-renderer-back','') // bodyRendererNew.setAttribute('tyt-info-renderer-front','') // elements.infoExpanderBack = hostElement; // } // -------------- // console.log('ytd-expander::attached', hostElement); }, 'ytd-expander::detached': hostElement => { // if (inPageRearrange) return; if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; // if (hostElement.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; // console.log(5992, hostElement) 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'); } // console.log('ytd-expander::detached', hostElement); }, 'ytd-live-chat-frame::defined': cProto => { // eslint-disable-next-line no-unused-vars let lastDomAction = 0; if (!cProto.attached498 && typeof cProto.attached === 'function') { cProto.attached498 = cProto.attached; cProto.attached = function () { lastDomAction = Date.now(); // console.log('chat868-attached', 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(); // console.log('chat868-detached', 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__; const t = (ath = (ath & 1073741823) + 1); 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); // neccessary for Brave 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(chatframe); }).catch(console.warn); await Promise.race([p1, p2]); if (t !== ath) return; } this.urlChanged66(); }; cProto.urlChanged = function () { const t = (this.__urlChangedAsyncT688__ = (this.__urlChangedAsyncT688__ & 1073741823) + 1); nextBrowserTick(() => { if (t !== this.__urlChangedAsyncT688__) return; this.urlChangedAsync12(); }); }; } makeInitAttached('ytd-live-chat-frame'); }, 'ytd-live-chat-frame::attached': async hostElement => { if (invalidFlexyParent(hostElement)) return; // if (inPageRearrange) 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.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; 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; aoChat.observe(chatElem, { attributes: true }); 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__; const p = (cnt.__urlChangedAsyncT689__ = new PromiseExternal()); 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 => { // if (inPageRearrange) return; DEBUG_5084 && console.log(5084, 'ytd-live-chat-frame::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; // if (hostElement.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; 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); aoEgmPanels.observe(hostElement, { attributes: true, attributeFilter: ['visibility', 'hidden'], }); // console.log(5094, 2, 'ytd-engagement-panel-section-list-renderer::attached', hostElement); } }, 'ytd-engagement-panel-section-list-renderer::attached': hostElement => { if (invalidFlexyParent(hostElement)) return; // if (inPageRearrange) 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.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; // console.log('ytd-engagement-panel-section-list-renderer::attached', hostElement) // console.log(5094, 1, 'ytd-engagement-panel-section-list-renderer::attached', hostElement); 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 => { // if (inPageRearrange) return; 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.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; 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; // if (inPageRearrange) 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 (hostElement.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; if (plugin.fullChannelNameOnHover.activated) { plugin.fullChannelNameOnHover.onNavigateFinish(); } }, 'ytd-watch-metadata::detached': hostElement => { // if (inPageRearrange) return; DEBUG_5084 && console.log(5084, 'ytd-watch-metadata::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; // if (hostElement.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; }, '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; // if (inPageRearrange) 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; // if (hostElement.__connectedFlg__ !== 4) return; // hostElement.__connectedFlg__ = 5; elements.playlist = hostElement; aoPlayList.observe(hostElement, { attributes: true, attributeFilter: ['hidden', 'collapsed', 'attr-1y6nu'], }); hostElement.incAttribute111('attr-1y6nu'); }, 'ytd-playlist-panel-renderer::detached': hostElement => { // if (inPageRearrange) return; DEBUG_5084 && console.log(5084, 'ytd-playlist-panel-renderer::detached'); if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return; if (hostElement.isConnected !== false) return; // if (hostElement.__connectedFlg__ !== 8) return; // hostElement.__connectedFlg__ = 9; }, _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; /* ytd-watch-flexy:not([panels-beside-player]):not([fixed-panels]) #panels-full-bleed-container.ytd-watch-flexy{ display: none;} #player-full-bleed-container.ytd-watch-flexy{ position: relative; flex: 1;} */ const ytdFlexyElm = elements.flexy; // if(ytdFlexyElm && ytdFlexyElm.matches('ytd-watch-flexy[fixed-panels][theater]')){ // // ytdFlexyElm.fixedPanels = true; // ytdFlexyElm.removeAttribute000('fixed-panels'); // } if ( ytdFlexyElm && ytdFlexyElm.matches( 'ytd-watch-flexy[theater][flexy][full-bleed-player]:not([full-bleed-no-max-width-columns])' ) ) { // ytdFlexyElm.fullBleedNoMaxWidthColumns = true; ytdFlexyElm.setAttribute111('full-bleed-no-max-width-columns', ''); } const related = elements.related; if (related && related.isConnected && !related.closest('#right-tabs #tab-videos')) { document.querySelector('#tab-videos').assignChildren111(null, related, null); } const infoExpander = elements.infoExpander; if ( infoExpander && infoExpander.isConnected && !infoExpander.closest('#right-tabs #tab-info') ) { document.querySelector('#tab-info').assignChildren111(null, infoExpander, null); } else { // if (infoExpander && ytdFlexyElm && shouldFixInfo) { // shouldFixInfo = false; // Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); // } } const commentsArea = elements.comments; if (commentsArea) { const isConnected = commentsArea.isConnected; if (isConnected && !commentsArea.closest('#right-tabs #tab-comments')) { const tab = document.querySelector('#tab-comments'); tab.assignChildren111(null, commentsArea, null); } else { // if (!isConnected || tab.classList.contains('tab-content-hidden')) removeKeepCommentsScroller(); } } }, '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; // shouldFixInfo = true; // console.log('yt-navigate-finish') const flexyArr = [...document.querySelectorAll('ytd-watch-flexy')].filter( e => !e.closest('[hidden]') && e.querySelector('#player') ); if (flexyArr.length === 1) { // const lockId = lockSet['yt-navigate-finish-videos']; 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.chat; if (chat instanceof Element) { chat.setAttribute111('tyt-active-chat-frame', 'CF'); // chat and flexy ready } const infoExpander = elements.infoExpander; 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: () => { // if(lockId !== lockGet['yt-navigate-finish-videos']) return; const related = elements.related; let rightTabs = document.querySelector('#right-tabs'); if (!document.querySelector('#right-tabs') && related) { getLangForPage(); const docTmp = document.createElement('template'); docTmp.innerHTML = createHTML(getTabsHTML()); const newElm = docTmp.content.firstElementChild; if (newElm !== null) { inPageRearrange = true; related.parentNode.insertBefore000(newElm, related); inPageRearrange = false; } rightTabs = newElm; rightTabs .querySelector('[tyt-tab-content="#tab-comments"]') .classList.add('tab-btn-hidden'); const secondaryWrapper = document.createElement('secondary-wrapper'); const secondaryInner = document.querySelector( '#secondary-inner.style-scope.ytd-watch-flexy' ); inPageRearrange = true; secondaryWrapper.replaceChildren000(...secondaryInner.childNodes); secondaryInner.insertBefore000(secondaryWrapper, secondaryInner.firstChild); inPageRearrange = false; rightTabs .querySelector('#material-tabs') .addEventListener('click', eventMap['tabs-btn-click'], true); inPageRearrange = true; if (!rightTabs.closest('secondary-wrapper')) secondaryWrapper.appendChild000(rightTabs); inPageRearrange = false; } 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' } ); for (const btn of document.querySelectorAll('.tab-btn[tyt-tab-content]')) { ioTabBtns.observe(btn); } if (!related.closest('#right-tabs')) { document.querySelector('#tab-videos').assignChildren111(null, related, null); } const infoExpander = elements.infoExpander; if (infoExpander && !infoExpander.closest('#right-tabs')) { document.querySelector('#tab-info').assignChildren111(null, infoExpander, null); } const commentsArea = elements.comments; if (commentsArea && !commentsArea.closest('#right-tabs')) { document.querySelector('#tab-comments').assignChildren111(null, commentsArea, null); } rightTabsProvidedPromise.resolve(); roRightTabs.disconnect(); roRightTabs.observe(rightTabs); const ytdFlexyElm = elements.flexy; const aoFlexy = new MutationObserver(eventMap['aoFlexyFn']); aoFlexy.observe(ytdFlexyElm, { attributes: true }); // Promise.resolve(lockSet['tabsStatusCorrectionLock']).then(eventMap['tabsStatusCorrection']).catch(console.warn); Promise.resolve(lockSet['fixInitialTabStateLock']) .then(eventMap['fixInitialTabStateFn']) .catch(console.warn); ytdFlexyElm.incAttribute111('attr-7qlsy'); // tabsStatusCorrectionLock and video-id } }, 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); let resetForPanelDisappeared = false; if (p !== q) { console.log(388, p, q); let actioned = false; 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' ) { // 24 -> 16 = -8; 'd' lastPanel = lastTab || ''; resetForPanelDisappeared = true; } else if ((p & (4 | 8)) === 8 && (q & (4 | 8)) === 4 && lastPanel === 'chat') { // click close lastPanel = lastTab || ''; resetForPanelDisappeared = true; } else if ((p & 128) === 128 && (q & 128) === 0 && lastPanel === 'playlist') { lastPanel = lastTab || ''; resetForPanelDisappeared = true; } tabAStatus = q; 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; } // p->q +2 if ((p & 2) === 0 && (q & 2) === 2 && (p & 128) === 128 && (q & 128) === 128) { lastPanel = lastTab || ''; ytBtnClosePlaylist(); actioned = true; } // p->q +8 if ( (p & (8 | 128)) === (0 | 128) && (q & (8 | 128)) === (8 | 128) && lastPanel === 'chat' ) { lastPanel = lastTab || ''; ytBtnClosePlaylist(); actioned = true; } // p->q +128 if ( (p & (2 | 128)) === (2 | 0) && (q & (2 | 128)) === (2 | 128) && lastPanel === 'playlist' ) { switchToTab(null); actioned = true; } // p->q +128 if ( (p & (8 | 128)) === (8 | 0) && (q & (8 | 128)) === (8 | 128) && lastPanel === 'playlist' ) { lastPanel = lastTab || ''; ytBtnCollapseChat(); actioned = true; } // p->q +128 if ((p & (1 | 16 | 128)) === (1 | 16) && (q & (1 | 16 | 128)) === (1 | 16 | 128)) { ytBtnCancelTheater(); actioned = true; } // p->q +1 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) { // p->q -64 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) ) { // p->q +8 ytBtnCancelTheater(); actioned = true; } else if ( (p & (1 | 16 | 32)) === (0 | 16 | 0) && (q & (1 | 16 | 32)) === (0 | 16 | 32) && (q & (2 | 8)) > 0 ) { // p->q +32 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 ) { // p->q -8 if (lastTab) { switchToTab(lastTab); actioned = true; } } else if ((p & 1) === 0 && (q & 1) === 1) { // p->q +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) { // p->q +2 ytBtnCancelTheater(); actioned = true; } else if ((p & 10) === 2 && (q & 10) === 10) { // p->q +8 switchToTab(null); actioned = true; } else if ((p & (8 | 32)) === (0 | 32) && (q & (8 | 32)) === (8 | 32)) { // p->q +8 ytBtnCloseEngagementPanels(); actioned = true; } else if ((p & (2 | 32)) === (0 | 32) && (q & (2 | 32)) === (2 | 32)) { // p->q +2 ytBtnCloseEngagementPanels(); actioned = true; } else if ((p & (2 | 8)) === (0 | 8) && (q & (2 | 8)) === (2 | 8)) { // p->q +2 ytBtnCollapseChat(); actioned = true; // if( lastPanel && (p & (1|16) === 16) && (q & (1 | 16 | 8 | 2)) === (16) ){ // switchToTab(lastTab) // actioned = true; // } } else if ((p & 1) === 1 && (q & (1 | 32)) === (0 | 0)) { // p->q -1 if (lastPanel === 'chat') { ytBtnExpandChat(); actioned = true; } else if (lastPanel === lastTab && lastTab) { switchToTab(lastTab); actioned = true; } } // 24 20 // 8 16 4 16 if (!actioned && (q & 128) === 128) { lastPanel = 'playlist'; if ((q & 2) === 2) { switchToTab(null); actioned = true; } } if ((p & 2) === 2 && (q & (2 | 128)) === (0 | 128)) { // p->q -2 } else if ((p & 8) === 8 && (q & (8 | 128)) === (0 | 128)) { // p->q -8 } else if ( !actioned && (p & (1 | 16)) === 16 && (q & (1 | 16 | 8 | 2 | 32 | 64)) === (16 | 0 | 0) ) { 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) { // if lastTab is undefined 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; // if ((bodyRendererNew.hasAttribute('hidden') ? 1 : 0) ^ (bodyRenderer.hasAttribute('hidden') ? 1 : 0)) { // if (bodyRenderer.hasAttribute('hidden')) bodyRendererNew.setAttribute('hidden', ''); // else bodyRendererNew.removeAttribute('hidden'); // } } Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); }, fixInitialTabStateFn: async lockId => { // console.log('fixInitialTabStateFn 0a'); if (lockGet['fixInitialTabStateLock'] !== lockId) return; // console.log('fixInitialTabStateFn 0b'); const delayTime = fixInitialTabStateK > 0 ? 200 : 1; await delayPn(delayTime); if (lockGet['fixInitialTabStateLock'] !== lockId) return; // console.log('fixInitialTabStateFn 0c'); 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 1b'); const btn0 = document.querySelector('.tab-btn-visible'); // or default button if (btn0) { switchToTab(btn0); } else { switchToTab(null); } } else { DEBUG_5085 && console.log('fixInitialTabStateFn 1c'); switchToTab(null); } } else { DEBUG_5085 && console.log('fixInitialTabStateFn 1z'); } // console.log('fixInitialTabStateFn 0d'); fixInitialTabStateK++; }, 'tabs-btn-click': evt => { const target = evt.target; if ( target instanceof HTMLElement_ && target.classList.contains('tab-btn') && target.hasAttribute000('tyt-tab-content') ) { evt.preventDefault(); evt.stopPropagation(); evt.stopImmediatePropagation(); const activeLink = target; switchToTab(activeLink); } }, }; 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)) { // https://github.com/webcomponents/polyfills/ Object.defineProperty(document, '__CE_registry', { get() { // return undefined }, 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 f = callback; callback = null; eventHandler = null; f(); }; document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false); } else { callback(); } }); // eslint-disable-next-line no-unused-vars 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.target; 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 ); // console.log('hi122') mLoaded.flag |= 1; document.documentElement.setAttribute111('tabview-loaded', mLoaded.makeString()); promiseForCustomYtElementsReady.then(eventMap['ceHack']).catch(console.warn); // eslint-disable-next-line no-unused-vars 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:var(--tyt-rounded-a1);} [tyt-tab^="#"] #right-tabs #material-tabs{border-radius:var(--tyt-rounded-a1) var(--tyt-rounded-a1) 0 0;} ytd-watch-flexy[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 var(--tyt-rounded-a1) var(--tyt-rounded-a1);} 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="+"] secondary-wrapper>[tyt-chat-container]{flex-grow:1;flex-shrink:0;display:flex;flex-direction:column;} [tyt-chat="+"] secondary-wrapper>[tyt-chat-container]>#chat{flex-grow:1;} 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_] 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;} `, }; (async () => { const communicationKey = `ck-${Date.now()}-${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`; /** @type {globalThis.PromiseConstructor} */ const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 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'}`; // Inject script using a script element with the page's nonce (if available) to comply with CSP let script = document.createElement('script'); // Try to get the nonce from an existing script on the page const existingScript = document.querySelector('script[nonce]'); if (existingScript && existingScript.nonce) { script.nonce = existingScript.nonce; } // Use TrustedTypes if available 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); })(); // --- MODULE: basic.js --- const YouTubeUtils = (() => { 'use strict'; /** * Error logging with module context * @param {string} module - Module name * @param {string} message - Error message * @param {Error} error - Error object */ const logError = (module, message, error) => { console.error(`[YouTube+][${module}] ${message}:`, error); }; /** * Safe function wrapper with error handling * @param {Function} fn - Function to wrap * @param {string} context - Context for error logging * @returns {Function} Wrapped function */ const safeExecute = (fn, context = 'Unknown') => { /** @this {any} */ return function (...args) { try { return fn.apply(this, args); } catch (error) { logError(context, 'Execution failed', error); return null; } }; }; /** * Safe async function wrapper with error handling * @param {Function} fn - Async function to wrap * @param {string} context - Context for error logging * @returns {Function} Wrapped async function */ const safeExecuteAsync = (fn, context = 'Unknown') => { /** @this {any} */ return async function (...args) { try { return await fn.apply(this, args); } catch (error) { logError(context, 'Async execution failed', error); return null; } }; }; /** * Sanitize HTML string to prevent XSS * @param {string} html - HTML string to sanitize * @returns {string} Sanitized HTML */ const sanitizeHTML = html => { if (typeof html !== 'string') return ''; const map = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '/': '/', }; return html.replace(/[<>&"'\/]/g, char => map[char]); }; /** * Validate URL to prevent injection attacks * @param {string} url - URL to validate * @returns {boolean} Whether URL is safe */ const isValidURL = url => { if (typeof url !== 'string') return false; try { const parsed = new URL(url); // Only allow http and https protocols return ['http:', 'https:'].includes(parsed.protocol); } catch { return false; } }; /** * Safe localStorage wrapper */ const storage = { /** * Get item from localStorage with JSON parsing * @param {string} key - Storage key * @param {*} defaultValue - Default value if key doesn't exist * @returns {*} Parsed value or default */ get: (key, defaultValue = null) => { try { if (typeof key !== 'string' || !key) { logError('Storage', 'Invalid storage key', new Error('Key must be a non-empty string')); return defaultValue; } const value = localStorage.getItem(key); return value !== null ? JSON.parse(value) : defaultValue; } catch (e) { logError('Storage', `Failed to get item: ${key}`, e); return defaultValue; } }, /** * Set item to localStorage with JSON serialization * @param {string} key - Storage key * @param {*} value - Value to store * @returns {boolean} Success status */ set: (key, value) => { try { if (typeof key !== 'string' || !key) { logError('Storage', 'Invalid storage key', new Error('Key must be a non-empty string')); return false; } localStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { logError('Storage', `Failed to set item: ${key}`, e); return false; } }, /** * Remove item from localStorage * @param {string} key - Storage key */ remove: key => { try { if (typeof key !== 'string' || !key) { logError('Storage', 'Invalid storage key', new Error('Key must be a non-empty string')); return; } localStorage.removeItem(key); } catch (e) { logError('Storage', `Failed to remove item: ${key}`, e); } }, }; // Use shared debounce and throttle from YouTubeUtils (defined in utils.js) const debounce = /** @type {any} */ (window).YouTubeUtils?.debounce || ((func, wait, options = {}) => { let timeout; let lastArgs; let lastThis; /** @this {any} */ const debounced = function (...args) { lastArgs = args; lastThis = this; clearTimeout(timeout); if (options.leading && !timeout) { /** @type {Function} */ (func).apply(this, args); } timeout = setTimeout(() => { if (!options.leading) { /** @type {Function} */ (func).apply(lastThis, lastArgs); } timeout = null; lastArgs = null; lastThis = null; }, wait); }; debounced.cancel = () => { clearTimeout(timeout); timeout = null; lastArgs = null; lastThis = null; }; return debounced; }); const throttle = /** @type {any} */ (window).YouTubeUtils?.throttle || ((func, limit) => { let inThrottle; let lastResult; /** @this {any} */ return function (...args) { if (!inThrottle) { lastResult = /** @type {Function} */ (func).apply(this, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } return lastResult; }; }); /** * Safe DOM element creation with props and children * @param {string} tag - HTML tag name * @param {Object} props - Element properties * @param {Array} children - Child elements or text * @returns {HTMLElement} Created element */ const createElement = (tag, props = {}, children = []) => { // Validate tag name to prevent XSS 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') { // Prevent direct HTML injection 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; }; /** * DOM Selector Cache with automatic cleanup */ const selectorCache = new Map(); const CACHE_MAX_SIZE = 50; const CACHE_MAX_AGE = 5000; // 5 seconds /** * Cached querySelector with LRU-like eviction * @param {string} selector - CSS selector * @param {boolean} nocache - Skip cache * @returns {HTMLElement|null} Found element */ const querySelector = (selector, nocache = false) => { if (nocache) return document.querySelector(selector); const now = Date.now(); const cached = selectorCache.get(selector); // Check if cached element is still valid if (cached?.element?.isConnected && now - cached.timestamp < CACHE_MAX_AGE) { return cached.element; } // Remove stale entry if (cached) { selectorCache.delete(selector); } const element = document.querySelector(selector); if (element) { // LRU eviction: remove oldest entries if cache is full if (selectorCache.size >= CACHE_MAX_SIZE) { const firstKey = selectorCache.keys().next().value; selectorCache.delete(firstKey); } selectorCache.set(selector, { element, timestamp: now }); } return element; }; /** * Wait for element with timeout and AbortController * @param {string} selector - CSS selector * @param {number} timeout - Timeout in ms * @param {HTMLElement} parent - Parent element to search in * @returns {Promise} Promise resolving to element */ const waitForElement = (selector, timeout = 5000, parent = document.body) => { return new Promise((resolve, reject) => { // Validate inputs 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; } // Check if element already exists try { const element = parent.querySelector(selector); if (element) { resolve(/** @type {HTMLElement} */(/** @type {unknown} */ (element))); return; } } catch { reject(new Error(`Invalid selector: ${selector}`)); return; } const controller = new AbortController(); let observer = null; const timeoutId = setTimeout(() => { controller.abort(); if (observer) { try { observer.disconnect(); } catch (e) { logError('waitForElement', 'Observer disconnect failed', e); } } reject(new Error(`Element ${selector} not found within ${timeout}ms`)); }, timeout); observer = new MutationObserver(() => { try { const element = parent.querySelector(selector); if (element) { clearTimeout(timeoutId); observer.disconnect(); resolve(/** @type {HTMLElement} */(/** @type {unknown} */ (element))); } } catch (e) { logError('waitForElement', 'Observer callback error', e); } }); try { // Ensure parent supports observe/querySelector if (!(parent instanceof Element) && parent !== document) { throw new Error('Parent does not support observation'); } observer.observe(parent, { childList: true, subtree: true }); } catch { // Fallback for browsers without signal support try { observer.observe(parent, { childList: true, subtree: true }); } catch { clearTimeout(timeoutId); reject(new Error('Failed to observe DOM')); } } }); }; /** * Resource Cleanup Manager * Manages observers, listeners, and intervals */ const cleanupManager = { observers: new Set(), listeners: new Map(), intervals: new Set(), timeouts: new Set(), animationFrames: new Set(), /** * Register MutationObserver for cleanup * @param {MutationObserver} observer - Observer to register * @returns {MutationObserver} Registered observer */ registerObserver: observer => { cleanupManager.observers.add(observer); return observer; }, /** * Unregister and disconnect specific observer * @param {MutationObserver} observer - Observer to unregister */ unregisterObserver: observer => { if (observer) { try { observer.disconnect(); } catch (e) { logError('Cleanup', 'Observer disconnect failed', e); } cleanupManager.observers.delete(observer); } }, /** * Register event listener for cleanup * @param {EventTarget|Document|Window} element - Target element * @param {string} event - Event name * @param {EventListener|EventListenerObject} handler - Event handler * @param {Object} options - Event listener options * @returns {Symbol} Listener key for later removal */ registerListener: (element, event, handler, options) => { const key = Symbol('listener'); cleanupManager.listeners.set(key, { element, event, handler, options }); try { element.addEventListener(event, /** @type {EventListener} */(handler), options); } catch { // best-effort: if addEventListener fails, still register the listener record } return key; }, /** * Unregister specific listener * @param {Symbol} key - Listener 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); } }, /** * Register interval for cleanup * @param {TimerId} id - Interval ID * @returns {TimerId} Interval ID */ registerInterval: id => { cleanupManager.intervals.add(id); return id; }, /** * Unregister specific interval * @param {number} id - Interval ID */ unregisterInterval: id => { clearInterval(id); cleanupManager.intervals.delete(id); }, /** * Register timeout for cleanup * @param {TimerId} id - Timeout ID * @returns {TimerId} Timeout ID */ registerTimeout: id => { cleanupManager.timeouts.add(id); return id; }, /** * Unregister specific timeout * @param {number} id - Timeout ID */ unregisterTimeout: id => { clearTimeout(id); cleanupManager.timeouts.delete(id); }, /** * Register animation frame for cleanup * @param {number} id - Animation frame ID * @returns {number} Animation frame ID */ registerAnimationFrame: id => { cleanupManager.animationFrames.add(id); return id; }, /** * Unregister specific animation frame * @param {number} id - Animation frame ID */ unregisterAnimationFrame: id => { cancelAnimationFrame(id); cleanupManager.animationFrames.delete(id); }, /** * Cleanup all registered resources */ cleanup: () => { // Disconnect all observers cleanupManager.observers.forEach(obs => { try { obs.disconnect(); } catch (e) { logError('Cleanup', 'Observer disconnect failed', e); } }); cleanupManager.observers.clear(); // Remove all listeners cleanupManager.listeners.forEach(({ element, event, handler, options }) => { try { element.removeEventListener(event, handler, options); } catch (e) { logError('Cleanup', 'Listener removal failed', e); } }); cleanupManager.listeners.clear(); // Clear all intervals cleanupManager.intervals.forEach(id => clearInterval(id)); cleanupManager.intervals.clear(); // Clear all timeouts cleanupManager.timeouts.forEach(id => clearTimeout(id)); cleanupManager.timeouts.clear(); // Cancel all animation frames cleanupManager.animationFrames.forEach(id => cancelAnimationFrame(id)); cleanupManager.animationFrames.clear(); }, }; /** * Settings Manager * Centralized settings storage and retrieval */ 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 }, // Add other modules... }, /** * Load all settings * @returns {Object} Settings object */ load() { const saved = storage.get(this.storageKey); return saved ? { ...this.defaults, ...saved } : { ...this.defaults }; }, /** * Save all settings * @param {Object} settings - Settings to save */ save(settings) { storage.set(this.storageKey, settings); // Dispatch event for modules to react window.dispatchEvent( new CustomEvent('youtube-plus-settings-changed', { detail: settings, }) ); }, /** * Get setting by path * @param {string} path - Dot-separated path (e.g., 'speedControl.enabled') * @returns {*} Setting value */ get(path) { const settings = this.load(); return path.split('.').reduce((obj, key) => obj?.[key], settings); }, /** * Set setting by path * @param {string} path - Dot-separated path * @param {*} value - Value to set */ 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); }, }; /** * Style Manager * Centralized CSS injection and management */ const StyleManager = { styles: new Map(), element: null, /** * Add CSS rules * @param {string} id - Unique identifier * @param {string} css - CSS rules */ 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 CSS rules * @param {string} id - Identifier */ remove(id) { this.styles.delete(id); this.update(); }, /** * Update style element */ 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 all styles */ clear() { this.styles.clear(); if (this.element) { try { this.element.remove(); } catch (e) { logError('StyleManager', 'Failed to remove style element', e); } this.element = null; } }, }; /** * Centralized Notification System * Manages all notifications with queue and deduplication */ const NotificationManager = { queue: [], activeNotifications: new Set(), maxVisible: 3, defaultDuration: 3000, /** * Show notification * @param {string} message - Notification message * @param {Object} options - Notification options * @returns {HTMLElement} Notification element */ show(message, options = {}) { // Validate message 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, // { text: string, callback: function } } = options; // Remove duplicate messages 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 { // Use shared enhancer notification class for consistent appearance const notification = createElement('div', { className: 'youtube-enhancer-notification', dataset: { message }, // Store message for deduplication // Keep minimal inline styles; main visuals come from the shared CSS class style: Object.assign( { zIndex: '10001', width: 'auto', display: 'flex', alignItems: 'center', gap: '10px', }, position && positions[position] ? positions[position] : {} ), }); // Add message (with accessibility attributes) 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); // Add action button if provided 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); } document.body.appendChild(notification); this.activeNotifications.add(notification); // Auto-dismiss if (duration > 0) { const timeoutId = setTimeout(() => this.remove(notification), duration); cleanupManager.registerTimeout(timeoutId); } // Limit visible notifications 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 * @param {HTMLElement} notification - Notification element */ remove(notification) { if (!notification || !notification.isConnected) return; try { notification.style.transform = 'translateY(100%)'; notification.style.opacity = '0'; const timeoutId = setTimeout(() => { try { notification.remove(); this.activeNotifications.delete(notification); } catch (e) { logError('NotificationManager', 'Failed to remove notification', e); } }, 300); cleanupManager.registerTimeout(timeoutId); } catch (error) { logError('NotificationManager', 'Failed to animate notification removal', error); // Force remove notification.remove(); this.activeNotifications.delete(notification); } }, /** * Clear all notifications */ clearAll() { this.activeNotifications.forEach(notif => { try { notif.remove(); } catch (e) { logError('NotificationManager', 'Failed to clear notification', e); } }); this.activeNotifications.clear(); }, }; // Add notification animation styles StyleManager.add( 'notification-animations', ` @keyframes slideInFromBottom { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } ` ); // Global cleanup on page unload window.addEventListener('beforeunload', () => { cleanupManager.cleanup(); selectorCache.clear(); StyleManager.clear(); NotificationManager.clearAll(); }); // Periodic cache cleanup to prevent memory leaks 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); // Clean every 30 seconds cleanupManager.registerInterval(cacheCleanupInterval); // Global error handler for uncaught promise rejections window.addEventListener('unhandledrejection', event => { logError('Global', 'Unhandled promise rejection', event.reason); event.preventDefault(); // Prevent console spam }); // Global error handler for uncaught errors window.addEventListener('error', event => { // Only log errors from our script if (event.filename && event.filename.includes('youtube')) { logError( 'Global', 'Uncaught error', new Error(`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`) ); } }); /** * Performance monitoring wrapper * @param {string} label - Operation label * @param {Function} fn - Function to monitor * @returns {Function} Wrapped function */ const measurePerformance = (label, fn) => { /** @this {any} */ 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; } }; }; /** * Async performance monitoring wrapper * @param {string} label - Operation label * @param {Function} fn - Async function to monitor * @returns {Function} Wrapped async function */ const measurePerformanceAsync = (label, fn) => { /** @this {any} */ 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; } }; }; /** * Mobile device detection * @returns {boolean} True if mobile device */ const isMobile = () => { return ( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768 ); }; /** * Get viewport dimensions * @returns {Object} Width and height */ const getViewport = () => ({ width: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0), height: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0), }); /** * Safe async retry wrapper * @param {Function} fn - Async function to retry * @param {number} retries - Number of retries * @param {number} delay - Delay between retries * @returns {Promise} Result or error */ 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))); } } }; // Export public API return { logError, safeExecute, safeExecuteAsync, sanitizeHTML, isValidURL, storage, debounce, throttle, createElement, querySelector, waitForElement, cleanupManager, SettingsManager, StyleManager, NotificationManager, clearCache: () => selectorCache.clear(), isMobile, getViewport, retryAsync, measurePerformance, measurePerformanceAsync, }; })(); // Make available globally if (typeof window !== 'undefined') { // Merge utilities into existing global YouTubeUtils without overwriting /** @type {any} */ (window).YouTubeUtils = /** @type {any} */ (window).YouTubeUtils || {}; const existing = /** @type {any} */ (window).YouTubeUtils; try { for (const k of Object.keys(YouTubeUtils)) { if (existing[k] === undefined) existing[k] = YouTubeUtils[k]; } } catch { } // Add initialization health check (non-intrusive) console.log('[YouTube+ v2.0] Core utilities merged'); // Expose debug info /** @type {any} */ (window).YouTubePlusDebug = { version: '2.0', 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, }), }; // Show subtle startup notification (only once per session) if (!sessionStorage.getItem('youtube_plus_started')) { sessionStorage.setItem('youtube_plus_started', 'true'); setTimeout(() => { if (YouTubeUtils.NotificationManager) { YouTubeUtils.NotificationManager.show('YouTube+ v2.0 loaded', { type: 'success', duration: 2000, position: 'bottom-right', }); } }, 1000); } } //----------------------------------------------------------------------------- // YouTube enhancements module (function () { 'use strict'; const YouTubeEnhancer = { // Speed control variables speedControl: { currentSpeed: 1, activeAnimationId: null, storageKey: 'youtube_playback_speed', }, _initialized: false, // Settings settings: { enableSpeedControl: true, enableScreenshot: true, enableDownload: true, // Состояние сайтов внутри сабменю кнопки Download (ytdl всегда включён) downloadSites: { y2mate: true, xbbuddy: true, }, // Настройки кастомизации download сайтов downloadSiteCustomization: { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}', }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}', }, }, storageKey: 'youtube_plus_settings', }, // Cache DOM queries _cache: new Map(), // Cached element getter 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)); } catch (e) { console.error('Error loading settings:', e); } }, init() { if (this._initialized) { return; } this._initialized = true; try { this.loadSettings(); } catch (error) { console.warn('[YouTube Enhancer] 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'; }); }, refreshDownloadButton() { const selector = '.ytp-download-button'; // Очистить кеш, чтобы избежать возврата удалённых элементов if (this._cache.has(selector)) { this._cache.delete(selector); } const existingButton = document.querySelector(selector); if (existingButton?.parentElement) { existingButton.remove(); } if (!this.settings.enableDownload) { return; } const controls = this.getElement('.ytp-right-controls', false); if (!controls) { return; } this.addDownloadButton(controls); }, 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() { // Glassmorphism styles for modal and controls 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;bottom:12px;width:44px;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;float:left;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:absolute!important;background:var(--yt-glass-bg)!important;color:var(--yt-text-primary)!important;border-radius:var(--yt-radius-md)!important;display:none;bottom: 100%!important;width:48px!important;z-index:9999!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);} .speed-option-item{cursor:pointer!important;height:25px!important;line-height:25px!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{position:fixed;bottom:70px;left:50%;transform:translateX(-50%);background:var(--yt-glass-bg);color:var(--yt-text-primary);padding:12px 24px;border-radius:var(--yt-radius-md);z-index:9999;transition:opacity .5s,transform .3s;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;} .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: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);animation:ytEnhanceFadeIn .25s ease-out;} .ytp-plus-settings-panel{background:var(--yt-glass-bg);color:var(--yt-text-primary);border-radius:var(--yt-radius-lg);width:720px;max-width:90%;max-height:90vh;overflow:hidden;box-shadow:var(--yt-glass-shadow);animation:ytEnhanceScaleIn .3s cubic-bezier(.4,0,.2,1);backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);border:1px solid var(--yt-glass-border);will-change:transform,opacity;display:flex;flex-direction:row;} .ytp-plus-settings-sidebar{width:200px;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:99999;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);display:none;} .download-options.visible{display:block;} .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);} .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;} `; // ✅ Use StyleManager instead of createElement('style') 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', 'YouTube + Settings'); 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(() => { }); }, createSettingsModal() { const modal = document.createElement('div'); modal.className = 'ytp-plus-settings-modal'; modal.innerHTML = `

Settings

Basic
Advanced
Experimental
About
Add speed control buttons to video player
Add screenshot capture button to video player
Add download button with multiple site options to video player
${this.settings.downloadSiteCustomization?.y2mate?.name || 'Y2Mate'}
Use custom downloader
${this.settings.downloadSiteCustomization?.xbbuddy?.name || '9xbuddy'}
Use custom downloader
by YTDL
Always enabled - GitHub repository
`; // Event delegation for better performance modal.addEventListener('click', e => { const target = /** @type {HTMLElement} */ (e.target); if (target === modal) modal.remove(); if ( target.classList.contains('ytp-plus-settings-close') || target.closest('.ytp-plus-settings-close') ) { modal.remove(); } // Обработка кнопки GitHub для YTDL if (target.id === 'open-ytdl-github' || target.closest('#open-ytdl-github')) { window.open('https://github.com/diorhc/YouTube-Downloader', '_blank'); return; } if (target.classList.contains('ytp-plus-settings-nav-item')) { // Handle sidebar navigation const section = /** @type {HTMLElement} */ (target).dataset.section; modal .querySelectorAll('.ytp-plus-settings-nav-item') .forEach(item => item.classList.remove('active')); modal .querySelectorAll('.ytp-plus-settings-section') .forEach(section => section.classList.add('hidden')); target.classList.add('active'); modal .querySelector(`.ytp-plus-settings-section[data-section="${section}"]`) .classList.remove('hidden'); } if (target.classList.contains('ytp-plus-settings-checkbox')) { const setting = /** @type {HTMLElement} */ (target).dataset.setting; if (!setting) return; // Сохранение простых настроек (enableSpeedControl, enableScreenshot, enableDownload) if (!setting.startsWith('downloadSite_')) { this.settings[setting] = /** @type {HTMLInputElement} */ (target).checked; // Показывать/скрывать сабменю при переключении Download if (setting === 'enableDownload') { const submenu = modal.querySelector('.download-submenu'); if (submenu) { submenu.style.display = /** @type {HTMLInputElement} */ (target).checked ? 'block' : 'none'; } } } else { // Обработка чекбоксов в сабменю: data-setting = downloadSite_ const key = setting.replace('downloadSite_', ''); if (!this.settings.downloadSites) { this.settings.downloadSites = { y2mate: true, xbbuddy: true }; } const checkbox = /** @type {HTMLElement} */ (target); this.settings.downloadSites[key] = /** @type {HTMLInputElement} */ (checkbox).checked; // Toggle visibility of controls for this site (if present in DOM) try { const container = checkbox.closest('.download-site-option'); if (container) { const controls = container.querySelector('.download-site-controls'); if (controls) { controls.style.display = /** @type {HTMLInputElement} */ (checkbox).checked ? 'block' : 'none'; } } } catch (err) { console.warn('[YouTube+] toggle download-site-controls failed:', err); } // Rebuild dropdown if present try { if ( typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && typeof (/** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown) === 'function' ) { /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } } } // Обработка кастомизации download сайтов if (target.classList.contains('download-site-input')) { const site = /** @type {HTMLElement} */ (target).dataset.site; const field = /** @type {HTMLElement} */ (target).dataset.field; if (!site || !field) return; if (!this.settings.downloadSiteCustomization) { this.settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}' }, }; } if (!this.settings.downloadSiteCustomization[site]) { this.settings.downloadSiteCustomization[site] = { name: '', url: '' }; } this.settings.downloadSiteCustomization[site][field] = /** @type {HTMLInputElement} */ ( target ).value; // Обновить имя в UI в реальном времени if (field === 'name') { const nameDisplay = target .closest('.download-site-option') ?.querySelector('.download-site-name'); if (nameDisplay) { nameDisplay.textContent = /** @type {HTMLInputElement} */ (target).value || (site === 'y2mate' ? 'Y2Mate' : '9xbuddy'); } } // Rebuild dropdown if present so changes reflect immediately try { if ( typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && typeof (/** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown) === 'function' ) { /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } } if (target.id === 'ytp-plus-save-settings') { this.saveSettings(); modal.remove(); this.showNotification('Settings saved'); } // Save specific Y2Mate customization if (target.id === 'download-y2mate-save') { // Ensure settings structure if (!this.settings.downloadSiteCustomization) { this.settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}' }, }; } if (!this.settings.downloadSiteCustomization.y2mate) { this.settings.downloadSiteCustomization.y2mate = { name: '', url: '' }; } // Read current inputs inside this download-site-option const container = /** @type {HTMLElement|null} */ ( /** @type {unknown} */ (target.closest('.download-site-option')) ); if (container) { 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) this.settings.downloadSiteCustomization.y2mate.name = nameInput.value; if (urlInput) this.settings.downloadSiteCustomization.y2mate.url = urlInput.value; } this.saveSettings(); try { if ( typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && typeof (/** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown) === 'function' ) { /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } this.showNotification('Y2Mate settings saved'); } // Reset Y2Mate to defaults if (target.id === 'download-y2mate-reset') { if (!this.settings.downloadSiteCustomization) { // Initialize with expected structure to satisfy type checks this.settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}' }, }; } this.settings.downloadSiteCustomization.y2mate = { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}', }; // Update inputs in modal if present const container = /** @type {HTMLElement|null} */ ( /** @type {unknown} */ (modal.querySelector('.download-site-option')) ); if (container) { 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'); if (nameInput) nameInput.value = this.settings.downloadSiteCustomization.y2mate.name; if (urlInput) urlInput.value = this.settings.downloadSiteCustomization.y2mate.url; if (nameDisplay) { nameDisplay.textContent = this.settings.downloadSiteCustomization.y2mate.name; } } this.saveSettings(); try { if ( typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && typeof (/** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown) === 'function' ) { /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } this.showNotification('Y2Mate reset to defaults'); } // Save specific 9xBuddy customization if (target.id === 'download-xbbuddy-save') { if (!this.settings.downloadSiteCustomization) { // Initialize expected structure this.settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}' }, }; } if (!this.settings.downloadSiteCustomization.xbbuddy) { this.settings.downloadSiteCustomization.xbbuddy = { name: '', url: '' }; } const container = /** @type {HTMLElement|null} */ ( /** @type {unknown} */ (target.closest('.download-site-option')) ); if (container) { const nameInput = container.querySelector( 'input.download-site-input[data-site="xbbuddy"][data-field="name"]' ); const urlInput = container.querySelector( 'input.download-site-input[data-site="xbbuddy"][data-field="url"]' ); if (nameInput) this.settings.downloadSiteCustomization.xbbuddy.name = nameInput.value; if (urlInput) this.settings.downloadSiteCustomization.xbbuddy.url = urlInput.value; } this.saveSettings(); try { if ( typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && typeof (/** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown) === 'function' ) { /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } this.showNotification('9xBuddy settings saved'); } // Reset 9xBuddy to defaults if (target.id === 'download-xbbuddy-reset') { if (!this.settings.downloadSiteCustomization) { this.settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}' }, }; } this.settings.downloadSiteCustomization.xbbuddy = { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}', }; // Update inputs in modal if present const container = /** @type {HTMLElement|null} */ ( /** @type {unknown} */ (modal.querySelectorAll('.download-site-option')[1]) ); if (container) { const nameInput = container.querySelector( 'input.download-site-input[data-site="xbbuddy"][data-field="name"]' ); const urlInput = container.querySelector( 'input.download-site-input[data-site="xbbuddy"][data-field="url"]' ); const nameDisplay = container.querySelector('.download-site-name'); if (nameInput) nameInput.value = this.settings.downloadSiteCustomization.xbbuddy.name; if (urlInput) urlInput.value = this.settings.downloadSiteCustomization.xbbuddy.url; if (nameDisplay) { nameDisplay.textContent = this.settings.downloadSiteCustomization.xbbuddy.name; } } this.saveSettings(); try { if ( typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && typeof (/** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown) === 'function' ) { /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } this.showNotification('9xBuddy reset to defaults'); } }); // Обработка изменений input полей для кастомизации modal.addEventListener('input', e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target.classList.contains('download-site-input')) { const site = /** @type {HTMLElement} */ (target).dataset.site; const field = /** @type {HTMLElement} */ (target).dataset.field; if (!site || !field) return; if (!this.settings.downloadSiteCustomization) { this.settings.downloadSiteCustomization = { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}' }, }; } if (!this.settings.downloadSiteCustomization[site]) { this.settings.downloadSiteCustomization[site] = { name: '', url: '' }; } this.settings.downloadSiteCustomization[site][field] = /** @type {HTMLInputElement} */ ( target ).value; // Обновить имя в UI в реальном времени if (field === 'name') { const nameDisplay = /** @type {HTMLElement|null} */ ( /** @type {unknown} */ (target.closest('.download-site-option')) )?.querySelector('.download-site-name'); if (nameDisplay) { nameDisplay.textContent = /** @type {HTMLInputElement} */ (target).value || (site === 'y2mate' ? 'Y2Mate' : '9xbuddy'); } } // Rebuild dropdown if present so changes reflect immediately try { if ( typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && typeof (/** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown) === 'function' ) { /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err); } } }); 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) { // ✅ Use centralized utility 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', 'Take screenshot'); button.innerHTML = ` `; button.addEventListener('click', this.captureFrame.bind(this)); controls.insertBefore(button, controls.firstChild); }, addDownloadButton(controls) { if (!this.settings.enableDownload) return; const button = document.createElement('div'); button.className = 'ytp-button ytp-download-button'; button.setAttribute('title', 'Download options'); button.setAttribute('tabindex', '0'); button.setAttribute('role', 'button'); button.setAttribute('aria-haspopup', 'true'); button.setAttribute('aria-expanded', 'false'); button.style.display = 'inline-block'; button.style.padding = '0 10px 0 0'; button.style.height = '36px'; button.innerHTML = ` `; // Dropdown options const options = document.createElement('div'); options.className = 'download-options'; options.setAttribute('role', 'menu'); // Position dropdown below button function positionDropdown() { const rect = button.getBoundingClientRect(); options.style.left = `${rect.left + rect.width / 2 - 75}px`; options.style.bottom = `${window.innerHeight - rect.top + 12}px`; } // Helper to open download site function openDownloadSite(url, isYTDL = false) { if (isYTDL) { // For YTDL: copy video URL to clipboard and open localhost const videoId = new URLSearchParams(location.search).get('v'); const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href; // Copy to clipboard navigator.clipboard .writeText(videoUrl) .then(() => { // Show notification YouTubeUtils.NotificationManager.show('URL скопирован в буфер обмена!', { duration: 2000, type: 'success', }); }) .catch(() => { // Fallback for older browsers const input = document.createElement('input'); input.value = videoUrl; document.body.appendChild(input); input.select(); document.execCommand('copy'); document.body.removeChild(input); YouTubeUtils.NotificationManager.show('URL скопирован в буфер обмена!', { duration: 2000, type: 'success', }); }); // Open YTDL in new tab window.open(url, '_blank'); } else { window.open(url, '_blank'); } options.classList.remove('visible'); button.setAttribute('aria-expanded', 'false'); } // Helper to rebuild the dropdown if settings changed while dropdown exists // Exposed on button element via dataset so external handlers can trigger a rebuild function rebuildDropdown() { try { // Remove existing list if present const existingList = options.querySelector('.download-options-list'); if (existingList) existingList.remove(); // Rebuild downloadSites from current settings const customizationNow = typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && /** @type {any} */ (window).youtubePlus.settings && /** @type {any} */ (window).youtubePlus.settings.downloadSiteCustomization ? /** @type {any} */ (window).youtubePlus.settings.downloadSiteCustomization : customization; const videoIdNow = new URLSearchParams(location.search).get('v'); const videoUrlNow = videoIdNow ? `https://www.youtube.com/watch?v=${videoIdNow}` : location.href; const buildUrlNow = template => (template || '') .replace('{videoId}', videoIdNow || '') .replace('{videoUrl}', encodeURIComponent(videoUrlNow)); const baseSitesNow = [ { key: 'y2mate', name: customizationNow?.y2mate?.name || 'Y2Mate', url: buildUrlNow( customizationNow?.y2mate?.url || `https://www.y2mate.com/youtube/{videoId}` ), isYTDL: false, }, { key: 'xbbuddy', name: customizationNow?.xbbuddy?.name || '9xbuddy', url: buildUrlNow( customizationNow?.xbbuddy?.url || `https://9xbuddy.org/process?url={videoUrl}` ), isYTDL: false, }, { key: 'ytdl', name: 'by YTDL', url: `http://localhost:5005`, isYTDL: true }, ]; const enabledSitesNow = typeof window !== 'undefined' && /** @type {any} */ (window).youtubePlus && /** @type {any} */ (window).youtubePlus.settings && /** @type {any} */ (window).youtubePlus.settings.downloadSites ? /** @type {any} */ (window).youtubePlus.settings.downloadSites : enabledSites; const downloadSitesNow = baseSitesNow.filter(s => { if (s.key === 'ytdl') return true; return enabledSitesNow[s.key] !== false; }); // If only one site remains replace click handler if (downloadSitesNow.length === 1) { const single = downloadSitesNow[0]; // Remove any existing clickable handlers on button button.replaceWith(button.cloneNode(true)); const newButton = controls.querySelector('.ytp-download-button'); if (newButton) { newButton.addEventListener('click', () => openDownloadSite(single.url, single.isYTDL) ); } return; } // Build new list const newList = document.createElement('div'); newList.className = 'download-options-list'; downloadSitesNow.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)); opt.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') openDownloadSite(site.url, site.isYTDL); }); newList.appendChild(opt); }); options.appendChild(newList); } catch (err) { console.warn('[YouTube+] rebuildDropdown failed:', err); } } // Get current video URL const videoId = new URLSearchParams(location.search).get('v'); const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href; // Получить кастомные настройки или использовать defaults const customization = this.settings.downloadSiteCustomization || { y2mate: { name: 'Y2Mate', url: 'https://www.y2mate.com/youtube/{videoId}' }, xbbuddy: { name: '9xbuddy', url: 'https://9xbuddy.org/process?url={videoUrl}' }, }; // Функция для замены плейсхолдеров в URL const buildUrl = template => { return template .replace('{videoId}', videoId || '') .replace('{videoUrl}', encodeURIComponent(videoUrl)); }; // List of download sites (ytdl всегда включён, filter by user settings.downloadSites для остальных) const baseSites = [ { key: 'y2mate', name: customization.y2mate?.name || 'Y2Mate', url: buildUrl(customization.y2mate?.url || `https://www.y2mate.com/youtube/{videoId}`), isYTDL: false, }, { key: 'xbbuddy', name: customization.xbbuddy?.name || '9xbuddy', url: buildUrl(customization.xbbuddy?.url || `https://9xbuddy.org/process?url={videoUrl}`), isYTDL: false, }, { key: 'ytdl', name: 'by YTDL', url: `http://localhost:5005`, isYTDL: true }, ]; const enabledSites = this.settings && this.settings.downloadSites ? this.settings.downloadSites : { y2mate: true, xbbuddy: true }; // YTDL всегда включён, фильтруем остальные по настройкам const downloadSites = baseSites.filter(s => { if (s.key === 'ytdl') return true; // ytdl всегда включён return enabledSites[s.key] !== false; }); // Если активен только один сайт — прямой переход без dropdown if (downloadSites.length === 1) { const singleSite = downloadSites[0]; button.style.cursor = 'pointer'; button.addEventListener('click', () => openDownloadSite(singleSite.url, singleSite.isYTDL)); controls.insertBefore(button, controls.firstChild); return; // Не создаём dropdown } // Centered list 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)); opt.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { openDownloadSite(site.url, site.isYTDL); } }); list.appendChild(opt); }); options.appendChild(list); button.appendChild(options); // Expose rebuild function globally (safe guard) so settings handlers can call it try { if (typeof window !== 'undefined') { /** @type {any} */ (window).youtubePlus = /** @type {any} */ (window).youtubePlus || {}; /** @type {any} */ (window).youtubePlus.rebuildDownloadDropdown = rebuildDropdown; // also store settings ref for rebuildDropdown to read /** @type {any} */ (window).youtubePlus.settings = /** @type {any} */ (window).youtubePlus.settings || this.settings; } } catch (e) { console.warn('[YouTube+] expose rebuildDownloadDropdown failed:', e); } let dropdownTimeout; function showDropdown() { clearTimeout(dropdownTimeout); positionDropdown(); options.classList.add('visible'); button.setAttribute('aria-expanded', 'true'); } function hideDropdown() { dropdownTimeout = setTimeout(() => { options.classList.remove('visible'); button.setAttribute('aria-expanded', 'false'); }, 150); } button.addEventListener('mouseenter', showDropdown); button.addEventListener('mouseleave', hideDropdown); options.addEventListener('mouseenter', showDropdown); options.addEventListener('mouseleave', hideDropdown); button.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { if (options.classList.contains('visible')) { hideDropdown(); } else { showDropdown(); } } }); controls.insertBefore(button, controls.firstChild); }, addSpeedControlButton(controls) { const speedBtn = document.createElement('div'); speedBtn.className = 'ytp-button speed-control-btn'; speedBtn.innerHTML = `${this.speedControl.currentSpeed}×`; const speedOptions = document.createElement('div'); speedOptions.className = 'speed-options'; [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.addEventListener('click', () => this.changeSpeed(speed)); speedOptions.appendChild(option); }); speedBtn.appendChild(speedOptions); let isHovering = false; speedBtn.addEventListener('mouseenter', () => { isHovering = true; speedOptions.style.display = 'block'; }); speedBtn.addEventListener('mouseleave', () => { isHovering = false; setTimeout(() => { if (!isHovering) speedOptions.style.display = 'none'; }, 150); }); controls.insertBefore(speedBtn, controls.firstChild); }, 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 ? '15px' : '12px'; }); }, changeSpeed(speed) { speed = Number(speed); this.speedControl.currentSpeed = speed; localStorage.setItem(this.speedControl.storageKey, String(speed)); const speedBtn = this.getElement('.speed-control-btn span', false); if (speedBtn) speedBtn.textContent = `${speed}×`; document.querySelectorAll('.speed-option-item').forEach(option => { option.classList.toggle('speed-option-active', parseFloat(option.dataset.speed) === speed); }); this.applyCurrentSpeed(); this.showSpeedIndicator(speed); }, 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); // ✅ Register interval in cleanupManager 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(); }); // ✅ Register observer in cleanupManager 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); // ✅ Safe observe with document.body check 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) ); }, }; // Save reference to init function BEFORE IIFE closes (critical for DOMContentLoaded) const initFunction = YouTubeEnhancer.init.bind(YouTubeEnhancer); // Initialize immediately or on DOMContentLoaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFunction); } else { initFunction(); } })(); // --- MODULE: enhanced.js --- // Enhanced Tabviews (function () { 'use strict'; /** * Configuration object for scroll-to-top button * @type {Object} * @property {boolean} enabled - Whether the feature is enabled * @property {string} storageKey - LocalStorage key for settings */ const config = { enabled: true, storageKey: 'youtube_top_button_settings', }; /** * Adds CSS styles for scroll-to-top button and scrollbars * @returns {void} */ 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:absolute;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:1000;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);} ytd-watch-flexy:not([tyt-tab^="#"]) .top-button{display:none;} `; document.head.appendChild(style); }; /** * Updates button visibility based on scroll position * @param {HTMLElement} scrollContainer - The container being scrolled * @returns {void} */ const handleScroll = scrollContainer => { const button = document.getElementById('right-tabs-top-button'); if (!button || !scrollContainer) return; button.classList.toggle('visible', scrollContainer.scrollTop > 100); }; /** * Sets up scroll event listener on active tab * @returns {void} */ const setupScrollListener = () => { 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)' ); if (activeTab) { const scrollHandler = () => handleScroll(activeTab); activeTab._topButtonScrollHandler = scrollHandler; activeTab.addEventListener('scroll', scrollHandler, { passive: true }); handleScroll(activeTab); } }; /** * Creates and appends scroll-to-top button * @returns {void} */ const createButton = () => { 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 = 'Scroll to top'; button.innerHTML = ''; button.addEventListener('click', () => { const activeTab = document.querySelector( '#right-tabs .tab-content-cld:not(.tab-content-hidden)' ); if (activeTab) activeTab.scrollTo({ top: 0, behavior: 'smooth' }); }); rightTabs.style.position = 'relative'; rightTabs.appendChild(button); setupScrollListener(); }; /** * Observes DOM changes to detect tab switches * @returns {void} */ const observeTabChanges = () => { const observer = new MutationObserver(mutations => { if ( mutations.some( m => m.type === 'attributes' && m.attributeName === 'class' && m.target instanceof Element && m.target.classList.contains('tab-content-cld') ) ) { setTimeout(setupScrollListener, 100); } }); const rightTabs = document.querySelector('#right-tabs'); if (rightTabs) { observer.observe(rightTabs, { attributes: true, subtree: true, attributeFilter: ['class'], }); } }; // Events const setupEvents = () => { document.addEventListener( 'click', e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target.closest && target.closest('.tab-btn[tyt-tab-content]')) { setTimeout(setupScrollListener, 100); } }, true ); }; // Initialize const init = () => { addStyles(); setupEvents(); const checkForTabs = () => { if (document.querySelector('#right-tabs')) { createButton(); observeTabChanges(); } else { setTimeout(checkForTabs, 500); } }; checkForTabs(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); // YouTube End Screen Remover (function () { 'use strict'; // Optimized configuration 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,.ytp-cards-button,.iv-drawer,.video-annotations', debounceMs: 32, batchSize: 20, }; // Minimal state with better tracking const state = { observer: null, styleEl: null, isActive: false, removeCount: 0, lastCheck: 0, ytNavigateListenerKey: null, settingsNavListenerKey: null, }; // High-performance utilities: use shared debounce when available 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 { } } } }; // Settings with caching 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()), }; // Optimized core functions const injectCSS = () => { if (state.styleEl || !CONFIG.enabled) return; // ✅ Use StyleManager instead of createElement('style') 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; // Mark as added }; 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 setupWatcher = () => { if (state.observer || !CONFIG.enabled) return; const throttledRemove = debounce(removeEndScreens, CONFIG.debounceMs); state.observer = new MutationObserver(mutations => { let hasRelevantChanges = false; for (const { addedNodes } of mutations) { for (const node of addedNodes) { if ( node instanceof Element && (node.className?.includes('ytp-') || node.querySelector?.('.ytp-ce-element')) ) { hasRelevantChanges = true; break; } } if (hasRelevantChanges) break; } if (hasRelevantChanges) throttledRemove(); }); // ✅ Register observer in cleanupManager 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(); }; // Streamlined settings UI 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 = `
Remove end screen suggestions and info cards${state.removeCount ? ` (${state.removeCount} removed)` : ''}
`; section.appendChild(container); container.querySelector('input').addEventListener( 'change', e => { const target = /** @type {EventTarget & HTMLInputElement} */ (e.target); CONFIG.enabled = target.checked; settings.save(); }, { passive: true } ); }; // Optimized navigation handler const handlePageChange = debounce(() => { if (location.pathname === '/watch') { cleanup(); requestIdleCallback ? requestIdleCallback(init) : setTimeout(init, 1); } }, 50); // Initialize settings.load(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } const handleSettingsNavClick = e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target.dataset?.section === 'advanced') { setTimeout(addSettingsUI, 10); } }; if (!state.ytNavigateListenerKey) { state.ytNavigateListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'yt-navigate-finish', handlePageChange, { passive: true } ); } // Settings modal integration 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; } } } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(settingsObserver); // ✅ Safe observe with document.body check 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 } ); } })(); // --- MODULE: adblocker.js --- // Ad Blocker (function () { 'use strict'; /** * Ad blocking functionality for YouTube * @namespace AdBlocker */ const AdBlocker = { /** * Configuration settings * @type {Object} */ config: { skipInterval: 500, removeInterval: 1500, enableLogging: false, maxRetries: 2, enabled: true, storageKey: 'youtube_adblocker_settings', }, /** * Current state tracking * @type {Object} */ state: { isYouTubeShorts: false, isYouTubeMusic: location.hostname === 'music.youtube.com', lastSkipAttempt: 0, retryCount: 0, initialized: false, }, /** * Cached DOM queries for performance * @type {Object} */ cache: { moviePlayer: null, ytdPlayer: null, lastCacheTime: 0, cacheTimeout: 5000, }, /** * Optimized CSS selectors for ad elements * @type {Object} */ 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 management with localStorage persistence * @type {Object} */ settings: { /** * Load settings from localStorage * @returns {void} */ load() { try { const saved = localStorage.getItem(AdBlocker.config.storageKey); if (saved) { const parsed = JSON.parse(saved); AdBlocker.config.enabled = parsed.enabled ?? true; AdBlocker.config.enableLogging = parsed.enableLogging ?? false; } } catch { // Silently fail if localStorage is unavailable } }, /** * Save settings to localStorage * @returns {void} */ save() { try { localStorage.setItem( AdBlocker.config.storageKey, JSON.stringify({ enabled: AdBlocker.config.enabled, enableLogging: AdBlocker.config.enableLogging, }) ); } catch { // Silently fail if localStorage is unavailable } }, }, /** * Get cached player elements * @returns {Object} Object containing player element and controller */ 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, }; }, /** * Skip current ad by seeking to end * @returns {void} */ 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; // Fast ad detection 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); // Mute ad immediately if (video) video.muted = true; // Skip logic based on platform if (AdBlocker.state.isYouTubeMusic && video) { /** @type {HTMLVideoElement} */ (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); // Use most efficient skip method 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); } } }, // Minimal CSS injection addCss() { if (document.querySelector('#yt-ab-styles') || !AdBlocker.config.enabled) return; // ✅ Use StyleManager instead of createElement('style') 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'); }, // Batched element removal removeElements() { if (!AdBlocker.config.enabled || AdBlocker.state.isYouTubeMusic) return; // Use requestIdleCallback for non-blocking removal 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); } }, // Optimized settings UI 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 = `
Skip ads and remove ad elements automatically
`; section.appendChild(item); item.querySelector('input').addEventListener('change', e => { const target = /** @type {EventTarget & HTMLInputElement} */ (e.target); AdBlocker.config.enabled = target.checked; AdBlocker.settings.save(); AdBlocker.config.enabled ? AdBlocker.addCss() : AdBlocker.removeCss(); }); } catch (error) { YouTubeUtils.logError('AdBlocker', 'Failed to add settings UI', error); } }, // Streamlined initialization init() { if (AdBlocker.state.initialized) return; AdBlocker.state.initialized = true; AdBlocker.settings.load(); if (AdBlocker.config.enabled) { AdBlocker.addCss(); AdBlocker.removeElements(); } // Start optimized intervals with cleanup registration const skipInterval = setInterval(AdBlocker.skipAd, AdBlocker.config.skipInterval); const removeInterval = setInterval(AdBlocker.removeElements, AdBlocker.config.removeInterval); // ✅ Register intervals in cleanupManager YouTubeUtils.cleanupManager.registerInterval(skipInterval); YouTubeUtils.cleanupManager.registerInterval(removeInterval); // Navigation handling const handleNavigation = () => { AdBlocker.state.isYouTubeShorts = location.pathname.startsWith('/shorts/'); AdBlocker.cache.lastCacheTime = 0; // Reset cache }; // Override pushState for SPA navigation const originalPushState = history.pushState; history.pushState = function () { const result = originalPushState.apply(this, arguments); setTimeout(handleNavigation, 50); return result; }; // Settings modal integration 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; } } } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(settingsObserver); // ✅ Safe observe with document.body check if (document.body) { settingsObserver.observe(document.body, { childList: true }); } else { document.addEventListener('DOMContentLoaded', () => { settingsObserver.observe(document.body, { childList: true }); }); } // ✅ Register global click listener in cleanupManager const clickHandler = e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target.dataset?.section === 'basic') { setTimeout(AdBlocker.addSettingsUI, 25); } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, { passive: true, capture: true, }); // Initial skip attempt if (AdBlocker.config.enabled) { setTimeout(AdBlocker.skipAd, 200); } }, }; // Initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', AdBlocker.init, { once: true }); } else { AdBlocker.init(); } })(); // --- MODULE: count.js --- // count (function () { 'use strict'; // Enhanced configuration with better defaults 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, // 5 minutes DEBOUNCE_DELAY: 100, STORAGE_KEY: 'youtube_channel_stats_settings', }; // Global state management const state = { overlay: null, isUpdating: false, intervalId: null, currentChannelName: null, enabled: localStorage.getItem(CONFIG.STORAGE_KEY) !== 'false', updateInterval: parseInt(localStorage.getItem('youtubeEnhancerInterval')) || 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(), }; // Utility functions const utils = { log: (message, ...args) => { console.log(`[YouTube Enhancer] ${message}`, ...args); }, warn: (message, ...args) => { console.warn(`[YouTube Enhancer] ${message}`, ...args); }, error: (message, ...args) => { console.error(`[YouTube Enhancer] ${message}`, ...args); }, // Use shared debounce from YouTubeUtils 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.OPTIONS; const FONT_LINK = CONFIG.FONT_LINK; const STATS_API_URL = CONFIG.STATS_API_URL; /** * Fetches channel data from YouTube * @param {string} url - The channel URL to fetch * @returns {Promise} The parsed channel data or null on error */ 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 || '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) { /** @this {any} */ return function () { f.apply(this, arguments); checkUrlChange(); }; })(history.pushState); history.replaceState = (function (f) { /** @this {any} */ return function () { f.apply(this, arguments); 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.appendChild(fontLink); } function initializeLocalStorage() { OPTIONS.forEach(option => { if (localStorage.getItem(`show-${option}`) === null) { localStorage.setItem(`show-${option}`, 'true'); } }); } function addStyles() { // ✅ Use StyleManager instead of createElement('style') 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 = 'Display Options'; 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 = option.charAt(0).toUpperCase() + option.slice(1); 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'; // Font family selector const fontLabel = document.createElement('label'); fontLabel.textContent = 'Font Family'; 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 = /** @type {EventTarget & HTMLSelectElement} */ (e.target); localStorage.setItem('youtubeEnhancerFontFamily', target.value); if (state.overlay) { // Only update .subscribers-number, .views-number, .videos-number state.overlay .querySelectorAll('.subscribers-number,.views-number,.videos-number') .forEach(el => { el.style.fontFamily = target.value; }); } }); // Font size slider const fontSizeLabel = document.createElement('label'); fontSizeLabel.textContent = 'Font Size'; 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 = /** @type {EventTarget & HTMLInputElement} */ (e.target); fontSizeValue.textContent = `${target.value}px`; localStorage.setItem('youtubeEnhancerFontSize', target.value); if (state.overlay) { // Only update .subscribers-number, .views-number, .videos-number state.overlay .querySelectorAll('.subscribers-number,.views-number,.videos-number') .forEach(el => { el.style.fontSize = `${target.value}px`; }); } }); // ...existing code... const intervalLabel = document.createElement('label'); intervalLabel.textContent = 'Update Interval'; 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 = /** @type {EventTarget & HTMLInputElement} */ (e.target); const newInterval = parseInt(target.value) * 1000; intervalValue.textContent = `${target.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); // ✅ Register interval in cleanupManager YouTubeUtils.cleanupManager.registerInterval(state.intervalId); } }); const opacityLabel = document.createElement('label'); opacityLabel.textContent = 'Background Opacity'; 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 = /** @type {EventTarget & HTMLInputElement} */ (e.target); const newOpacity = parseInt(target.value) / 100; opacityValue.textContent = `${target.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 createOverlay(bannerElement) { clearExistingOverlay(); if (!bannerElement) return null; 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', }); // Accessibility attributes overlay.setAttribute('role', 'region'); overlay.setAttribute('aria-label', 'YouTube Channel Statistics Overlay'); overlay.setAttribute('tabindex', '-1'); // Responsive design for mobile if (window.innerWidth <= 768) { overlay.style.flexDirection = 'column'; overlay.style.padding = '10px'; overlay.style.minHeight = '200px'; } const settingsButton = createSettingsButton(); settingsButton.setAttribute('tabindex', '0'); settingsButton.setAttribute('aria-label', 'Open settings menu'); settingsButton.setAttribute('role', 'button'); const settingsMenu = createSettingsMenu(); settingsMenu.setAttribute('aria-label', 'Statistics display settings'); settingsMenu.setAttribute('role', 'dialog'); overlay.appendChild(settingsButton); overlay.appendChild(settingsMenu); // Enhanced event handling with keyboard support 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')); } }); // Close menu when clicking outside or pressing escape const documentClickHandler = e => { const target = /** @type {EventTarget & Node} */ (e.target); if (!settingsMenu.contains(target) && !settingsButton.contains(target)) { toggleMenu(false); } }; const clickListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', documentClickHandler ); state.documentListenerKeys.add(clickListenerKey); const documentKeydownHandler = e => { if (e.key === 'Escape' && settingsMenu.classList.contains('show')) { toggleMenu(false); settingsButton.focus(); } }; const keyListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'keydown', documentKeydownHandler ); state.documentListenerKeys.add(keyListenerKey); const spinner = createSpinner(); overlay.appendChild(spinner); 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); bannerElement.appendChild(overlay); updateDisplayState(); return overlay; } function fetchWithGM(url, headers = {}) { const requestHeaders = { Accept: 'application/json', ...headers, }; // Access GM_xmlhttpRequest via window to avoid TS "Cannot find name" when d.ts isn't picked up const gm = /** @type {any} */ (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) { // Try meta tag first const metaTag = document.querySelector('meta[itemprop="channelId"]'); if (metaTag && metaTag.content) return metaTag.content; // Try URL pattern const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/); if (urlMatch && urlMatch[1]) return urlMatch[1]; // Try ytInitialData 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) { try { let retries = CONFIG.MAX_RETRIES; while (retries > 0) { try { const stats = await fetchWithGM(`${STATS_API_URL}${channelId}`, { origin: 'https://livecounts.io', referer: 'https://livecounts.io/', }); // Validate response structure if (!stats || typeof stats.followerCount === 'undefined') { throw new Error('Invalid stats response structure'); } // Cache successful response state.lastSuccessfulStats.set(channelId, { ...stats, timestamp: Date.now(), }); return stats; } catch (e) { utils.warn('Fetch attempt failed:', e.message); retries--; if (retries > 0) { // Exponential backoff for retries await new Promise(resolve => setTimeout(resolve, 1000 * (CONFIG.MAX_RETRIES - retries + 1)) ); } } } // Try to use cached data if available and recent (within 5 minutes) if (state.lastSuccessfulStats.has(channelId)) { const cached = state.lastSuccessfulStats.get(channelId); const isRecent = Date.now() - cached.timestamp < CONFIG.CACHE_DURATION; if (isRecent) { utils.log('Using cached stats for channel:', channelId); return cached; } } // Fallback: try to extract subscriber count from page const fallbackStats = { followerCount: 0, bottomOdos: [0, 0], error: true, timestamp: Date.now(), }; // Try multiple selectors for subscriber count 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 subMatch = subText.match(/[\d,\.]+[KMB]?/); if (subMatch) { const raw = subMatch[0].replace(/,/g, ''); // parse into number safely let numCount = Number(raw.replace(/[KMB]/, '')) || 0; if (raw.includes('K')) { numCount = numCount * 1000; } else if (raw.includes('M')) { numCount = numCount * 1000000; } else if (raw.includes('B')) { numCount = numCount * 1000000000; } // Ensure followerCount is a number fallbackStats.followerCount = Math.floor(numCount); utils.log('Extracted fallback subscriber count:', fallbackStats.followerCount); break; } } } return fallbackStats; } catch (error) { utils.error('Failed to fetch channel stats:', error); return { followerCount: 0, bottomOdos: [0, 0], error: true, timestamp: Date.now(), }; } } 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); // ✅ Unregister from cleanupManager if it was registered 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 updateDigits(container, newValue) { const newValueStr = newValue.toString(); const digits = []; for (let i = newValueStr.length - 1; i >= 0; i -= 3) { const start = Math.max(0, i - 2); digits.unshift(newValueStr.slice(start, i + 1)); } while (container.firstChild) { container.removeChild(container.firstChild); } for (let i = 0; i < digits.length; i++) { const group = digits[i]; for (let j = 0; j < group.length; j++) { const digitElement = createDigitElement(); digitElement.textContent = group[j]; container.appendChild(digitElement); } if (i < digits.length - 1) { container.appendChild(createCommaElement()); } } let elementIndex = 0; for (let i = 0; i < digits.length; i++) { const group = digits[i]; for (let j = 0; j < group.length; j++) { const digitElement = container.children[elementIndex]; const newDigit = parseInt(group[j]); const currentDigit = parseInt(digitElement.textContent || '0'); if (currentDigit !== newDigit) { animateDigit(digitElement, currentDigit, newDigit); } elementIndex++; } if (i < digits.length - 1) { elementIndex++; } } } 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'; } }); // Only update font size and font family for .subscribers-number, .views-number, .videos-number 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'; } async function updateOverlayContent(overlay, channelName) { if (state.isUpdating || channelName !== state.currentChannelName) return; state.isUpdating = true; try { const channelId = await fetchChannelId(channelName); const stats = await fetchChannelStats(channelId); // Check if channel changed during async operations if (channelName !== state.currentChannelName) { state.isUpdating = false; return; } if (stats.error) { 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'); return; } const updateElement = (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 = className === 'subscribers' ? state.previousStats.get(channelId).followerCount : state.previousStats.get(channelId).bottomOdos[className === 'views' ? 0 : 1]; updateDifferenceElement(differenceElement, value, previousValue); } if (labelElement) { labelElement.textContent = label; } }; updateElement('subscribers', stats.followerCount, 'Subscribers'); updateElement('views', stats.bottomOdos[0], 'Views'); updateElement('videos', stats.bottomOdos[1], 'Videos'); 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); const containers = overlay.querySelectorAll('[class$="-number"]'); containers.forEach(container => { container.textContent = '---'; }); } finally { state.isUpdating = false; } } // Add settings UI to experimental section 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 = `
Show live subscriber/views/videos overlay on channel banner
`; section.appendChild(item); item.querySelector('input').addEventListener('change', e => { const target = /** @type {EventTarget & HTMLInputElement} */ (e.target); state.enabled = target.checked; localStorage.setItem(CONFIG.STORAGE_KEY, state.enabled ? 'true' : 'false'); if (!state.enabled) { clearExistingOverlay(); } else { observePageChanges(); addNavigationListener(); setTimeout(() => { const bannerElement = document.getElementById('page-header-banner-sizer'); if (bannerElement && isChannelPage()) { addOverlay(bannerElement); } }, 100); } }); } // Observe settings modal for experimental section 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); // ✅ Safe observe with document.body check 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 = /** @type {EventTarget & HTMLElement} */ (e.target); if ( target.classList?.contains('ytp-plus-settings-nav-item') && target.dataset?.section === 'experimental' ) { setTimeout(addSettingsUI, 50); } }; const listenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', experimentalNavClickHandler, true ); state.documentListenerKeys.add(listenerKey); function addOverlay(bannerElement) { // Improved channel name extraction with better URL parsing let channelName = null; const pathname = window.location.pathname; if (pathname.startsWith('/@')) { channelName = pathname.split('/')[1].replace('@', ''); } else if (pathname.startsWith('/channel/')) { channelName = pathname.split('/')[2]; } else if (pathname.startsWith('/c/')) { channelName = pathname.split('/')[2]; } else if (pathname.startsWith('/user/')) { channelName = pathname.split('/')[2]; } // Skip if no valid channel name or already processing the same channel if (!channelName || (channelName === state.currentChannelName && state.overlay)) { return; } // Ensure banner element is properly positioned if (bannerElement && !bannerElement.style.position) { bannerElement.style.position = 'relative'; } state.currentChannelName = channelName; state.overlay = createOverlay(bannerElement); if (state.overlay) { // Clear existing interval if (state.intervalId) { clearInterval(state.intervalId); state.intervalId = null; } // Debounced update function for better performance let lastUpdateTime = 0; const debouncedUpdate = () => { const now = Date.now(); if (now - lastUpdateTime >= state.updateInterval - 100) { updateOverlayContent(state.overlay, channelName); lastUpdateTime = now; } }; // Set up interval with debouncing state.intervalId = setInterval(debouncedUpdate, state.updateInterval); // ✅ Register interval in cleanupManager YouTubeUtils.cleanupManager.registerInterval(state.intervalId); // Initial update 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 observePageChanges() { if (!state.enabled) return; // More robust banner detection with multiple fallback selectors const observer = new MutationObserver(_mutations => { // Throttle observations for better performance if (/** @type {any} */ (observer)._timeout) { YouTubeUtils.cleanupManager.unregisterTimeout(/** @type {any} */(observer)._timeout); clearTimeout(/** @type {any} */(observer)._timeout); } /** @type {any} */ (observer)._timeout = YouTubeUtils.cleanupManager.registerTimeout( setTimeout(() => { let bannerElement = document.getElementById('page-header-banner-sizer'); // Try alternative selectors if main one fails 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; } } if (bannerElement && isChannelPage()) { // Ensure banner has proper positioning if (bannerElement.style.position !== 'relative') { bannerElement.style.position = 'relative'; } addOverlay(bannerElement); } else if (!isChannelPage()) { // Clean up when not on channel page clearExistingOverlay(); state.currentChannelName = null; } }, 100) ); // Small delay to batch rapid changes }); // ✅ Safe observe with document.body check if (document.body) { observer.observe(document.body, { childList: true, subtree: true, attributes: false, // Reduce observation scope for performance }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true, attributes: false, }); }); } // Store timeout reference for cleanup /** @type {any} */ (observer)._timeout = null; // Store observer for cleanup on page unload 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()) { clearExistingOverlay(); state.currentChannelName = null; utils.log('Navigated away from channel page'); } else { const bannerElement = document.getElementById('page-header-banner-sizer'); if (bannerElement) { addOverlay(bannerElement); utils.log('Navigated to channel page'); } } }); } // Cleanup function for page unload function cleanup() { // Disconnect all observers 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 = []; } // Clear overlay and intervals clearExistingOverlay(); utils.log('Cleanup completed'); } // Register cleanup on page unload window.addEventListener('beforeunload', cleanup); init(); })(); // --- MODULE: pip.js --- // YouTube Picture-in-Picture settings (function () { 'use strict'; /** * PiP settings configuration * @type {Object} * @property {boolean} enabled - Whether PiP is enabled * @property {Object} shortcut - Keyboard shortcut configuration * @property {string} storageKey - LocalStorage key for persistence */ 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 = () => { const candidate = (typeof YouTubeUtils?.querySelector === 'function' && YouTubeUtils.querySelector('video')) || document.querySelector('video'); if (candidate && candidate.tagName && candidate.tagName.toLowerCase() === 'video') { return /** @type {HTMLVideoElement} */ (candidate); } return null; }; const waitForMetadata = video => { if (!video) return Promise.reject(new Error('No video element available')); 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('Video metadata failed to load')); }; let timeoutId = setTimeout(() => { if (settled) return; settled = true; cleanup(); reject(new Error('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; } }; /** * Load settings from localStorage * @returns {void} */ const loadSettings = () => { try { const saved = localStorage.getItem(pipSettings.storageKey); if (saved) Object.assign(pipSettings, JSON.parse(saved)); } catch (e) { console.error('Error loading PiP settings:', e); } }; /** * Save settings to localStorage * @returns {void} */ const saveSettings = () => { try { localStorage.setItem(pipSettings.storageKey, JSON.stringify(pipSettings)); } catch (e) { console.error('Error saving PiP settings:', e); } }; /** * Get current PiP element as HTMLVideoElement when available * @returns {HTMLVideoElement|null} */ const getCurrentPiPElement = () => { const current = document.pictureInPictureElement; if (current && typeof current === 'object' && 'tagName' in current) { const tag = /** @type {{ tagName?: string }} */ (current).tagName; if (typeof tag === 'string' && tag.toLowerCase() === 'video') { return /** @type {HTMLVideoElement} */ (/** @type {unknown} */ (current)); } } return null; }; /** * Toggle Picture-in-Picture mode * @param {HTMLVideoElement} video - The video element * @returns {Promise} */ 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); } }; /** * Add PiP settings UI to advanced settings modal * @returns {void} */ const addPipSettingsToModal = () => { // ✅ Use cached querySelector const advancedSection = YouTubeUtils.querySelector( '.ytp-plus-settings-section[data-section="advanced"]' ); if (!advancedSection || YouTubeUtils.querySelector('.pip-settings-item')) return; // Add styles if they don't exist // ✅ Use StyleManager instead of createElement('style') 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); } // Enable/disable toggle const enableItem = document.createElement('div'); enableItem.className = 'ytp-plus-settings-item pip-settings-item'; enableItem.innerHTML = `
Add Picture-in-Picture functionality with keyboard shortcut
`; advancedSection.appendChild(enableItem); // Shortcut settings const shortcutItem = document.createElement('div'); shortcutItem.className = 'ytp-plus-settings-item pip-shortcut-item'; shortcutItem.style.display = pipSettings.enabled ? 'flex' : 'none'; const { ctrlKey, altKey, shiftKey } = pipSettings.shortcut; const modifierValue = ctrlKey && altKey && shiftKey ? 'ctrl+alt+shift' : ctrlKey && altKey ? 'ctrl+alt' : ctrlKey && shiftKey ? 'ctrl+shift' : altKey && shiftKey ? 'alt+shift' : ctrlKey ? 'ctrl' : altKey ? 'alt' : shiftKey ? 'shift' : 'none'; shortcutItem.innerHTML = `
Customize keyboard combination to toggle PiP mode
+
`; advancedSection.appendChild(shortcutItem); // Event listeners document.getElementById('pip-enable-checkbox').addEventListener('change', e => { const target = /** @type {EventTarget & HTMLInputElement} */ (e.target); pipSettings.enabled = target.checked; shortcutItem.style.display = pipSettings.enabled ? 'flex' : 'none'; saveSettings(); }); document.getElementById('pip-modifier-combo').addEventListener('change', e => { const target = /** @type {EventTarget & HTMLSelectElement} */ (e.target); const value = target.value; pipSettings.shortcut.ctrlKey = value.includes('ctrl'); pipSettings.shortcut.altKey = value.includes('alt'); pipSettings.shortcut.shiftKey = value.includes('shift'); saveSettings(); }); document.getElementById('pip-key').addEventListener('input', e => { const target = /** @type {EventTarget & HTMLInputElement} */ (e.target); if (target.value) { pipSettings.shortcut.key = target.value.toUpperCase(); saveSettings(); } }); document.getElementById('pip-key').addEventListener('keydown', e => e.stopPropagation()); }; // Initialize loadSettings(); // Event listeners 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 ) { // ✅ Use cached querySelector and guard by tagName to avoid referencing DOM lib types in TS const video = getVideoElement(); if (video) { void 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(() => { // If resume fails we reset the session flag to avoid loops 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 }); }); // DOM observers for the settings modal 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); }); // Check for section changes - ✅ Use cached querySelector if (YouTubeUtils.querySelector('.ytp-plus-settings-nav-item[data-section="advanced"].active')) { // If advanced section is active and our settings aren't there yet, add them if (!YouTubeUtils.querySelector('.pip-settings-item')) { setTimeout(addPipSettingsToModal, 50); } } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(observer); // ✅ Safe observe with document.body check if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); } // ✅ Register global click listener in cleanupManager const clickHandler = e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); 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); })(); // --- MODULE: timecode.js --- // YouTube Timecode Panel (function () { 'use strict'; // Early exit for embeds to prevent duplicate panels - ✅ Use cached querySelector if (window.location.hostname !== 'www.youtube.com' || window.frameElement) { return; } // Prevent multiple initializations if (window._timecodeModuleInitialized) return; window._timecodeModuleInitialized = true; // Configuration 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, }; // State management 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); }; // Utilities const loadSettings = () => { try { const saved = localStorage.getItem(config.storageKey); if (saved) Object.assign(config, JSON.parse(saved)); } catch { } }; const saveSettings = () => { try { localStorage.setItem(config.storageKey, JSON.stringify(config)); } catch { } }; const clampPanelPosition = (panel, left, top) => { if (!panel) return { left: 0, top: 0 }; const rect = panel.getBoundingClientRect(); const width = rect.width || panel.offsetWidth || 0; const height = rect.height || panel.offsetHeight || 0; const maxLeft = Math.max(0, window.innerWidth - width); const maxTop = Math.max(0, window.innerHeight - height); return { left: Math.min(Math.max(0, left), maxLeft), top: Math.min(Math.max(0, top), maxTop), }; }; const savePanelPosition = (left, top) => { config.panelPosition = { left, top }; saveSettings(); }; 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 }); }; // Time utilities const formatTime = seconds => { if (isNaN(seconds)) return '00:00'; seconds = Math.round(seconds); const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 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 => { if (!timeStr) return null; const str = timeStr.trim(); // Handle HH:MM:SS format let match = str.match(/^(\d+):(\d{1,2}):(\d{2})$/); if (match) { const [, h, m, s] = match.map(Number); return m < 60 && s < 60 ? h * 3600 + m * 60 + s : null; } // Handle MM:SS format match = str.match(/^(\d{1,2}):(\d{2})$/); if (match) { const [, m, s] = match.map(Number); return m < 60 && s < 60 ? m * 60 + s : null; } return null; }; // Timecode extraction const extractTimecodes = text => { if (!text) return []; 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; while ((match = pattern.exec(text)) !== null) { const time = parseTime(match[1]); if (time !== null && !seen.has(time)) { seen.add(time); const label = (match[2] || formatTime(time)) .trim() .replace(/^\d+[\.\)]\s*/, '') .substring(0, 100); if (label) { timecodes.push({ time, label, originalText: match[1] }); } } } } return timecodes.sort((a, b) => a.time - b.time); }; 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 expandDescriptionIfNeeded = async () => { for (const selector of DESCRIPTION_EXPANDERS) { const button = document.querySelector(selector); if (!button) continue; const ariaExpanded = button.getAttribute('aria-expanded'); if (ariaExpanded === 'true') return false; const ariaLabel = button.getAttribute('aria-label')?.toLowerCase(); if (ariaLabel && ariaLabel.includes('less')) return false; if (button.offsetParent !== null) { try { /** @type {HTMLElement} */ (button).click(); await sleep(400); return true; } catch (error) { console.warn('[Timecode] Failed to click expand button:', error); } } } const inlineExpander = document.querySelector('ytd-text-inline-expander[collapsed]'); if (inlineExpander) { try { inlineExpander.removeAttribute('collapsed'); } catch (error) { YouTubeUtils.logError('TimecodePanel', 'Failed to expand description', error); } await sleep(300); return true; } return false; }; 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 { // Continue trying } 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'); // Detection 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); } }); } // Get native chapters 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(`Найдено таймкодов: ${result.length}`); } else { updateTimecodePanel([]); showNotification('Таймкоды не найдены'); } } catch (error) { YouTubeUtils.logError('TimecodePanel', 'Reload failed', error); showNotification('Ошибка при обновлении таймкодов'); } finally { if (button) { button.disabled = false; button.classList.remove('loading'); } state.isReloading = false; } }; 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(); items.forEach(item => { // Попробуем разные способы извлечения времени и заголовка const timeSelectors = ['.time-info', '.timestamp', '#time', 'span[id*="time"]']; const titleSelectors = ['.marker-title', '.chapter-title', '#details', 'h4', '.title']; let timeText = null; for (const sel of timeSelectors) { const el = item.querySelector(sel); if (el?.textContent) { timeText = el.textContent; break; } } let titleText = null; for (const sel of titleSelectors) { const el = item.querySelector(sel); if (el?.textContent) { titleText = el.textContent; break; } } if (timeText) { const time = parseTime(timeText.trim()); if (time !== null) { // Очищаем заголовок от лишних пробелов и переносов строк const cleanTitle = titleText?.trim().replace(/\s+/g, ' ') || formatTime(time); chapters.set(time.toString(), { time, label: cleanTitle, isChapter: true, }); } } }); const result = Array.from(chapters.values()).sort((a, b) => a.time - b.time); return result; }; // Settings panel const addTimecodePanelSettings = () => { // ✅ Use cached querySelector const advancedSection = YouTubeUtils.querySelector( '.ytp-plus-settings-section[data-section="advanced"]' ); if (!advancedSection || YouTubeUtils.querySelector('.timecode-settings-item')) return; const { ctrlKey, altKey, shiftKey } = config.shortcut; const modifierValue = [ ctrlKey && altKey && shiftKey && 'ctrl+alt+shift', ctrlKey && altKey && 'ctrl+alt', ctrlKey && shiftKey && 'ctrl+shift', altKey && shiftKey && 'alt+shift', ctrlKey && 'ctrl', altKey && 'alt', shiftKey && 'shift', ].find(Boolean) || 'none'; const enableDiv = document.createElement('div'); enableDiv.className = 'ytp-plus-settings-item timecode-settings-item'; enableDiv.innerHTML = `
Enable video timecode/chapter panel with quick navigation
`; 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 = `
Customize keyboard combination to toggle Timecode Panel
+
`; advancedSection.append(enableDiv, shortcutDiv); // Event listeners advancedSection.addEventListener('change', e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target.matches && target.matches('.ytp-plus-settings-checkbox[data-setting="enabled"]')) { config.enabled = /** @type {HTMLInputElement} */ (target).checked; shortcutDiv.style.display = config.enabled ? 'flex' : 'none'; toggleTimecodePanel(config.enabled); saveSettings(); } }); document.getElementById('timecode-modifier-combo')?.addEventListener('change', e => { const target = /** @type {EventTarget & HTMLSelectElement} */ (e.target); const value = target.value; config.shortcut.ctrlKey = value.includes('ctrl'); config.shortcut.altKey = value.includes('alt'); config.shortcut.shiftKey = value.includes('shift'); saveSettings(); }); document.getElementById('timecode-key')?.addEventListener('input', e => { const target = /** @type {EventTarget & HTMLInputElement} */ (e.target); if (target.value) { config.shortcut.key = target.value.toUpperCase(); saveSettings(); } }); }; // CSS const insertTimecodeStyles = () => { if (document.getElementById('timecode-panel-styles')) return; // ✅ Use StyleManager instead of createElement('style') const styles = ` #timecode-panel{position:fixed;right:20px;top:80px;background:rgba(34,34,34,.9);border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.4);width:250px;max-height:70vh;z-index:9999;color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.1);transition:transform .3s,opacity .3s;overflow:hidden;display:flex;flex-direction:column} #timecode-panel.hidden{transform:translateX(270px);opacity:0;pointer-events:none} #timecode-panel.auto-tracking{border-color:rgba(255,0,0,.5)} #timecode-header{display:flex;justify-content:space-between;align-items:center;padding:12px;border-bottom:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.3);cursor:move} #timecode-title{font-weight:500;margin:0;font-size:14px;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:0 0;border:none;color:rgba(255,255,255,.7);cursor:pointer;width:24px;height:24px;padding:0;display:flex;align-items:center;justify-content:center;transition:color .2s} #timecode-reload:hover,#timecode-close:hover{color:#fff} #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:8px 12px;display:flex;align-items:center;cursor:pointer;transition:background-color .2s;border-left:3px solid transparent;position:relative} .timecode-item:hover{background:rgba(255,255,255,.1)} .timecode-item:hover .timecode-actions{opacity:1} .timecode-item.active{background:rgba(255,0,0,.25);border-left-color:red} .timecode-item.active.pulse{animation:pulse .8s ease-out} .timecode-item.editing{background:rgba(255,255,0,.15);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:10px;border-top:1px solid rgba(255,255,255,.1);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:8px;border-top:1px solid rgba(255,255,255,.1);display:flex;gap:8px;background:rgba(0,0,0,.2)} #timecode-actions button{padding:6px 10px;border:none;border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s;background:rgba(255,255,255,.2);color:#fff} #timecode-actions button:hover{background:rgba(255,255,255,.3)} #timecode-track-toggle.active{background:#ff4444!important} `; YouTubeUtils.StyleManager.add('timecode-panel-styles', styles); }; // Panel creation const createTimecodePanel = () => { if (state.dom.panel) return state.dom.panel; // Remove any existing panels (for redundancy) 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 = `

Timecodes

No timecodes found
Click + to add current time
`; // Cache DOM elements 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'), }; // Event delegation panel.addEventListener('click', handlePanelClick); makeDraggable(panel); document.body.appendChild(panel); applySavedPanelPosition(panel); return panel; }; // Event handling const handlePanelClick = e => { const { target } = e; const item = target.closest('.timecode-item'); const reloadButton = target.id === 'timecode-reload' ? target : target.closest('#timecode-reload'); if (reloadButton) { e.preventDefault(); reloadTimecodes(reloadButton); return; } if (target.id === 'timecode-close') { toggleTimecodePanel(false); } else if (target.id === 'timecode-add-btn') { // ✅ Use cached querySelector 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 ? 'Tracking' : '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.action; const index = parseInt(target.closest('.timecode-item').dataset.index); 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)) { /** @type {HTMLVideoElement} */ (video).currentTime = time; if (video.paused) video.play(); updateActiveItem(item); } } }; // Edit timecode const editTimecode = index => { const timecodes = getCurrentTimecodes(); if (index < 0 || index >= timecodes.length) return; const timecode = timecodes[index]; state.editingIndex = index; // Update item appearance const item = state.dom.list.querySelector(`.timecode-item[data-index="${index}"]`); if (item) { item.classList.add('editing'); // Hide other editing items state.dom.list.querySelectorAll('.timecode-item.editing').forEach(el => { if (el !== item) el.classList.remove('editing'); }); } showTimecodeForm(timecode.time, timecode.label); }; // Delete timecode const deleteTimecode = index => { const timecodes = getCurrentTimecodes(); if (index < 0 || index >= timecodes.length) return; const timecode = timecodes[index]; // Don't allow deletion of native YouTube chapters if (timecode.isChapter && !timecode.isUserAdded) { showNotification('Cannot delete YouTube chapters'); return; } // Confirm deletion if (!confirm(`Delete timecode "${timecode.label}"?`)) return; timecodes.splice(index, 1); updateTimecodePanel(timecodes); saveTimecodesToStorage(timecodes); showNotification('Timecode deleted'); }; // Form handling 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; // Remove editing class from all items 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('Invalid time format'); return; } const timecodes = getCurrentTimecodes(); const newTimecode = { time, label: labelValue || formatTime(time), isUserAdded: true, isChapter: false, }; if (state.editingIndex !== null) { // Editing existing timecode const oldTimecode = timecodes[state.editingIndex]; if (oldTimecode.isChapter && !oldTimecode.isUserAdded) { showNotification('Cannot edit YouTube chapters'); hideTimecodeForm(); return; } timecodes[state.editingIndex] = { ...oldTimecode, ...newTimecode }; showNotification('Timecode updated'); } else { // Adding new timecode timecodes.push(newTimecode); showNotification('Timecode added'); } const sorted = timecodes.sort((a, b) => a.time - b.time); updateTimecodePanel(sorted); saveTimecodesToStorage(sorted); hideTimecodeForm(); }; // Export const exportTimecodes = () => { const timecodes = getCurrentTimecodes(); if (!timecodes.length) { showNotification('No timecodes to export'); return; } const exportBtn = state.dom.panel?.querySelector('#timecode-export-btn'); if (exportBtn) { exportBtn.textContent = 'Copied!'; exportBtn.style.backgroundColor = 'rgba(0,220,0,0.8)'; setTimeout(() => { exportBtn.textContent = '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('Timecodes copied to clipboard'); }); } }; // Panel updates 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); } }; // Tracking const startTracking = () => { if (state.trackingId) return; const track = () => { try { const video = document.querySelector('video'); const { panel, currentTime, list } = state.dom; // Stop tracking if essential elements are missing or panel is hidden if (!video || !panel || panel.classList.contains('hidden') || !config.autoTrackPlayback) { if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } return; } // Update current time display if (currentTime && !isNaN(video.currentTime)) { currentTime.textContent = formatTime(video.currentTime); } // Update active item const items = list?.querySelectorAll('.timecode-item'); if (items?.length) { 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 (video.currentTime >= time) { activeIndex = i; } else if (nextIndex === -1) { nextIndex = i; } } // Update active state if (state.activeIndex !== activeIndex) { // Remove previous active state if (state.activeIndex !== null && state.activeIndex >= 0 && items[state.activeIndex]) { items[state.activeIndex].classList.remove('active'); } // Set new active state if (activeIndex >= 0 && items[activeIndex]) { items[activeIndex].classList.add('active'); try { items[activeIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { // Fallback for browsers that don't support smooth scrolling items[activeIndex].scrollIntoView(false); } } state.activeIndex = activeIndex; } // Update progress bar if (activeIndex >= 0 && nextIndex >= 0 && items[activeIndex]) { const currentTimeData = items[activeIndex].dataset.time; const nextTimeData = items[nextIndex].dataset.time; if (currentTimeData && nextTimeData) { const current = parseFloat(currentTimeData); const next = parseFloat(nextTimeData); if (!isNaN(current) && !isNaN(next) && next > current) { const progress = ((video.currentTime - 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}%`; } } } } } // Continue tracking if enabled if (config.autoTrackPlayback) { state.trackingId = requestAnimationFrame(track); } } catch (error) { console.warn('Timecode tracking error:', error); // Stop tracking on error to prevent infinite error loops if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } } }; state.trackingId = requestAnimationFrame(track); }; // Stop tracking function const stopTracking = () => { if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } }; // Drag functionality 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); }; // ✅ Register the mousedown listener for cleanup YouTubeUtils.cleanupManager.registerListener(header, 'mousedown', mouseDownHandler); }; // Storage 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); }; // Toggle panel const toggleTimecodePanel = show => { // Close any existing panels first (cleanup) document.querySelectorAll('#timecode-panel').forEach(panel => { if (panel !== state.dom.panel) panel.remove(); }); const panel = state.dom.panel || createTimecodePanel(); if (show === undefined) show = panel.classList.contains('hidden'); panel.classList.toggle('hidden', !show); if (show) { applySavedPanelPosition(panel); const saved = loadTimecodesFromStorage(); if (saved?.length) { updateTimecodePanel(saved); } else if (config.autoDetect) { detectTimecodes().catch(err => console.error('[Timecode] Detection failed:', err)); } if (config.autoTrackPlayback) startTracking(); } else if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } }; // Navigation handling const setupNavigation = () => { let currentVideoId = new URLSearchParams(window.location.search).get('v'); const handleNavigationChange = () => { const newVideoId = new URLSearchParams(window.location.search).get('v'); if (newVideoId === currentVideoId || window.location.pathname !== '/watch') return; currentVideoId = newVideoId; state.activeIndex = null; state.editingIndex = null; state.timecodes.clear(); if (config.enabled && state.dom.panel && !state.dom.panel.classList.contains('hidden')) { const saved = loadTimecodesFromStorage(); if (saved?.length) { updateTimecodePanel(saved); } else if (config.autoDetect) { setTimeout( () => detectTimecodes().catch(err => console.error('[Timecode] Detection failed:', err)), 500 ); } if (config.autoTrackPlayback) startTracking(); } }; document.addEventListener('yt-navigate-finish', handleNavigationChange); // Also watch for URL changes using MutationObserver as a fallback const observer = new MutationObserver(() => { const newVideoId = new URLSearchParams(window.location.search).get('v'); if (newVideoId !== currentVideoId) { handleNavigationChange(); } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(observer); // ✅ Safe observe with document.body check if (document.body) { observer.observe(document.body, { subtree: true, childList: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { subtree: true, childList: true }); }); } }; // Keyboard shortcuts const setupKeyboard = () => { document.addEventListener('keydown', e => { // ✅ Проверяем, включена ли функция в настройках if (!config.enabled) return; const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target.matches && target.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(); } }); }; // Cleanup on unload const cleanup = () => { stopTracking(); if (state.dom.panel) { state.dom.panel.remove(); state.dom.panel = null; } }; // Initialize 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(); // Settings modal observer 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); } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(observer); // ✅ Safe observe with document.body check if (document.body) { observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'], }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'], }); }); } // ✅ Register global click listener in cleanupManager const clickHandler = e => { if ( /** @type {HTMLElement} */ (e.target).classList?.contains('ytp-plus-settings-nav-item') && /** @type {HTMLElement} */ (e.target).dataset.section === 'advanced' ) { setTimeout(addTimecodePanelSettings, 50); } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, true); if (config.enabled) { createTimecodePanel(); if (!state.resizeListenerKey) { 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 saved = loadTimecodesFromStorage(); if (saved?.length) { updateTimecodePanel(saved); } else if (config.autoDetect) { setTimeout( () => detectTimecodes().catch(err => console.error('[Timecode] Detection failed:', err)), 1500 ); } if (config.autoTrackPlayback) startTracking(); } }; // Start on document ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } // Cleanup on beforeunload window.addEventListener('beforeunload', cleanup); })(); // --- MODULE: shorts.js --- // Shorts Keyboard controls (function () { 'use strict'; // Configuration const config = { enabled: true, shortcuts: { seekBackward: { key: 'ArrowLeft', description: 'Seek backward 5s' }, seekForward: { key: 'ArrowRight', description: 'Seek forward 5s' }, volumeUp: { key: '+', description: 'Volume up' }, volumeDown: { key: '-', description: 'Volume down' }, mute: { key: 'm', description: 'Mute/Unmute' }, showHelp: { key: '?', description: 'Show/Hide help', editable: false }, }, storageKey: 'youtube_shorts_keyboard_settings', }; // State management const state = { helpVisible: false, lastAction: null, actionTimeout: null, editingShortcut: null, cachedVideo: null, lastVideoCheck: 0, }; // Optimized video selector with caching 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) { // ✅ Use cached querySelector const video = YouTubeUtils.querySelector(selector); if (video) { state.cachedVideo = video; state.lastVideoCheck = now; return video; } } state.cachedVideo = null; return null; }; })(); // Optimized utilities 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) Object.assign(config, JSON.parse(saved)); } catch { } }, saveSettings: () => { try { localStorage.setItem( config.storageKey, JSON.stringify({ enabled: config.enabled, shortcuts: config.shortcuts, }) ); } catch { } }, getDefaultShortcuts: () => ({ seekBackward: { key: 'ArrowLeft', description: 'Seek backward 5s' }, seekForward: { key: 'ArrowRight', description: 'Seek forward 5s' }, volumeUp: { key: '+', description: 'Volume up' }, volumeDown: { key: '-', description: 'Volume down' }, mute: { key: 'm', description: 'Mute/Unmute' }, showHelp: { key: '?', description: 'Show/Hide help', editable: false }, }), }; // Optimized feedback system 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(255,255,255,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); }, }; })(); // Optimized actions 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'); } }, 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(); if (video) { video.muted = !video.muted; feedback.show(video.muted ? '🔇' : '🔊'); } }, showHelp: () => helpPanel.toggle(), }; // Help panel system 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 = `

Keyboard Shortcuts

${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('Reset all shortcuts?')) { config.shortcuts = utils.getDefaultShortcuts(); utils.saveSettings(); feedback.show('Shortcuts reset'); 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; } }, }; })(); // Shortcut editing 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 = `

Edit: ${config.shortcuts[actionKey].description}

Press any key to set as new shortcut

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(`Key "${e.key}" already used`); return; } config.shortcuts[actionKey].key = e.key; utils.saveSettings(); feedback.show('Shortcut updated'); helpPanel.refresh(); cleanup(); }; const cleanup = () => { document.removeEventListener('keydown', handleKey, true); dialog.remove(); state.editingShortcut = null; }; dialog.querySelector('.shortcut-cancel').onclick = cleanup; dialog.onclick = e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target === dialog) cleanup(); }; document.addEventListener('keydown', handleKey, true); }; // Optimized styles with glassmorphism const addStyles = () => { if (document.getElementById('shorts-keyboard-styles')) return; // ✅ Use StyleManager instead of createElement('style') 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); }; // Main keyboard handler const handleKeydown = e => { if ( !config.enabled || !utils.isInShortsPage() || utils.isInputFocused() || state.editingShortcut ) { return; } let key = e.key; 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](); } }; // Initialize const init = () => { utils.loadSettings(); addStyles(); // ✅ Register listeners in cleanupManager YouTubeUtils.cleanupManager.registerListener(document, 'keydown', handleKeydown, true); const clickHandler = e => { const target = /** @type {EventTarget & HTMLElement} */ (e.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); } })(); // --- MODULE: stats.js --- // Stats button and menu (function () { 'use strict'; // Glassmorphism styles for stats button and menu 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.15);box-shadow:0 8px 32px rgba(0,0,0,.18);backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);border:1px solid rgba(255,255,255,.18);transition:background .2s} html[dark] .videoStats{background:rgba(34,34,34,0.7);border:1px solid rgba(255,255,255,.18)}html:not([dark]) .videoStats{background:rgba(255,255,255,0.15);border:1px solid rgba(0,0,0,.08)}.videoStats:hover{background:rgba(255,255,255,0.22)}.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.15);box-shadow:0 8px 32px rgba(0,0,0,.18);backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);border:1px solid rgba(255,255,255,.18);transition:background .3s}html[dark] .shortsStats{background:rgba(34,34,34,0.7);border:1px solid rgba(255,255,255,.18)}html:not([dark]) .shortsStats{background:rgba(255,255,255,0.15);border:1px solid rgba(0,0,0,.08)} .shortsStats:hover{background:rgba(255,255,255,0.22)}.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:rgba(0,0,0,0.7);z-index:99999;display:flex;align-items:center;justify-content:center;animation:fadeInModal 0.2s;backdrop-filter:blur(18px) saturate(180%);-webkit-backdrop-filter:blur(18px) saturate(180%)}.stats-modal-content{background:rgba(34,34,34,0.95);border-radius:18px;box-shadow:0 8px 32px rgba(0,0,0,.2);max-width:75vw;max-height:90vh;overflow:auto;position:relative;padding:24px 0 0 0;display:flex;flex-direction:column;align-items:center;animation:scaleInModal 0.2s;border:1px solid rgba(255,255,255,.2);backdrop-filter:blur(18px) saturate(180%);-webkit-backdrop-filter:blur(18px) saturate(180%)}html[dark] .stats-modal-content{background:rgba(34,34,34,0.95)}html:not([dark]) .stats-modal-content{background:#fff;color:#222}.stats-modal-close{position:absolute;top:12px;right:18px;background:transparent;color:#fff;border:none;font-size:28px;line-height:1;width:36px;height:36px;cursor:pointer;transition:background 0.2s;z-index:2;display:flex;align-items:center;justify-content:center}.stats-modal-close:hover{color:#ff4444;transform:rotate(90deg) scale(1.25)}.stats-modal-iframe{width:72vw;height:70vh;box-shadow:0 8px 32px rgba(0,0,0,.2);background:#222;border:1px solid rgba(255,255,255,.2)}.stats-modal-title{font-size:18px;font-weight:600;color:#fff;margin-bottom:10px;text-align:center;text-shadow:0 2px 8px rgba(0,0,0,0.15)}html:not([dark]) .stats-modal-title{color:#222}@keyframes fadeInModal{from{opacity:0}to{opacity:1}}@keyframes scaleInModal{from{transform:scale(0.95)}to{transform:scale(1)}} `; // Settings state 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, }; function addStyles() { if (!document.querySelector('#youtube-enhancer-styles')) { // ✅ Use StyleManager instead of createElement('style') YouTubeUtils.StyleManager.add('youtube-enhancer-styles', styles); } } function getCurrentVideoUrl() { const url = window.location.href; const urlParams = new URLSearchParams(window.location.search); const videoId = urlParams.get('v'); if (videoId) { return `https://www.youtube.com/watch?v=${videoId}`; } const shortsMatch = url.match(/\/shorts\/([^?]+)/); if (shortsMatch) { return `https://www.youtube.com/shorts/${shortsMatch[1]}`; } return null; } function getChannelIdentifier() { 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]; } return identifier; } async function checkChannelTabs(url) { if (isChecking) return; isChecking = true; try { const response = await fetch(url, { credentials: 'same-origin', }); if (!response.ok) { isChecking = false; return; } const html = await response.text(); const match = html.match(/var ytInitialData = (.+?);<\/script>/); if (!match || !match[1]) { isChecking = false; return; } const data = JSON.parse(match[1]); const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || []; let hasStreams = false; let hasShorts = false; tabs.forEach(tab => { const tabUrl = tab?.tabRenderer?.endpoint?.commandMetadata?.webCommandMetadata?.url; if (tabUrl) { if (/\/streams$/.test(tabUrl)) hasStreams = true; if (/\/shorts$/.test(tabUrl)) hasShorts = true; } }); channelFeatures = { hasStreams: hasStreams, hasShorts: hasShorts, }; const existingMenu = document.querySelector('.stats-menu-container'); if (existingMenu) { existingMenu.remove(); createStatsMenu(); } } catch { } finally { isChecking = false; } } function isChannelPage(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 !== previousUrl) { previousUrl = currentUrl; if (isChannelPage(currentUrl)) { setTimeout(() => checkChannelTabs(currentUrl), 500); } } } function createStatsIcon(isShorts = false) { const icon = document.createElement('div'); icon.className = isShorts ? 'shortsStats' : 'videoStats'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 512 512'); 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); icon.appendChild(svg); icon.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); const videoUrl = getCurrentVideoUrl(); if (videoUrl) { openStatsModal( `https://stats.afkarxyz.fun/?directVideo=${encodeURIComponent(videoUrl)}`, 'Video Stats' ); } }); return icon; } function insertIconForRegularVideo() { if (!statsButtonEnabled) return; const targetSelector = '#owner'; const target = document.querySelector(targetSelector); if (target && !document.querySelector('.videoStats')) { const statsIcon = createStatsIcon(); target.appendChild(statsIcon); } } function insertIconForShorts() { if (!statsButtonEnabled) return false; const likeButtonContainer = document.querySelector( 'ytd-reel-video-renderer[is-active] #like-button' ); if (likeButtonContainer && !document.querySelector('.shortsStats')) { const iconDiv = createStatsIcon(true); likeButtonContainer.parentNode.insertBefore(iconDiv, likeButtonContainer); return true; } return false; } 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', function (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; } function openStatsModal(url, titleText) { document.querySelectorAll('.stats-modal-overlay').forEach(m => m.remove()); const overlay = document.createElement('div'); overlay.className = 'stats-modal-overlay'; const content = document.createElement('div'); content.className = 'stats-modal-content'; const closeBtn = document.createElement('button'); closeBtn.className = 'stats-modal-close'; closeBtn.innerHTML = ` `; closeBtn.title = 'Close'; closeBtn.onclick = () => overlay.remove(); const title = document.createElement('div'); title.className = 'stats-modal-title'; title.textContent = titleText || 'Stats'; const iframe = document.createElement('iframe'); iframe.className = 'stats-modal-iframe'; iframe.src = url; iframe.setAttribute('frameborder', '0'); iframe.setAttribute('allowfullscreen', 'true'); iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups'); iframe.style.background = '#222'; content.append(closeBtn, title, iframe); overlay.appendChild(content); overlay.onclick = e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if (target === overlay) overlay.remove(); }; document.addEventListener( 'keydown', function escHandler(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler, true); } }, true ); document.body.appendChild(overlay); } function createStatsMenu() { if (!statsButtonEnabled) return; if (document.querySelector('.stats-menu-container')) { return; } 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', '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 = '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( '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(`https://stats.afkarxyz.fun/?directChannel=${channelId}`, 'Channel Stats'); } } ); 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( '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(`https://stats.afkarxyz.fun/?directStream=${channelId}`, 'Live Stats'); } } ); 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( '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(`https://stats.afkarxyz.fun/?directShorts=${channelId}`, 'Shorts Stats'); } } ); 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; const isShorts = window.location.pathname.includes('/shorts/'); if (isShorts) { const shortsObserver = new MutationObserver((_mutations, observer) => { if (insertIconForShorts()) { observer.disconnect(); } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(shortsObserver); const shortsContainer = document.querySelector('ytd-shorts'); if (shortsContainer) { shortsObserver.observe(shortsContainer, { childList: true, subtree: true, }); insertIconForShorts(); } } else if (getCurrentVideoUrl()) { insertIconForRegularVideo(); } } 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 = `
Show statistics button on videos and channel menu for quick access to statistics
`; section.appendChild(item); item.querySelector('input').addEventListener('change', e => { const target = /** @type {EventTarget & HTMLInputElement} */ (e.target); statsButtonEnabled = target.checked; localStorage.setItem(SETTINGS_KEY, statsButtonEnabled ? 'true' : 'false'); // Remove all stats buttons and menus document .querySelectorAll('.videoStats,.shortsStats,.stats-menu-container') .forEach(el => el.remove()); if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } }); } // Observe settings modal for experimental section 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); } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(settingsObserver); // ✅ Safe observe with document.body check 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 = /** @type {EventTarget & HTMLElement} */ (e.target); if ( target.classList?.contains('ytp-plus-settings-nav-item') && target.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) { /** @this {any} */ return function () { const fAny = /** @type {any} */ (f); const result = fAny.apply(this, arguments); checkUrlChange(); return result; }; })(history.pushState); history.replaceState = (function (f) { /** @this {any} */ return function () { const fAny = /** @type {any} */ (f); const result = fAny.apply(this, arguments); 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(); } } } }); // ✅ Safe observe with document.body check 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', function (event) { const ev = /** @type {CustomEvent} */ (event); if (ev.detail && ev.detail.actionName === 'yt-reload-continuation-items-command') { if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } } }); })(); // --- MODULE: thumbnail.js --- (function () { 'use strict'; function extractVideoId(thumbnailSrc) { const match = thumbnailSrc.match(/\/vi\/([^\/]+)\//); return match ? match[1] : null; } function extractShortsId(href) { const match = href.match(/\/shorts\/([^\/\?]+)/); return match ? match[1] : null; } async function checkImageExists(url) { try { const corsTest = await fetch(url, { method: 'HEAD' }).catch(() => null); if (corsTest) { return corsTest.ok; } else { return true; } } catch { return new Promise(resolve => { const img = document.createElement('img'); img.style.display = 'none'; const timeout = setTimeout(() => { document.body.removeChild(img); resolve(false); }, 2000); img.onload = () => { clearTimeout(timeout); document.body.removeChild(img); resolve(true); }; img.onerror = () => { clearTimeout(timeout); document.body.removeChild(img); resolve(false); }; document.body.appendChild(img); img.src = url; }); } } 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.appendChild(style); } return spinner; } async function openThumbnail(videoId, isShorts, overlayElement) { if (isShorts) { const originalSvg = overlayElement.querySelector('svg'); const spinner = createSpinner(); overlayElement.replaceChild(spinner, originalSvg); try { const oardefaultUrl = `https://i.ytimg.com/vi/${videoId}/oardefault.jpg`; const isOarDefaultAvailable = await checkImageExists(oardefaultUrl); if (isOarDefaultAvailable) { showImageModal(oardefaultUrl); } else { showImageModal(`https://i.ytimg.com/vi/${videoId}/oar2.jpg`); } } finally { overlayElement.replaceChild(originalSvg, spinner); } } else { // For non-shorts thumbnails: capture original svg, show spinner while checking const originalSvg = overlayElement.querySelector('svg'); const spinner = createSpinner(); // replace original with spinner overlayElement.replaceChild(spinner, originalSvg); try { const maxresdefaultUrl = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`; const isMaxResAvailable = await checkImageExists(maxresdefaultUrl); if (isMaxResAvailable) { showImageModal(maxresdefaultUrl); } else { showImageModal(`https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`); } } finally { // restore original svg try { overlayElement.replaceChild(originalSvg, spinner); } catch { // fallback: remove spinner if original not found if (spinner && spinner.parentNode) spinner.parentNode.removeChild(spinner); } } } } // Inject CSS styles via StyleManager (if available) to match base theme (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-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.7); z-index: 99999; display: flex; align-items: center; justify-content: center; animation: fadeInModal 0.2s; backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); } .thumbnail-modal-content { background: var(--thumbnail-modal-bg); border-radius: 18px; box-shadow: var(--thumbnail-glass-shadow); max-width: 75vw; max-height: 90vh; overflow: auto; position: relative; padding: 24px 0 16px 0; display: flex; flex-direction: column; align-items: center; animation: scaleInModal 0.2s; border: 1px solid var(--thumbnail-glass-border); backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); } .thumbnail-modal-close { position: absolute; top: 12px; right: 18px; background: transparent; color: #fff; border: none; font-size: 28px; line-height: 1; width: 36px; height: 36px; cursor: pointer; transition: background 0.2s; z-index: 2; display: flex; align-items: center; justify-content: center; } .thumbnail-modal-close:hover { color: #ff4444; transform: rotate(90deg) scale(1.25); } .thumbnail-modal-img { max-width: 72vw; max-height: 70vh; margin-bottom: 12px; 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; margin-top: 8px; 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.appendChild(s); } } catch { // fallback: inject minimal styles 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.appendChild(s); } } })(); // Modal image viewer (class-based, uses injected CSS) function showImageModal(url, titleText) { // remove existing 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 closeBtn = document.createElement('button'); closeBtn.className = 'thumbnail-modal-close'; closeBtn.innerHTML = `\n \n \n \n `; closeBtn.title = 'Close'; closeBtn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); overlay.remove(); }); const title = document.createElement('div'); title.className = 'thumbnail-modal-title'; title.textContent = titleText || 'Thumbnail Preview'; const img = document.createElement('img'); img.className = 'thumbnail-modal-img'; img.src = url; img.alt = 'Thumbnail Preview'; img.title = 'Click to open in new tab'; img.style.cursor = 'pointer'; img.addEventListener('click', () => window.open(img.src, '_blank')); const optionsDiv = document.createElement('div'); optionsDiv.className = 'thumbnail-modal-options'; content.appendChild(closeBtn); content.appendChild(title); content.appendChild(img); content.appendChild(optionsDiv); overlay.appendChild(content); overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); function escHandler(e) { if (e.key === 'Escape') { overlay.remove(); window.removeEventListener('keydown', escHandler, true); } } window.addEventListener('keydown', escHandler, true); img.addEventListener('error', () => { const err = document.createElement('div'); err.textContent = 'Не удалось загрузить изображение'; err.style.color = 'white'; content.appendChild(err); }); document.body.appendChild(overlay); } 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 addOrUpdateThumbnailImage() { if (!isWatchPage()) return; const newVideoId = new URLSearchParams(window.location.search).get('v'); if (newVideoId !== thumbnailPreviewCurrentVideoId) { thumbnailPreviewClosed = false; } if (!newVideoId || newVideoId === thumbnailPreviewCurrentVideoId || thumbnailPreviewClosed) { return; } thumbnailPreviewCurrentVideoId = newVideoId; function attemptInsertion() { const player = document.querySelector('#movie_player') || document.querySelector('ytd-player'); if (!player) { thumbnailInsertionAttempts++; if (thumbnailInsertionAttempts < MAX_ATTEMPTS) { setTimeout(attemptInsertion, RETRY_DELAY); } else { thumbnailInsertionAttempts = 0; } return; } // Add or update a small overlay icon at top-left of the player const overlayId = 'thumbnailPreview-player-overlay'; let overlay = player.querySelector(`#${overlayId}`); if (!overlay) { // create a standard thumb-overlay and adapt it for the top-left player position overlay = /** @type {any} */ ( createThumbnailOverlay(thumbnailPreviewCurrentVideoId, player) ); overlay.id = overlayId; // override position/size for player overlay (top-left) overlay.style.cssText = ` position: absolute; top: 8px; left: 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.5; `; // ensure the player is positioned to allow absolute child const playerAny = /** @type {any} */ (player); if (/** @type {any} */ (getComputedStyle(playerAny)).position === 'static') { playerAny.style.position = 'relative'; } playerAny.appendChild(overlay); } else { // overlay already exists — keep it updated (no img src needed, overlay contains svg) } thumbnailInsertionAttempts = 0; } 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.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)'; svg.style.stroke = '#f50057'; }; overlay.onmouseleave = () => { overlay.style.background = 'rgba(0, 0, 0, 0.7)'; svg.style.stroke = 'white'; }; 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 addThumbnailOverlay(container) { if (container.querySelector('.thumb-overlay')) return; let videoId = null; let thumbnailContainer = null; const img = container.querySelector('img[src*="ytimg.com"]'); if (img?.src) { videoId = extractVideoId(img.src); thumbnailContainer = img.closest('yt-thumbnail-view-model') || img.parentElement; } if (!videoId) { const link = container.querySelector('a[href*="/shorts/"]'); if (link?.href) { videoId = extractShortsId(link.href); const shortsImg = container.querySelector('img[src*="ytimg.com"]'); if (shortsImg) { thumbnailContainer = shortsImg.closest('.ytCoreImageHost') || shortsImg.closest('[class*="ThumbnailContainer"]') || shortsImg.closest('[class*="ImageHost"]') || shortsImg.parentElement; } } } if (!videoId || !thumbnailContainer) return; if (getComputedStyle(thumbnailContainer).position === 'static') { thumbnailContainer.style.position = 'relative'; } const overlay = createThumbnailOverlay(videoId, container); overlay.className = 'thumb-overlay'; thumbnailContainer.appendChild(overlay); thumbnailContainer.onmouseenter = () => (overlay.style.opacity = '1'); thumbnailContainer.onmouseleave = () => (overlay.style.opacity = '0'); } 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)'; svg.style.stroke = '#f50057'; }; overlay.onmouseleave = () => { overlay.style.background = 'rgba(0, 0, 0, 0.7)'; svg.style.stroke = 'white'; }; return overlay; } function addAvatarOverlay(img) { const container = img.parentElement; 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)'; svg.style.stroke = '#f50057'; }; overlay.onmouseleave = () => { overlay.style.background = 'rgba(0, 0, 0, 0.7)'; svg.style.stroke = 'white'; }; 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"]', ]; avatarSelectors.forEach(selector => { document.querySelectorAll(selector).forEach(img => { if (img.src && img.src.includes('yt') && !img.closest('.avatar-overlay')) { 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"]', ]; bannerSelectors.forEach(selector => { document.querySelectorAll(selector).forEach(img => { if ( img.src && (img.src.includes('banner') || img.src.includes('yt')) && !img.closest('.banner-overlay') ) { addBannerOverlay(img); } }); }); } function processThumbnails() { document.querySelectorAll('yt-thumbnail-view-model').forEach(addThumbnailOverlay); document.querySelectorAll('.ytd-thumbnail').forEach(addThumbnailOverlay); document.querySelectorAll('ytm-shorts-lockup-view-model').forEach(addThumbnailOverlay); document.querySelectorAll('.shortsLockupViewModelHost').forEach(addThumbnailOverlay); document.querySelectorAll('[class*="shortsLockupViewModelHost"]').forEach(addThumbnailOverlay); } function processAll() { processThumbnails(); processAvatars(); processBanners(); addOrUpdateThumbnailImage(); } function setupMutationObserver() { const observer = new MutationObserver(() => { setTimeout(processAll, 50); }); // ✅ Safe observe with document.body check 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 () { originalPushState.apply(history, arguments); setTimeout(() => { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 500); } }, 100); }; history.replaceState = function () { originalReplaceState.apply(history, arguments); setTimeout(() => { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 500); } }, 100); }; window.addEventListener('popstate', function () { setTimeout(() => { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 500); } }, 100); }); setInterval(function () { if (location.href !== currentUrl) { currentUrl = location.href; setTimeout(addOrUpdateThumbnailImage, 300); } }, 500); } function initialize() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { setTimeout(init, 100); }); } else { setTimeout(init, 100); } } function init() { setupUrlChangeDetection(); setupMutationObserver(); processAll(); setTimeout(processAll, 500); setTimeout(processAll, 1000); setTimeout(processAll, 2000); } initialize(); })(); // --- MODULE: comment.js --- /** * Comment Manager Module * Provides bulk delete functionality and comment management tools for YouTube * @module CommentManager */ (function () { 'use strict'; /** * Configuration object for comment manager * @const {Object} */ 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', }; // State management const state = { observer: null, isProcessing: false, settingsNavListenerKey: null, panelCollapsed: false, }; // Optimized settings 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 { } }, }; // Utility functions: use shared debounce when available const debounce = (func, wait) => { try { return ( (window.YouTubeUtils && window.YouTubeUtils.debounce) || ((f, w) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => f(...args), w); }; })(func, wait) ); } catch { // fallback let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } }; /** * Safely query a single element * @param {string} selector - CSS selector * @returns {HTMLElement|null} The first matching element or null */ const $ = selector => /** @type {HTMLElement|null} */(document.querySelector(selector)); /** * Safely query multiple elements * @param {string} selector - CSS selector * @returns {NodeListOf} NodeList of matching elements */ const $$ = selector => /** @type {NodeListOf} */(document.querySelectorAll(selector)); /** * Log error with error boundary integration * @param {string} context - Error context * @param {Error|string|unknown} error - Error object or message */ 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); } }; /** * Wraps function with error boundary protection * @template {Function} T * @param {T} fn - Function to wrap * @param {string} context - Error context for debugging * @returns {T} Wrapped function */ const withErrorBoundary = (fn, context) => { if (window.YouTubeErrorBoundary?.withErrorBoundary) { return /** @type {T} */ (window.YouTubeErrorBoundary.withErrorBoundary(fn, 'CommentManager')); } return /** @type {any} */ ( (...args) => { try { return fn(...args); } catch (error) { logError(context, error); return null; } } ); }; /** * Add checkboxes to comment elements for selection * Core functionality for bulk operations */ 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', 'Select comment'); checkbox.addEventListener('change', updateDeleteButtonState); checkbox.addEventListener('click', e => e.stopPropagation()); // Optimized positioning 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'); /** * Add control panel with bulk action buttons */ 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', 'Comment manager controls'); const header = document.createElement('div'); header.className = CONFIG.classes.header; const title = document.createElement('div'); title.className = CONFIG.classes.title; title.textContent = 'Comment Manager'; 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', 'Toggle panel'); 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( 'Delete Selected', `${CONFIG.classes.buttonDanger} ${CONFIG.classes.deleteButton}`, deleteSelectedComments, { disabled: true } ); const selectAllButton = createActionButton('Select All', CONFIG.classes.buttonPrimary, () => { $$(`.${CONFIG.classes.checkbox}`).forEach(cb => (cb.checked = true)); updateDeleteButtonState(); }); const clearAllButton = createActionButton('Clear All', 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'); /** * Update delete button state based on checkbox selection */ 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'); /** * Delete selected comments with confirmation */ 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'); /** * Clean up all comment manager elements */ const cleanup = withErrorBoundary(() => { $$(`.${CONFIG.classes.checkbox}`).forEach(el => el.remove()); $(`.${CONFIG.classes.container}`)?.remove(); }, 'cleanup'); /** * Initialize or cleanup script based on enabled state */ const initializeScript = withErrorBoundary(() => { if (CONFIG.enabled) { addCheckboxes(); addControlButtons(); updateDeleteButtonState(); } else { cleanup(); } }, 'initializeScript'); /** * Add enhanced CSS styles for comment manager UI */ 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:16px;z-index:9999;padding:18px;background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-lg);box-shadow:var(--yt-glass-shadow);backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);min-width:220px;max-width:260px;color:var(--yt-text-primary);transition:transform .3s ease,opacity .3s ease;} 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'); /** * Add comment manager settings to YouTube+ settings panel */ const addCommentManagerSettings = withErrorBoundary(() => { const advancedSection = $('.ytp-plus-settings-section[data-section="advanced"]'); if (!advancedSection) return; // If already exists, move it to the bottom to ensure Comment Manager is last const existing = $('.comment-manager-settings-item'); if (existing) { try { advancedSection.appendChild(existing); } catch { // ignore } return; } const settingsItem = document.createElement('div'); settingsItem.className = 'ytp-plus-settings-item comment-manager-settings-item'; settingsItem.innerHTML = `
Add bulk delete functionality for managing comments on YouTube
`; // Append to end (ensure it's the bottom-most item) advancedSection.appendChild(settingsItem); $('#open-comment-history-page').addEventListener('click', () => { window.open('https://www.youtube.com/feed/history/comment_history', '_blank'); }); }, 'addCommentManagerSettings'); /** * Initialize comment manager module * Sets up observers, event listeners, and initial state */ const init = withErrorBoundary(() => { settings.load(); addStyles(); // Setup observer with throttling state.observer?.disconnect(); state.observer = new MutationObserver(debounce(initializeScript, CONFIG.debounceDelay)); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(state.observer); // ✅ Safe observe with document.body check 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 }); }); } // Initial setup if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeScript); } else { initializeScript(); } // Settings modal integration 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; } } } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(settingsObserver); // ✅ Safe observe with document.body check 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 = /** @type {EventTarget & HTMLElement} */ (e.target); if (target.dataset?.section === 'advanced') { setTimeout(addCommentManagerSettings, 50); } }; if (!state.settingsNavListenerKey) { state.settingsNavListenerKey = YouTubeUtils.cleanupManager.registerListener( document, 'click', handleAdvancedNavClick, { passive: true, capture: true } ); } }, 'init'); // Start the module init(); })(); // --- MODULE: update.js --- // Update checker module (function () { 'use strict'; const UPDATE_CONFIG = { enabled: true, checkInterval: 24 * 60 * 60 * 1000, // 24 hours updateUrl: 'https://update.greasyfork.icu/scripts/537017/YouTube%20%2B.meta.js', currentVersion: '2.0', storageKey: 'youtube_plus_update_check', notificationDuration: 8000, autoInstallUrl: 'https://update.greasyfork.icu/scripts/537017/YouTube%20%2B.user.js', }; const updateState = { lastCheck: 0, lastVersion: UPDATE_CONFIG.currentVersion, updateAvailable: false, checkInProgress: false, updateDetails: null, }; // Optimized utilities const utils = { loadSettings: () => { try { const saved = localStorage.getItem(UPDATE_CONFIG.storageKey); if (saved) Object.assign(updateState, JSON.parse(saved)); } catch (e) { console.warn('[YouTube+] Failed to load update settings:', e); } }, saveSettings: () => { try { localStorage.setItem( UPDATE_CONFIG.storageKey, JSON.stringify({ lastCheck: updateState.lastCheck, lastVersion: updateState.lastVersion, updateAvailable: updateState.updateAvailable, updateDetails: updateState.updateDetails, }) ); } catch (e) { console.warn('[YouTube+] Failed to save update settings:', e); } }, compareVersions: (v1, v2) => { const normalize = v => v .replace(/[^\d.]/g, '') .split('.') .map(n => parseInt(n) || 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 => { const extractField = field => text.match(new RegExp(`@${field}\\s+([^\\r\\n]+)`))?.[1]?.trim(); return { version: extractField('version'), description: extractField('description') || '', downloadUrl: extractField('downloadURL') || UPDATE_CONFIG.autoInstallUrl, }; }, formatTimeAgo: timestamp => { if (!timestamp) return '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 `${diffDays}d ago`; if (diffHours > 0) return `${diffHours}h ago`; if (diffMinutes > 0) return `${diffMinutes}m ago`; return 'Just now'; }, showNotification: (text, type = 'info', duration = 3000) => { YouTubeUtils.NotificationManager.show(text, { type, duration }); }, }; // Enhanced update notification const showUpdateNotification = updateDetails => { const notification = document.createElement('div'); notification.className = 'youtube-enhancer-notification update-notification'; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; 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: slideInFromRight 0.4s ease-out; `; notification.innerHTML = `
YouTube + Update Available
Version ${updateDetails.version} • ${updateDetails.description || 'New features and improvements'}
`; document.body.appendChild(notification); const removeNotification = () => { notification.style.animation = 'slideInFromRight 0.3s ease-in reverse'; setTimeout(() => notification.remove(), 300); }; // Event handlers notification.querySelector('#update-install-btn').addEventListener('click', () => { try { window.open(updateDetails.downloadUrl, '_blank'); sessionStorage.setItem('update_dismissed', updateDetails.version); removeNotification(); setTimeout( () => utils.showNotification('Update started! Follow your userscript manager instructions.'), 500 ); } catch (error) { console.error('Error installing update:', error); window.open('https://greasyfork.org/en/scripts/537017-youtube', '_blank'); removeNotification(); } }); notification.querySelector('#update-dismiss-btn').addEventListener('click', () => { sessionStorage.setItem('update_dismissed', updateDetails.version); removeNotification(); }); notification.querySelector('#update-close-btn').addEventListener('click', removeNotification); // Auto-dismiss setTimeout(() => { if (notification.isConnected) removeNotification(); }, UPDATE_CONFIG.notificationDuration); }; // Optimized update checker const checkForUpdates = async (force = false) => { if (!UPDATE_CONFIG.enabled || updateState.checkInProgress) return; const now = Date.now(); if (!force && now - updateState.lastCheck < UPDATE_CONFIG.checkInterval) return; updateState.checkInProgress = true; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const response = await fetch(UPDATE_CONFIG.updateUrl, { method: 'GET', cache: 'no-cache', signal: controller.signal, headers: { Accept: 'text/plain', 'User-Agent': 'YouTube+ UpdateChecker' }, }); clearTimeout(timeoutId); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); const metaText = await response.text(); const updateDetails = utils.parseMetadata(metaText); if (updateDetails.version) { updateState.lastCheck = now; updateState.lastVersion = updateDetails.version; updateState.updateDetails = updateDetails; const comparison = utils.compareVersions( UPDATE_CONFIG.currentVersion, updateDetails.version ); updateState.updateAvailable = comparison < 0; if ( updateState.updateAvailable && (force || sessionStorage.getItem('update_dismissed') !== updateDetails.version) ) { showUpdateNotification(updateDetails); console.log(`YouTube + Update available: ${updateDetails.version}`); } else if (force) { utils.showNotification( updateState.updateAvailable ? `Update ${updateDetails.version} available!` : `You're using the latest version (${UPDATE_CONFIG.currentVersion})` ); } utils.saveSettings(); } } catch (error) { console.error('Update check failed:', error); if (force) utils.showNotification(`Update check failed: ${error.message}`, 'error', 4000); } finally { updateState.checkInProgress = false; } }; // Optimized settings UI const addUpdateSettings = () => { // ✅ Use cached querySelector 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 = `

Enhanced YouTube experience with powerful features

Current Version ${UPDATE_CONFIG.currentVersion}
Last checked: ${lastCheckTime} ${updateState.lastVersion && updateState.lastVersion !== UPDATE_CONFIG.currentVersion ? `
Latest available: ${updateState.lastVersion}` : '' }
${updateState.updateAvailable ? `
Update Available
` : `
Up to Date
` }
`; aboutSection.appendChild(updateContainer); // Event listeners with optimization const attachClickHandler = (id, handler) => { const element = document.getElementById(id); if (element) YouTubeUtils.cleanupManager.registerListener(element, 'click', handler); }; attachClickHandler('manual-update-check', async e => { const button = /** @type {EventTarget & HTMLElement} */ (e.target); const originalHTML = button.innerHTML; button.innerHTML = ` Checking... `; button.disabled = true; await checkForUpdates(true); setTimeout(() => { button.innerHTML = originalHTML; button.disabled = false; }, 1000); }); attachClickHandler('install-update-btn', () => { const url = updateState.updateDetails?.downloadUrl || 'https://greasyfork.org/en/scripts/537017-youtube'; window.open(url, '_blank'); }); attachClickHandler('open-update-page', () => { window.open('https://greasyfork.org/en/scripts/537017-youtube', '_blank'); }); }; // Optimized initialization const init = () => { utils.loadSettings(); // Initial check with delay setTimeout(() => checkForUpdates(), 3000); // Periodic checks // ✅ Register interval in cleanupManager const intervalId = setInterval(() => checkForUpdates(), UPDATE_CONFIG.checkInterval); YouTubeUtils.cleanupManager.registerInterval(intervalId); window.addEventListener('beforeunload', () => clearInterval(intervalId)); // Optimized settings modal observer let settingsObserved = false; const observer = new MutationObserver(mutations => { if (settingsObserved) return; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node instanceof Element && node.classList?.contains('ytp-plus-settings-modal')) { settingsObserved = true; setTimeout(addUpdateSettings, 100); return; } } } // ✅ Use cached querySelector 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); } }); // ✅ Register observer in cleanupManager YouTubeUtils.cleanupManager.registerObserver(observer); // ✅ Safe observe with document.body check if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); } // Optimized click handler // ✅ Register global listener in cleanupManager const clickHandler = e => { const target = /** @type {EventTarget & HTMLElement} */ (e.target); if ( target.classList?.contains('ytp-plus-settings-nav-item') && target.dataset?.section === 'about' ) { setTimeout(addUpdateSettings, 50); } }; YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, { passive: true, capture: true, }); console.log('YouTube + Update Checker initialized', { version: UPDATE_CONFIG.currentVersion, enabled: UPDATE_CONFIG.enabled, lastCheck: new Date(updateState.lastCheck).toLocaleString(), updateAvailable: updateState.updateAvailable, }); }; // Start if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();