Vista a schede per YouTube, download e altre funzionalità ↴
// ==UserScript== // @name YouTube + // @name:ar YouTube + // @name:az YouTube + // @name:be YouTube + // @name:bg YouTube + // @name:zh-CN YouTube + // @name:de YouTube + // @name:nl YouTube + // @name:en YouTube + // @name:es YouTube + // @name:fr YouTube + // @name:hi YouTube + // @name:id YouTube + // @name:it YouTube + // @name:ja YouTube + // @name:kk YouTube + // @name:ko YouTube + // @name:ky YouTube + // @name:pl YouTube + // @name:pt YouTube + // @name:tr YouTube + // @name:zh-TW YouTube + // @name:uk YouTube + // @name:uz YouTube + // @name:vi YouTube + // @namespace by // @version 2.4.5 // @author diorhc // @description Вкладки для информации, комментариев, видео, плейлиста и скачивание видео и другие функции ↴ // @description:ar Tabview YouTube and download and other features ↴ // @description:az Tabview YouTube və yükləmə və digər xüsusiyyətlər ↴ // @description:be Tabview YouTube і загрузка і іншыя функцыі ↴ // @description:bg Tabview YouTube и изтегляне и други функции ↴ // @description:zh-CN 标签视图 YouTube、下载及其他功能 ↴ // @description:de Tabview YouTube und Download und andere Funktionen ↴ // @description:nl Tabview YouTube en Download en andere functies ↴ // @description:en Tabview YouTube and Download and others features ↴ // @description:es Vista de pestañas de YouTube, descarga y otras funciones ↴ // @description:fr Tabview YouTube et Télécharger et autres fonctionnalités ↴ // @description:hi YouTube टैब व्यू, डाउनलोड और अन्य सुविधाएँ ↴ // @description:id Tampilan tab YouTube, unduh, dan fitur lainnya ↴ // @description:it Vista a schede per YouTube, download e altre funzionalità ↴ // @description:ja タブビューYouTubeとダウンロードおよびその他の機能 ↴ // @description:kk Tabview YouTube және жүктеу және басқа функциялар ↴ // @description:ko Tabview YouTube 및 다운로드 및 기타 기능 ↴ // @description:ky Tabview YouTube жана жүктөө жана башка функциялар ↴ // @description:pl Widok kart YouTube, pobieranie i inne funkcje ↴ // @description:pt Visualização em abas do YouTube, download e outros recursos ↴ // @description:tr Sekmeli Görünüm YouTube ve İndir ve diğer özellikler ↴ // @description:zh-TW 標籤檢視 YouTube 及下載及其他功能 ↴ // @description:uk Перегляд вкладок YouTube, завантаження та інші функції ↴ // @description:uz YouTube uchun tabview va yuklab olish va boshqa xususiyatlar ↴ // @description:vi Chế độ tab cho YouTube, tải xuống và các tính năng khác ↴ // @match https://*.youtube.com/* // @match https://music.youtube.com/* // @match https://studio.youtube.com/* // @match *://myactivity.google.com/* // @include *://www.youtube.com/feed/history/* // @include https://www.youtube.com // @include *://*.youtube.com/** // @exclude *://accounts.youtube.com/* // @exclude *://www.youtube.com/live_chat_replay* // @exclude *://www.youtube.com/persist_identity* // @exclude /^https?://\w+\.youtube\.com\/live_chat.*$/ // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @license MIT // @require https://cdn.jsdelivr.net/npm/@preact/[email protected]/dist/signals-core.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/browser-id3-writer.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/preact.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/hooks/dist/hooks.umd.js // @require https://cdn.jsdelivr.net/npm/@preact/[email protected]/dist/signals.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect api.livecounts.io // @connect cnv.cx // @connect mp3yt.is // @connect * // @connect youtube.com // @connect googlevideo.com // @connect self // @run-at document-start // @noframes // @homepageURL https://github.com/diorhc/YTP // @supportURL https://github.com/diorhc/YTP/issues // ==/UserScript== !(function() { "use strict"; const LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3 }; const logBuffer = []; const rateLimitMap = new Map; let currentLevel = (function isDevMode() { try { if ("undefined" != typeof window) { if (window.__ytpDevMode) { return !0; } const settings = localStorage.getItem("youtube_plus_settings"); if (settings) { const parsed = JSON.parse(settings); if (parsed.debugMode) { return !0; } } } } catch {} return !1; })() ? "debug" : "warn"; function log(level, module, message, data) { if (LOG_LEVELS[level] > LOG_LEVELS[currentLevel]) { return; } if (!(function checkRateLimit(module) { const now = Date.now(); const entry = rateLimitMap.get(module); if (!entry || now > entry.resetTime) { rateLimitMap.set(module, { count: 1, resetTime: now + 6e4 }); return !0; } if (entry.count >= 60) { return !1; } entry.count++; return !0; })(module)) { return; } const formatted = (function formatMessage(level, module, message) { return `[YouTube+][${module}][${level.toUpperCase()}] ${message}`; })(level, module, message); const entry = { timestamp: Date.now(), level, module, message, data: void 0 !== data ? data : void 0 }; logBuffer.push(entry); logBuffer.length > 200 && logBuffer.splice(0, logBuffer.length - 200); "error" === level ? void 0 !== data ? console.error(formatted, data) : console.error(formatted) : ("warn" === level || "debug" === currentLevel) && (void 0 !== data ? console.warn(formatted, data) : console.warn(formatted)); } const logger = { error(module, message, data) { log("error", module, message, data); }, warn(module, message, data) { log("warn", module, message, data); }, info(module, message, data) { log("info", module, message, data); }, debug(module, message, data) { log("debug", module, message, data); }, setLevel(level) { void 0 !== LOG_LEVELS[level] && (currentLevel = level); }, getLevel: () => currentLevel, getRecent(count = 50, filterLevel) { let entries = logBuffer; filterLevel && (entries = entries.filter(e => e.level === filterLevel)); return entries.slice(-count); }, export: () => JSON.stringify(logBuffer, null, 2), clear() { logBuffer.length = 0; rateLimitMap.clear(); }, getStats() { const byLevel = { error: 0, warn: 0, info: 0, debug: 0 }; const byModule = {}; for (const entry of logBuffer) { byLevel[entry.level]++; byModule[entry.module] = (byModule[entry.module] || 0) + 1; } return { totalEntries: logBuffer.length, byLevel, byModule, currentLevel }; }, createLogger: moduleName => ({ error(message, data) { log("error", moduleName, message, data); }, warn(message, data) { log("warn", moduleName, message, data); }, info(message, data) { log("info", moduleName, message, data); }, debug(message, data) { log("debug", moduleName, message, data); } }) }; "undefined" != typeof window && (window.YouTubePlusLogger = logger); "undefined" != typeof module && module.exports && (module.exports = { logger, LOG_LEVELS }); })(); !(function() { "use strict"; const modules = new Map; const pendingCallbacks = new Map; const registry = { register(name, moduleExport) { if (!name || "string" != typeof name) { console.warn("[YouTube+ Registry] Invalid module name:", name); return; } modules.set(name, moduleExport); const windowAliases = { utils: "YouTubeUtils", domCache: "YouTubeDOMCache", errorBoundary: "YouTubeErrorBoundary", performance: "YouTubePerformance", i18n: "YouTubePlusI18n", lazyLoader: "YouTubePlusLazyLoader", eventDelegation: "YouTubePlusEventDelegation", security: "YouTubeSecurityUtils", settings: "YouTubePlusSettingsHelpers", modalHandlers: "YouTubePlusModalHandlers", stats: "YouTubeStats", download: "YouTubePlusDownload", music: "YouTubeMusic", voting: "YouTubePlus", logger: "YouTubePlusLogger" }; windowAliases[name] && "undefined" != typeof window && (window[windowAliases[name]] = moduleExport); const pending = pendingCallbacks.get(name); if (pending) { for (const cb of pending) { try { cb(moduleExport); } catch (e) { console.error(`[YouTube+ Registry] Callback error for "${name}":`, e); } } pendingCallbacks.delete(name); } }, get: name => modules.get(name), has: name => modules.has(name), onReady(name, callback) { if (modules.has(name)) { try { callback(modules.get(name)); } catch (e) { console.error(`[YouTube+ Registry] onReady callback error for "${name}":`, e); } } else { pendingCallbacks.has(name) || pendingCallbacks.set(name, new Set); pendingCallbacks.get(name).add(callback); } }, list: () => Array.from(modules.keys()), getStats: () => ({ totalModules: modules.size, moduleNames: Array.from(modules.keys()), pendingCallbacks: Array.from(pendingCallbacks.keys()) }), unregister(name) { modules.delete(name); }, clear() { modules.clear(); pendingCallbacks.clear(); } }; "undefined" != typeof window && (window.YouTubePlusRegistry = registry); "undefined" != typeof module && module.exports && (module.exports = { registry }); })(); !(function() { "use strict"; const qs = selector => window.YouTubeDOMCache && "function" == typeof window.YouTubeDOMCache.get ? window.YouTubeDOMCache.get(selector) : document.querySelector(selector); const $ = (sel, ctx) => { const cache = window.YouTubeDOMCache; return cache && "function" == typeof cache.querySelector ? cache.querySelector(sel, ctx) : cache && "function" == typeof cache.get && !ctx ? cache.get(sel) : (ctx || document).querySelector(sel); }; const $$ = (sel, ctx) => { const cache = window.YouTubeDOMCache; return cache && "function" == typeof cache.querySelectorAll ? cache.querySelectorAll(sel, ctx) : cache && "function" == typeof cache.getAll && !ctx ? cache.getAll(sel) : Array.from((ctx || document).querySelectorAll(sel)); }; const byId = id => { const cache = window.YouTubeDOMCache; return cache && "function" == typeof cache.getElementById ? cache.getElementById(id) : document.getElementById(id); }; const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const t = (key, params = {}) => { if (window.YouTubePlusI18n?.t) { return window.YouTubePlusI18n.t(key, params); } if (!key) { return ""; } let result = String(key); for (const [k, v] of Object.entries(params || {})) { result = result.replace(new RegExp(`\\{${escapeRegex(k)}\\}`, "g"), String(v)); } return result; }; const SETTINGS_KEY = "youtube_plus_settings"; const isStudioPage = () => { try { return location.hostname.includes("studio.youtube.com"); } catch { return !1; } }; const loadFeatureEnabled = (featureKey, defaultValue = !0) => { try { const settings = localStorage.getItem(SETTINGS_KEY); if (settings) { const parsed = JSON.parse(settings); return !1 !== parsed[featureKey]; } } catch {} return defaultValue; }; const logError = (module, message, error) => { try { const errorDetails = { module, message, error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : error, timestamp: (new Date).toISOString(), userAgent: "undefined" != typeof navigator ? navigator.userAgent : "unknown", url: "undefined" != typeof window ? window.location.href : "unknown" }; console.error(`[YouTube+][${module}] ${message}:`, error); console.warn("[YouTube+] Error details:", errorDetails); } catch (loggingError) { console.error("[YouTube+] Error logging failed:", loggingError); } }; const debounce = (fn, ms, options = {}) => { let timeout = null; let lastArgs = null; let lastThis = null; let isDestroyed = !1; const debounced = function(...args) { if (!isDestroyed) { lastArgs = args; lastThis = this; null !== timeout && clearTimeout(timeout); if (options.leading && null === timeout) { try { fn.apply(this, args); } catch (e) { console.error("[YouTube+] Debounced function error:", e); } } timeout = setTimeout(() => { if (!isDestroyed && !options.leading) { try { fn.apply(lastThis, lastArgs); } catch (e) { console.error("[YouTube+] Debounced function error:", e); } } timeout = null; lastArgs = null; lastThis = null; }, ms); } }; debounced.cancel = () => { null !== timeout && clearTimeout(timeout); timeout = null; lastArgs = null; lastThis = null; }; debounced.destroy = () => { debounced.cancel(); isDestroyed = !0; }; return debounced; }; const throttle = (fn, limit) => { let inThrottle = !1; let lastResult; return function(...args) { if (!inThrottle) { lastResult = fn.apply(this, args); inThrottle = !0; setTimeout(() => inThrottle = !1, limit); } return lastResult; }; }; const StyleManager = (function() { const styles = new Map; return { add(id, css) { try { let el = document.getElementById(id); styles.set(id, css); if (!el) { el = document.createElement("style"); el.id = id; if (!document.head) { document.addEventListener("DOMContentLoaded", () => { if (!document.getElementById(id) && document.head) { document.head.appendChild(el); el.textContent = Array.from(styles.values()).join("\n\n"); } }, { once: !0 }); return; } document.head.appendChild(el); } 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); el && el.remove(); } catch (e) { logError("StyleManager", "remove failed", e); } }, clear() { for (const id of Array.from(styles.keys())) { this.remove(id); } } }; })(); const EventDelegator = (() => { const delegations = new Map; return { delegate(parent, selector, event, handler) { const delegateHandler = e => { const target = e.target; const match = target.closest(selector); match && parent.contains(match) && handler.call(match, e); }; parent.addEventListener(event, delegateHandler, { passive: !0 }); const key = `${event}_${selector}`; delegations.has(parent) || delegations.set(parent, new Map); delegations.get(parent).set(key, delegateHandler); return () => { parent.removeEventListener(event, delegateHandler); const parentMap = delegations.get(parent); if (parentMap) { parentMap.delete(key); 0 === parentMap.size && delegations.delete(parent); } }; }, clearFor(parent) { const parentMap = delegations.get(parent); if (parentMap) { parentMap.forEach((handler, key) => { const event = key.split("_")[0]; parent.removeEventListener(event, handler); }); delegations.delete(parent); } }, clearAll() { delegations.forEach((map, parent) => { map.forEach((handler, key) => { const event = key.split("_")[0]; parent.removeEventListener(event, handler); }); }); delegations.clear(); } }; })(); const cleanupManager = (function() { const observers = new Set; const listeners = new Map; const listenerStats = { registeredTotal: 0 }; const intervals = new Set; const timeouts = new Set; const animationFrames = new Set; const callbacks = new Set; const elementObservers = new WeakMap; return { registerObserver(o, el) { try { o && observers.add(o); if (el && "object" == typeof el) { try { let set = elementObservers.get(el); if (!set) { set = new Set; elementObservers.set(el, set); } set.add(o); } catch {} } } catch {} return o; }, registerListener(target, ev, fn, opts) { try { target.addEventListener(ev, fn, opts); const key = Symbol(); listeners.set(key, { target, ev, fn, opts }); listenerStats.registeredTotal++; return key; } catch (e) { logError("cleanupManager", "registerListener failed", e); return null; } }, getListenerStats() { try { return { active: listeners.size, registeredTotal: listenerStats.registeredTotal }; } catch { return { active: 0, registeredTotal: 0 }; } }, registerInterval(id) { intervals.add(id); return id; }, registerTimeout(id) { timeouts.add(id); return id; }, registerAnimationFrame(id) { animationFrames.add(id); return id; }, register(cb) { "function" == typeof cb && callbacks.add(cb); }, cleanup() { try { for (const cb of callbacks) { try { cb(); } catch (e) { logError("cleanupManager", "callback failed", e); } } callbacks.clear(); for (const o of observers) { try { o && "function" == typeof o.disconnect && o.disconnect(); } catch {} } observers.clear(); for (const keyEntry of listeners.values()) { try { keyEntry.target.removeEventListener(keyEntry.ev, keyEntry.fn, keyEntry.opts); } catch {} } listeners.clear(); for (const id of intervals) { clearInterval(id); } intervals.clear(); for (const id of timeouts) { clearTimeout(id); } timeouts.clear(); for (const id of animationFrames) { cancelAnimationFrame(id); } animationFrames.clear(); } catch (e) { logError("cleanupManager", "cleanup failed", e); } }, observers, elementObservers, disconnectForElement(el) { try { const set = elementObservers.get(el); if (!set) { return; } for (const o of set) { try { o && "function" == typeof o.disconnect && o.disconnect(); observers.delete(o); } catch {} } elementObservers.delete(el); } catch (e) { logError("cleanupManager", "disconnectForElement failed", e); } }, disconnectObserver(o) { try { if (!o) { return; } try { "function" == typeof o.disconnect && o.disconnect(); } catch {} observers.delete(o); } catch (e) { logError("cleanupManager", "disconnectObserver failed", e); } }, listeners, intervals, timeouts, animationFrames }; })(); const createElement = (tag, props = {}, children = []) => { try { const element = document.createElement(tag); Object.entries(props).forEach(([k, v]) => { "className" === k ? element.className = v : "style" === k && "object" == typeof v ? Object.assign(element.style, v) : "dataset" === k && "object" == typeof v ? Object.assign(element.dataset, v) : k.startsWith("on") && "function" == typeof v ? element.addEventListener(k.slice(2), v) : element.setAttribute(k, v); }); children.forEach(c => { "string" == typeof c ? element.appendChild(document.createTextNode(c)) : c instanceof Node && element.appendChild(c); }); return element; } catch (e) { logError("createElement", "failed", e); return document.createElement("div"); } }; const waitForElement = (selector, timeout = 5e3, parent = document.body) => new Promise((resolve, reject) => { if (!selector || "string" != typeof selector) { 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: !0, subtree: !0 }); const id = setTimeout(() => { try { obs.disconnect(); } catch {} reject(new Error("timeout")); }, timeout); cleanupManager.registerTimeout(id); }); const sanitizeHTML = html => { if ("string" != typeof html) { return ""; } if (html.length > 1e6) { console.warn("[YouTube+] HTML content too large, truncating"); html = html.substring(0, 1e6); } const map = { "<": "<", ">": ">", "&": "&", '"': """, "'": "'", "/": "/", "`": "`", "=": "=" }; return html.replace(/[<>&"'\/`=]/g, char => map[char] || char); }; const escapeHTMLAttribute = str => { if ("string" != typeof str) { return ""; } const map = { "<": "<", ">": ">", "&": "&", '"': """, "'": "'", "/": "/", "`": "`", "=": "=", "\n": " ", "\r": " ", "\t": "	" }; return str.replace(/[<>&"'\/`=\n\r\t]/g, char => map[char] || char); }; const isValidURL = url => { if ("string" != typeof url) { return !1; } if (url.length > 2048) { return !1; } if (/^\s|\s$/.test(url)) { return !1; } try { const parsed = new URL(url); return !![ "http:", "https:" ].includes(parsed.protocol); } catch { return !1; } }; const safeMerge = (target, source) => { if (!source || "object" != typeof source) { return target; } if (!target || "object" != typeof target) { return target; } const dangerousKeys = [ "__proto__", "constructor", "prototype" ]; for (const key in source) { if (!Object.prototype.hasOwnProperty.call(source, key)) { continue; } if (dangerousKeys.includes(key)) { console.warn(`[YouTube+][Security] Blocked attempt to set dangerous key: ${key}`); continue; } const value = source[key]; target[key] = value && "object" == typeof value && !Array.isArray(value) ? safeMerge(target[key] || {}, value) : value; } return target; }; const validateVideoId = videoId => "string" != typeof videoId ? null : /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : null; const validatePlaylistId = playlistId => "string" != typeof playlistId || !/^[a-zA-Z0-9_-]+$/.test(playlistId) || playlistId.length < 2 || playlistId.length > 50 ? null : playlistId; const validateChannelId = channelId => "string" != typeof channelId ? null : /^UC[a-zA-Z0-9_-]{22}$/.test(channelId) || /^@[\w-]{3,30}$/.test(channelId) ? channelId : null; const validateNumber = (value, min = -Infinity, max = Infinity, defaultValue = 0) => { const num = Number(value); return Number.isNaN(num) || !Number.isFinite(num) ? defaultValue : Math.max(min, Math.min(max, num)); }; const retryWithBackoff = async (fn, maxRetries = 3, baseDelay = 1e3) => { let lastError; for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error; if (i < maxRetries - 1) { const delay = baseDelay * Math.pow(2, i); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; }; const storage = { get(key, def = null) { if ("string" != typeof key || !/^[a-zA-Z0-9_\-\.]+$/.test(key)) { logError("storage", "Invalid key format", new Error(`Invalid key: ${key}`)); return def; } try { const v = localStorage.getItem(key); if (null === v) { return def; } if (v.length > 5242880) { logError("storage", "Stored value too large", new Error(`Key: ${key}`)); return def; } return JSON.parse(v); } catch (e) { logError("storage", "Failed to parse stored value", e); return def; } }, set(key, val) { if ("string" != typeof key || !/^[a-zA-Z0-9_\-\.]+$/.test(key)) { logError("storage", "Invalid key format", new Error(`Invalid key: ${key}`)); return !1; } try { const serialized = JSON.stringify(val); if (serialized.length > 5242880) { logError("storage", "Value too large to store", new Error(`Key: ${key}`)); return !1; } localStorage.setItem(key, serialized); return !0; } catch (e) { logError("storage", "Failed to store value", e); return !1; } }, remove(key) { try { localStorage.removeItem(key); } catch (e) { logError("storage", "Failed to remove value", e); } }, clear() { try { localStorage.clear(); } catch (e) { logError("storage", "Failed to clear storage", e); } }, has(key) { try { return null !== localStorage.getItem(key); } catch { return !1; } } }; const DOMCache = (() => { const cache = new Map; return { get(selector, parent = document) { const key = `${selector}_${parent === document ? "doc" : ""}`; const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < 5e3) { return cached.element; } const element = parent.querySelector(selector); if (element) { cache.set(key, { element, timestamp: Date.now() }); if (cache.size > 200) { const oldestKey = cache.keys().next().value; cache.delete(oldestKey); } } return element; }, clear(selector) { const keys = Array.from(cache.keys()).filter(k => k.startsWith(selector)); keys.forEach(k => cache.delete(k)); }, clearAll() { cache.clear(); } }; })(); const ScrollManager = (() => { const listeners = new WeakMap; return { addScrollListener: (element, callback, options = {}) => { try { const {debounce: debounceMs = 0, throttle: throttleMs = 0, runInitial = !1} = options; let handler = callback; debounceMs > 0 && (handler = debounce(handler, debounceMs)); throttleMs > 0 && (handler = throttle(handler, throttleMs)); listeners.has(element) || listeners.set(element, new Set); listeners.get(element).add(handler); element.addEventListener("scroll", handler, { passive: !0 }); if (runInitial) { try { callback(); } catch (err) { logError("ScrollManager", "Initial callback error", err); } } return () => { try { element.removeEventListener("scroll", handler); const set = listeners.get(element); if (set) { set.delete(handler); 0 === set.size && listeners.delete(element); } } catch (err) { logError("ScrollManager", "Cleanup error", err); } }; } catch (err) { logError("ScrollManager", "addScrollListener error", err); return () => {}; } }, removeAllListeners: element => { try { const set = listeners.get(element); if (!set) { return; } set.forEach(handler => { try { element.removeEventListener("scroll", handler); } catch {} }); listeners.delete(element); } catch (err) { logError("ScrollManager", "removeAllListeners error", err); } }, scrollToTop: (element, options = {}) => { const {duration = 300, easing = "ease-out"} = options; try { if ("scrollBehavior" in document.documentElement.style) { element.scrollTo({ top: 0, behavior: "smooth" }); return; } const start = element.scrollTop; const startTime = performance.now(); const scroll = currentTime => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const easedProgress = "ease-out" === easing ? (t => t * (2 - t))(progress) : progress; element.scrollTop = start * (1 - easedProgress); progress < 1 && requestAnimationFrame(scroll); }; requestAnimationFrame(scroll); } catch (err) { logError("ScrollManager", "scrollToTop error", err); } } }; })(); if ("undefined" != typeof window && !window.__ytp_history_wrapped) { window.__ytp_history_wrapped = !0; const _origPush = history.pushState; const _origReplace = history.replaceState; history.pushState = function() { const result = _origPush.apply(this, arguments); try { window.dispatchEvent(new CustomEvent("ytp-history-navigate", { detail: { type: "pushState" } })); } catch (e) { console.warn("[YouTube+] pushState event error:", e); } return result; }; history.replaceState = function() { const result = _origReplace.apply(this, arguments); try { window.dispatchEvent(new CustomEvent("ytp-history-navigate", { detail: { type: "replaceState" } })); } catch (e) { console.warn("[YouTube+] replaceState event error:", e); } return result; }; } const createRetryScheduler = opts => { const {check, maxAttempts = 20, interval = 250, onGiveUp, label} = opts; let attempts = 0; let timerId = null; let stopped = !1; const _label = label || "retry"; const _hasPerfApi = "undefined" != typeof performance && "function" == typeof performance.mark; const tick = () => { if (!stopped) { attempts++; if (_hasPerfApi) { try { performance.mark(`ytp:${_label}:attempt:${attempts}`); } catch {} } try { if (check()) { stopped = !0; if (_hasPerfApi) { try { performance.mark(`ytp:${_label}:success`); } catch {} } return; } } catch (e) { logError("RetryScheduler", "check error", e); } if (attempts >= maxAttempts) { stopped = !0; if (_hasPerfApi) { try { performance.mark(`ytp:${_label}:giveup`); } catch {} } if ("function" == typeof onGiveUp) { try { onGiveUp(); } catch {} } } else { timerId = setTimeout(tick, interval); } } }; timerId = setTimeout(tick, 0); return { stop() { stopped = !0; timerId && clearTimeout(timerId); timerId = null; } }; }; const ObserverRegistry = (() => { let _active = 0; let _peak = 0; let _created = 0; let _disconnected = 0; return { track() { _active++; _created++; _active > _peak && (_peak = _active); }, untrack() { _active = Math.max(0, _active - 1); _disconnected++; }, getStats: () => ({ active: _active, peak: _peak, created: _created, disconnected: _disconnected }), reset() { _active = 0; _peak = 0; _created = 0; _disconnected = 0; }, dump() { const stats = { active: _active, peak: _peak, created: _created, disconnected: _disconnected }; const cmStats = cleanupManager ? { observers: cleanupManager.observers?.size ?? "n/a", intervals: cleanupManager.intervals?.size ?? "n/a", timeouts: cleanupManager.timeouts?.size ?? "n/a", listeners: "function" == typeof cleanupManager.getListenerStats ? cleanupManager.getListenerStats() : "n/a" } : null; console.warn("[YouTube+ Diagnostics] ObserverRegistry:", stats); cmStats && console.warn("[YouTube+ Diagnostics] CleanupManager:", cmStats); return { observers: stats, cleanup: cmStats }; } }; })(); const createFeatureToggle = (featureKey, defaultEnabled = !0) => { let _enabled = loadFeatureEnabled(featureKey, defaultEnabled); const _listeners = new Set; return { isEnabled: () => _enabled, setEnabled(value) { const next = !1 !== value; if (next !== _enabled) { _enabled = next; try { const raw = localStorage.getItem(SETTINGS_KEY); const settings = raw ? JSON.parse(raw) : {}; settings[featureKey] = _enabled; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch {} for (const cb of _listeners) { try { cb(_enabled); } catch {} } } }, onChange(cb) { _listeners.add(cb); return () => _listeners.delete(cb); }, reload() { _enabled = loadFeatureEnabled(featureKey, defaultEnabled); } }; }; if ("undefined" != typeof window) { window.YouTubeUtils = window.YouTubeUtils || {}; const U = 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.EventDelegator = U.EventDelegator || EventDelegator; U.DOMCache = U.DOMCache || DOMCache; U.ScrollManager = U.ScrollManager || ScrollManager; U.createElement = U.createElement || createElement; U.waitForElement = U.waitForElement || waitForElement; U.storage = U.storage || storage; U.sanitizeHTML = U.sanitizeHTML || sanitizeHTML; U.escapeHTMLAttribute = U.escapeHTMLAttribute || escapeHTMLAttribute; U.safeMerge = U.safeMerge || safeMerge; U.validateVideoId = U.validateVideoId || validateVideoId; U.validatePlaylistId = U.validatePlaylistId || validatePlaylistId; U.validateChannelId = U.validateChannelId || validateChannelId; U.validateNumber = U.validateNumber || validateNumber; U.isValidURL = U.isValidURL || isValidURL; U.logger = U.logger || (() => { const isDebugEnabled = (() => { try { if ("undefined" == typeof window) { return !1; } const cfg = window.YouTubePlusConfig; return !(!cfg || !cfg.debug) || void 0 !== window.YTP_DEBUG && !!window.YTP_DEBUG; } catch { return !1; } })(); return { debug: (...args) => { isDebugEnabled && console?.warn && console.warn("[YouTube+][DEBUG]", ...args); }, info: (...args) => { isDebugEnabled && console?.warn && console.warn("[YouTube+][INFO]", ...args); }, warn: (...args) => { console?.warn && console.warn("[YouTube+]", ...args); }, error: (...args) => { console?.error && console.error("[YouTube+]", ...args); } }; })(); U.retryWithBackoff = U.retryWithBackoff || retryWithBackoff; "function" != typeof U.createRetryScheduler && (U.createRetryScheduler = createRetryScheduler); U.ObserverRegistry = U.ObserverRegistry || ObserverRegistry; U.$ = U.$ || $; U.$$ = U.$$ || $$; U.byId = U.byId || byId; U.t = U.t || t; U.loadFeatureEnabled = U.loadFeatureEnabled || loadFeatureEnabled; U.createFeatureToggle = U.createFeatureToggle || createFeatureToggle; U.SETTINGS_KEY = U.SETTINGS_KEY || SETTINGS_KEY; U.isStudioPage = U.isStudioPage || isStudioPage; window.__ytpDiagnostics || (window.__ytpDiagnostics = function(verbose) { const obs = ObserverRegistry.getStats(); const cm = { observers: cleanupManager.observers.size, listeners: cleanupManager.getListenerStats(), intervals: cleanupManager.intervals.size, timeouts: cleanupManager.timeouts.size, animationFrames: cleanupManager.animationFrames.size }; let retryMetrics = null; try { if ("undefined" != typeof performance && "function" == typeof performance.getEntriesByType) { const marks = performance.getEntriesByType("mark").filter(m => m.name.startsWith("ytp:")); const retryLabels = new Set; const retryData = {}; for (const m of marks) { const parts = m.name.split(":"); if (parts.length >= 3) { const label = parts[1]; retryLabels.add(label); retryData[label] || (retryData[label] = { attempts: 0, success: !1, giveup: !1 }); "attempt" === parts[2] ? retryData[label].attempts++ : "success" === parts[2] ? retryData[label].success = !0 : "giveup" === parts[2] && (retryData[label].giveup = !0); } } retryMetrics = { totalMarks: marks.length, schedulers: retryData }; } } catch {} const report = { observers: obs, cleanupManager: cm, retrySchedulers: retryMetrics, timestamp: (new Date).toISOString() }; console.warn("[YouTube+ Diagnostics] Observers:", obs); console.warn("[YouTube+ Diagnostics] CleanupManager:", cm); retryMetrics && console.warn("[YouTube+ Diagnostics] RetrySchedulers:", retryMetrics); verbose && console.warn("[YouTube+ Diagnostics]", JSON.stringify(report, null, 2)); return report; }); U.channelStatsHelpers = U.channelStatsHelpers || null; try { const w = window; if (w && !w.__ytp_timers_wrapped) { const origSetTimeout = w.setTimeout.bind(w); const origSetInterval = w.setInterval.bind(w); const origRaf = w.requestAnimationFrame ? w.requestAnimationFrame.bind(w) : null; w.setTimeout = function(fn, ms, ...args) { const id = origSetTimeout(fn, ms, ...args); try { U.cleanupManager.registerTimeout(id); } catch {} return id; }; w.setInterval = function(fn, ms, ...args) { const id = origSetInterval(fn, ms, ...args); try { U.cleanupManager.registerInterval(id); } catch {} return id; }; origRaf && (w.requestAnimationFrame = function(cb) { const id = origRaf(cb); try { U.cleanupManager.registerAnimationFrame(id); } catch {} return id; }); w.__ytp_timers_wrapped = !0; } } catch (e) { logError("utils", "timer wrapper failed", e); } window.YouTubePlusChannelStatsHelpers || (window.YouTubePlusChannelStatsHelpers = { async fetchWithRetry(fetchFn, maxRetries = 2, logger = console) { let attempt = 0; for (;attempt <= maxRetries; ) { try { const res = await fetchFn(); return res; } catch (err) { attempt += 1; if (attempt > maxRetries) { logger && logger.warn && logger.warn("[ChannelStatsHelpers] fetch failed after retries", err); return null; } await new Promise(r => setTimeout(r, 300 * attempt)); } } return null; }, cacheStats(mapLike, channelId, stats) { try { if (!mapLike || "function" != typeof mapLike.set) { return; } mapLike.set(channelId, stats); } catch {} }, getCachedStats(mapLike, channelId, cacheDuration = 6e4) { try { if (!mapLike || "function" != typeof mapLike.get) { return null; } const s = mapLike.get(channelId); return s ? s.timestamp && Date.now() - s.timestamp > cacheDuration ? null : s : null; } catch { return null; } }, extractSubscriberCountFromPage() { try { const el = qs("yt-formatted-string#subscriber-count") || qs('[id*="subscriber-count"]'); if (!el) { return 0; } const txt = el.textContent || ""; const digits = txt.replace(/[^0-9]/g, ""); return digits ? parseInt(digits, 10) : 0; } catch { return 0; } }, createFallbackStats: (followerCount = 0) => ({ followerCount: followerCount || 0, bottomOdos: [ 0, 0 ], error: !0, timestamp: Date.now() }) }); } })(); !(function() { "use strict"; function escapeHtml(html) { if (!html || "string" != typeof html) { return ""; } const div = document.createElement("div"); div.textContent = html; return div.innerHTML; } function createSafeHTML(html) { return "function" == typeof window._ytplusCreateHTML ? window._ytplusCreateHTML(html) : html; } function sanitizeAttribute(attrName, attrValue) { if (!attrName || "string" != typeof attrName) { return null; } if (null == attrValue) { return ""; } if (/^on[a-z]/i.test(attrName)) { console.warn(`[Security] Blocked event handler attribute: ${attrName}`); return null; } const valueStr = String(attrValue); if ("href" === attrName.toLowerCase() || "src" === attrName.toLowerCase()) { if (/^javascript:/i.test(valueStr)) { console.warn(`[Security] Blocked javascript protocol in ${attrName}`); return null; } if (valueStr.toLowerCase().startsWith("data:") && !valueStr.toLowerCase().startsWith("data:image/")) { console.warn(`[Security] Blocked non-image data: URI in ${attrName}`); return null; } } return valueStr; } class RateLimiter { constructor(maxRequests = 10, timeWindow = 6e4, maxKeys = 100) { this.maxRequests = maxRequests; this.timeWindow = timeWindow; this.maxKeys = maxKeys; this.requests = new Map; } canRequest(key) { const now = Date.now(); const requests = this.requests.get(key) || []; const recentRequests = requests.filter(time => now - time < this.timeWindow); if (recentRequests.length >= this.maxRequests) { console.warn(`[Security] Rate limit exceeded for ${key}. Max ${this.maxRequests} requests per ${this.timeWindow}ms.`); return !1; } recentRequests.push(now); this.requests.set(key, recentRequests); if (this.requests.size > this.maxKeys) { const keysToDelete = this.requests.size - this.maxKeys; const iter = this.requests.keys(); for (let i = 0; i < keysToDelete; i++) { const oldest = iter.next().value; oldest !== key && this.requests.delete(oldest); } } return !0; } clear() { this.requests.clear(); } } if ("undefined" != typeof window) { window.YouTubeSecurityUtils = { isValidVideoId: function isValidVideoId(id) { return !(!id || "string" != typeof id) && /^[a-zA-Z0-9_-]{11}$/.test(id); }, isValidChannelId: function isValidChannelId(id) { return !(!id || "string" != typeof id) && /^UC[a-zA-Z0-9_-]{22}$/.test(id); }, isYouTubeUrl: function isYouTubeUrl(url) { if (!url || "string" != typeof url) { return !1; } try { const parsed = new URL(url); const hostname = parsed.hostname.toLowerCase(); return "www.youtube.com" === hostname || "youtube.com" === hostname || "m.youtube.com" === hostname || "music.youtube.com" === hostname || hostname.endsWith(".youtube.com"); } catch { return !1; } }, sanitizeText: function sanitizeText(text) { return text && "string" == typeof text ? text.replace(/[<>]/g, "").replace(/javascript:/gi, "").replace(/on\w+=/gi, "").trim() : ""; }, escapeHtml, createSafeHTML, setInnerHTMLSafe: function setInnerHTMLSafe(element, html, sanitize = !1) { if (!(element && element instanceof HTMLElement)) { console.error("[Security] Invalid element for setInnerHTMLSafe"); return; } const content = sanitize ? escapeHtml(html) : html; element.innerHTML = createSafeHTML(content); }, setTextContentSafe: function setTextContentSafe(element, text) { element && element instanceof HTMLElement ? element.textContent = text || "" : console.error("[Security] Invalid element for setTextContentSafe"); }, sanitizeAttribute, setAttributeSafe: function setAttributeSafe(element, attrName, attrValue) { if (!(element && element instanceof HTMLElement)) { console.error("[Security] Invalid element for setAttributeSafe"); return !1; } const sanitizedValue = sanitizeAttribute(attrName, attrValue); if (null === sanitizedValue) { return !1; } try { element.setAttribute(attrName, sanitizedValue); return !0; } catch (error) { console.error("[Security] setAttribute failed:", error); return !1; } }, validateNumber: function validateNumber(value, min = -Infinity, max = Infinity) { const num = Number(value); return isNaN(num) || !isFinite(num) || num < min || num > max ? null : num; }, RateLimiter, fetchWithTimeout: function fetchWithTimeout(url, options = {}, timeout = 1e4) { return Promise.race([ fetch(url, options), new Promise((_, reject) => setTimeout(() => reject(new Error("Request timeout")), timeout)) ]); }, validateJSONSchema: function validateJSONSchema(data, schema) { if (!data || "object" != typeof data) { return !1; } if (!schema || "object" != typeof schema) { return !0; } for (const key in schema) { if (schema[key].required && !(key in data)) { console.warn(`[Security] Missing required field: ${key}`); return !1; } if (key in data && schema[key].type && typeof data[key] !== schema[key].type) { console.warn(`[Security] Invalid type for field ${key}: expected ${schema[key].type}, got ${typeof data[key]}`); return !1; } } return !0; } }; window.YouTubePlusSecurity = window.YouTubeSecurityUtils; } })(); const YouTubeUtils = (() => { "use strict"; window; const Security = window.YouTubePlusSecurity || {}; const Storage = window.YouTubePlusStorage || {}; const Performance = window.YouTubePlusPerformance || {}; const t = window.YouTubeUtils?.t || (key => key || ""); const logError = (module, message, error) => { console.error(`[YouTube+][${module}] ${message}:`, error); }; const safeExecute = Security.safeExecute || ((fn, context = "Unknown") => function(...args) { try { return fn.call(this, ...args); } catch (error) { logError(context, "Execution failed", error); return null; } }); const safeExecuteAsync = Security.safeExecuteAsync || ((fn, context = "Unknown") => async function(...args) { try { return await fn.call(this, ...args); } catch (error) { logError(context, "Async execution failed", error); return null; } }); const sanitizeHTML = Security.sanitizeHTML || (html => "string" != typeof html ? "" : html.replace(/[<>&"'\/`=]/g, "")); const isValidURL = Security.isValidURL || (url => { if ("string" != typeof url) { return !1; } try { const parsed = new URL(url); return [ "http:", "https:" ].includes(parsed.protocol); } catch { return !1; } }); const storage = Storage || { get: (key, defaultValue = null) => { try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch { return defaultValue; } }, set: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); return !0; } catch { return !1; } }, remove: key => { try { localStorage.removeItem(key); return !0; } catch { return !1; } } }; const debounce = window.YouTubeUtils?.debounce || Performance?.debounce || ((func, wait, options = {}) => { let timeout = null; const debounced = function(...args) { null !== timeout && clearTimeout(timeout); options.leading && null === timeout && func.call(this, ...args); timeout = setTimeout(() => { options.leading || func.call(this, ...args); timeout = null; }, wait); }; debounced.cancel = () => { null !== timeout && clearTimeout(timeout); timeout = null; }; return debounced; }); const throttle = window.YouTubeUtils?.throttle || Performance?.throttle || ((func, limit) => { let inThrottle = !1; return function(...args) { if (!inThrottle) { func.call(this, ...args); inThrottle = !0; setTimeout(() => { inThrottle = !1; }, limit); } }; }); const createElement = (tag, props = {}, children = []) => { if (!/^[a-z][a-z0-9-]*$/i.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 ("className" === key) { element.className = value; } else if ("style" === key && "object" == typeof value) { Object.assign(element.style, value); } else if (key.startsWith("on") && "function" == typeof value) { element.addEventListener(key.substring(2).toLowerCase(), value); } else if ("dataset" === key && "object" == typeof value) { Object.assign(element.dataset, value); } else if ("innerHTML" === key || "outerHTML" === key) { 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 => { "string" == typeof child ? element.appendChild(document.createTextNode(child)) : child instanceof Node && element.appendChild(child); }); return element; }; const selectorCache = new Map; const cleanupManager = { observers: new Set, listeners: new Map, intervals: new Set, timeouts: new Set, animationFrames: new Set, cleanupFunctions: new Set, register: fn => { "function" == typeof fn && cleanupManager.cleanupFunctions.add(fn); return fn; }, unregister: fn => { cleanupManager.cleanupFunctions.delete(fn); }, registerObserver: observer => { cleanupManager.observers.add(observer); return observer; }, unregisterObserver: observer => { if (observer) { try { observer.disconnect(); } catch (e) { logError("Cleanup", "Observer disconnect failed", e); } cleanupManager.observers.delete(observer); } }, registerListener: (element, event, handler, options) => { const key = Symbol("listener"); cleanupManager.listeners.set(key, { element, event, handler, options }); try { element.addEventListener(event, handler, options); } catch {} return key; }, unregisterListener: key => { const listener = cleanupManager.listeners.get(key); if (listener) { const {element, event, handler, options} = listener; try { element.removeEventListener(event, handler, options); } catch (e) { logError("Cleanup", "Listener removal failed", e); } cleanupManager.listeners.delete(key); } }, registerInterval: id => { cleanupManager.intervals.add(id); return id; }, unregisterInterval: id => { clearInterval(id); cleanupManager.intervals.delete(id); }, registerTimeout: id => { cleanupManager.timeouts.add(id); return id; }, unregisterTimeout: id => { clearTimeout(id); cleanupManager.timeouts.delete(id); }, registerAnimationFrame: id => { cleanupManager.animationFrames.add(id); return id; }, unregisterAnimationFrame: id => { cancelAnimationFrame(id); cleanupManager.animationFrames.delete(id); }, cleanup: () => { cleanupManager.cleanupFunctions.forEach(fn => { try { fn(); } catch (e) { logError("Cleanup", "Cleanup function failed", e); } }); cleanupManager.cleanupFunctions.clear(); cleanupManager.observers.forEach(obs => { try { obs.disconnect(); } catch (e) { logError("Cleanup", "Observer disconnect failed", e); } }); cleanupManager.observers.clear(); cleanupManager.listeners.forEach(({element, event, handler, options}) => { try { element.removeEventListener(event, handler, options); } catch (e) { logError("Cleanup", "Listener removal failed", e); } }); cleanupManager.listeners.clear(); cleanupManager.intervals.forEach(id => clearInterval(id)); cleanupManager.intervals.clear(); cleanupManager.timeouts.forEach(id => clearTimeout(id)); cleanupManager.timeouts.clear(); cleanupManager.animationFrames.forEach(id => cancelAnimationFrame(id)); cleanupManager.animationFrames.clear(); } }; const SettingsManager = { storageKey: "youtube_plus_all_settings_v2", defaults: { speedControl: { enabled: !0, currentSpeed: 1 }, screenshot: { enabled: !0 }, download: { enabled: !0 }, updateChecker: { enabled: !0 }, adBlocker: { enabled: !0 }, pip: { enabled: !0 }, timecodes: { enabled: !0 } }, load() { const saved = storage.get(this.storageKey); return saved ? { ...this.defaults, ...saved } : { ...this.defaults }; }, save(settings) { storage.set(this.storageKey, settings); window.dispatchEvent(new CustomEvent("youtube-plus-settings-changed", { detail: settings })); }, get(path) { const settings = this.load(); return path.split(".").reduce((obj, key) => obj?.[key], settings); }, set(path, value) { const settings = this.load(); const keys = path.split("."); const last = keys.pop(); const target = keys.reduce((obj, key) => { obj[key] = obj[key] || {}; return obj[key]; }, settings); target[last] = value; this.save(settings); } }; const StyleManager = { styles: new Map, element: null, add(id, css) { if ("string" == typeof id && id) { if ("string" == typeof css) { this.styles.set(id, css); this.update(); } else { logError("StyleManager", "Invalid CSS", new Error("CSS must be a string")); } } else { logError("StyleManager", "Invalid style ID", new Error("ID must be a non-empty string")); } }, remove(id) { this.styles.delete(id); this.update(); }, update() { try { if (!this.element) { this.element = document.createElement("style"); this.element.id = "youtube-plus-styles"; this.element.type = "text/css"; (document.head || document.documentElement).appendChild(this.element); } this.element.textContent = Array.from(this.styles.values()).join("\n"); } catch (error) { logError("StyleManager", "Failed to update styles", error); } }, clear() { this.styles.clear(); if (this.element) { try { this.element.remove(); } catch (e) { logError("StyleManager", "Failed to remove style element", e); } this.element = null; } } }; const NotificationManager = { queue: [], activeNotifications: new Set, maxVisible: 3, defaultDuration: 3e3, show(message, options = {}) { if (!message || "string" != typeof message) { logError("NotificationManager", "Invalid message", new Error("Message must be a non-empty string")); return null; } const {duration = this.defaultDuration, position = null, action = null} = options; this.activeNotifications.forEach(notif => { notif.dataset.message === message && this.remove(notif); }); const positions = { "top-right": { top: "20px", right: "20px" }, "top-left": { top: "20px", left: "20px" }, "bottom-right": { bottom: "20px", right: "20px" }, "bottom-left": { bottom: "20px", left: "20px" } }; try { const notification = createElement("div", { className: "youtube-enhancer-notification", dataset: { message }, style: { zIndex: "10001", width: "auto", display: "flex", alignItems: "center", gap: "10px", ...position && positions[position] ? positions[position] : {} } }); notification.setAttribute("role", "status"); notification.setAttribute("aria-live", "polite"); notification.setAttribute("aria-atomic", "true"); const messageSpan = createElement("span", { style: { flex: "1" } }, [ message ]); notification.appendChild(messageSpan); if (action && action.text && "function" == typeof action.callback) { const actionBtn = createElement("button", { style: { background: "rgba(255,255,255,0.2)", border: "1px solid rgba(255,255,255,0.3)", color: "white", padding: "4px 12px", borderRadius: "4px", cursor: "pointer", fontSize: "12px", fontWeight: "600", transition: "background 0.2s" }, onClick: () => { action.callback(); this.remove(notification); } }, [ action.text ]); notification.appendChild(actionBtn); } const _notifContainerId = "youtube-enhancer-notification-container"; let _notifContainer = document.getElementById(_notifContainerId); if (!_notifContainer) { _notifContainer = createElement("div", { id: _notifContainerId, className: "youtube-enhancer-notification-container" }); try { document.body.appendChild(_notifContainer); } catch { document.body.appendChild(notification); this.activeNotifications.add(notification); } } try { _notifContainer.insertBefore(notification, _notifContainer.firstChild); } catch { document.body.appendChild(notification); } try { notification.style.pointerEvents = "auto"; } catch {} this.activeNotifications.add(notification); try { notification.style.animation = "slideInFromBottom 0.38s ease-out forwards"; } catch {} if (duration > 0) { const timeoutId = setTimeout(() => this.remove(notification), duration); cleanupManager.registerTimeout(timeoutId); } if (this.activeNotifications.size > this.maxVisible) { const oldest = Array.from(this.activeNotifications)[0]; this.remove(oldest); } return notification; } catch (error) { logError("NotificationManager", "Failed to show notification", error); return null; } }, remove(notification) { if (notification && notification.isConnected) { try { try { notification.style.animation = "slideOutToBottom 0.32s ease-in forwards"; const timeoutId = setTimeout(() => { try { notification.remove(); this.activeNotifications.delete(notification); } catch (e) { logError("NotificationManager", "Failed to remove notification", e); } }, 340); cleanupManager.registerTimeout(timeoutId); } catch { try { notification.remove(); this.activeNotifications.delete(notification); } catch (e) { logError("NotificationManager", "Failed to remove notification (fallback)", e); } } } catch (error) { logError("NotificationManager", "Failed to animate notification removal", error); notification.remove(); this.activeNotifications.delete(notification); } } }, clearAll() { this.activeNotifications.forEach(notif => { try { notif.remove(); } catch (e) { logError("NotificationManager", "Failed to clear notification", e); } }); this.activeNotifications.clear(); } }; StyleManager.add("notification-animations", "\n @keyframes slideInFromBottom {\n from { transform: translateY(100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n }\n\n @keyframes slideOutToBottom {\n from { transform: translateY(0); opacity: 1; }\n to { transform: translateY(100%); opacity: 0; }\n }\n "); StyleManager.add("shared-keyframes", "\n @keyframes fadeInModal{from{opacity:0}to{opacity:1}}\n @keyframes scaleInModal{from{transform:scale(0.95);opacity:0}to{transform:scale(1);opacity:1}}\n @keyframes spin{to{transform:rotate(360deg)}}\n @keyframes dash{0%{stroke-dashoffset:80}50%{stroke-dashoffset:10}100%{stroke-dashoffset:80}}\n "); window.addEventListener("beforeunload", () => { cleanupManager.cleanup(); selectorCache.clear(); StyleManager.clear(); NotificationManager.clearAll(); }); const cacheCleanup = () => { const now = Date.now(); for (const [key, value] of selectorCache.entries()) { (!value.element?.isConnected || now - value.timestamp > 1e4) && selectorCache.delete(key); } }; const cacheCleanupInterval = setInterval(() => { "function" == typeof requestIdleCallback ? requestIdleCallback(cacheCleanup, { timeout: 2e3 }) : cacheCleanup(); }, 3e4); cleanupManager.registerInterval(cacheCleanupInterval); cleanupManager.registerListener(window, "unhandledrejection", event => { logError("Global", "Unhandled promise rejection", event.reason); event.preventDefault(); }); cleanupManager.registerListener(window, "error", event => { const message = String(event?.message || ""); const errorMessage = String(event?.error?.message || ""); message.includes("ResizeObserver loop") || errorMessage.includes("ResizeObserver loop") || event.filename && event.filename.includes("youtube") && logError("Global", "Uncaught error", new Error(`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`)); }); return { logError, safeExecute, safeExecuteAsync, sanitizeHTML, isValidURL, storage, debounce, throttle, createElement, querySelector: (selector, nocache = !1) => { if (nocache) { return document.querySelector(selector); } const now = Date.now(); const cached = selectorCache.get(selector); if (cached?.element?.isConnected && now - cached.timestamp < 1e4) { return cached.element; } cached && selectorCache.delete(selector); const element = document.querySelector(selector); if (element) { if (selectorCache.size >= 100) { const firstKey = selectorCache.keys().next().value; selectorCache.delete(firstKey); } selectorCache.set(selector, { element, timestamp: now }); } return element; }, waitForElement: (selector, timeout = 5e3, parent = document.body) => new Promise((resolve, reject) => { const validationError = ((selector, parent) => selector && "string" == typeof selector ? parent && parent instanceof Element ? null : new Error("Parent must be a valid DOM element") : new Error("Selector must be a non-empty string"))(selector, parent); if (validationError) { reject(validationError); return; } const {element, error} = ((parent, selector) => { try { const element = parent.querySelector(selector); return { element, error: null }; } catch { return { element: null, error: new Error(`Invalid selector: ${selector}`) }; } })(parent, selector); if (error) { reject(error); return; } if (element) { resolve(element); return; } const controller = new AbortController; let observer = null; const timeoutId = setTimeout(() => { ((observer, timeoutId, controller) => { controller.abort(); if (observer) { try { observer.disconnect(); } catch (e) { logError("waitForElement", "Observer disconnect failed", e); } } clearTimeout(timeoutId); })(observer, timeoutId, controller); reject(new Error(`Element ${selector} not found within ${timeout}ms`)); }, timeout); observer = ((parent, selector, resolve, timeoutId) => new MutationObserver(() => { try { const element = parent.querySelector(selector); if (element) { clearTimeout(timeoutId); resolve(element); } } catch (e) { logError("waitForElement", "Observer callback error", e); } }))(parent, selector, resolve, timeoutId); const observeError = ((observer, parent) => { try { if (!(parent instanceof Element) && parent !== document) { throw new Error("Parent does not support observation"); } observer.observe(parent, { childList: !0, subtree: !0 }); return null; } catch { try { observer.observe(parent, { childList: !0, subtree: !0 }); return null; } catch { return new Error("Failed to observe DOM"); } } })(observer, parent); if (observeError) { clearTimeout(timeoutId); reject(observeError); } }), cleanupManager, SettingsManager, StyleManager, NotificationManager, clearCache: () => selectorCache.clear(), isMobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768, getViewport: () => ({ width: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0), height: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) }), retryAsync: async (fn, retries = 3, delay = 1e3) => { 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)); }); } } }, measurePerformance: (label, fn) => function(...args) { const start = performance.now(); try { const result = fn.apply(this, args); const duration = performance.now() - start; duration > 100 && console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`); return result; } catch (error) { logError("Performance", `${label} failed`, error); throw error; } }, measurePerformanceAsync: (label, fn) => async function(...args) { const start = performance.now(); try { const result = await fn.apply(this, args); const duration = performance.now() - start; duration > 100 && console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`); return result; } catch (error) { logError("Performance", `${label} failed`, error); throw error; } }, t }; })(); if ("undefined" != typeof window) { window.YouTubeUtils = window.YouTubeUtils || {}; const existing = window.YouTubeUtils; try { for (const k of Object.keys(YouTubeUtils)) { void 0 === existing[k] && (existing[k] = YouTubeUtils[k]); } } catch (e) { console.error("[YouTube+] Failed to merge core utilities:", e); } window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+ v2.4.5] Core utilities merged"); window.YouTubePlusDebug = { version: "2.4.5", 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(); window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+] All resources cleared"); }, stats: () => ({ observers: YouTubeUtils.cleanupManager.observers.size, listeners: YouTubeUtils.cleanupManager.listeners.size, intervals: YouTubeUtils.cleanupManager.intervals.size, timeouts: YouTubeUtils.cleanupManager.timeouts.size, animationFrames: YouTubeUtils.cleanupManager.animationFrames.size, styles: YouTubeUtils.StyleManager.styles.size, notifications: YouTubeUtils.NotificationManager.activeNotifications.size }) }; if (!sessionStorage.getItem("youtube_plus_started")) { sessionStorage.setItem("youtube_plus_started", "true"); setTimeout(() => { YouTubeUtils.NotificationManager && YouTubeUtils.NotificationManager.show("YouTube+ v2.4.5 loaded", { type: "success", duration: 2e3, position: "bottom-right" }); }, 1e3); } } !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const {t} = YouTubeUtils; const YouTubeEnhancer = { speedControl: { currentSpeed: 1, activeAnimationId: null, storageKey: "youtube_playback_speed", availableSpeeds: [ .25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3 ] }, loopControl: { enabled: !1, pointA: null, pointB: null, storageKey: "youtube_loop_state", timeUpdateListener: null }, _initialized: !1, settings: { enableSpeedControl: !0, speedControlHotkeys: { decrease: "g", increase: "h", reset: "b" }, enableScreenshot: !0, enableDownload: !0, enableZenStyles: !0, zenStyles: { thumbnailHover: !0, immersiveSearch: !0, hideVoiceSearch: !0, transparentHeader: !0, hideSideGuide: !0, cleanSideGuide: !1, fixFeedLayout: !0, betterCaptions: !0, playerBlur: !0, theaterEnhancements: !0, misc: !0 }, enableEnhanced: !0, enablePlayAll: !0, enableResumeTime: !0, enableZoom: !0, enableThumbnail: !0, enablePlaylistSearch: !0, enableScrollToTopButton: !0, enableLoop: !0, loopHotkeys: { toggleLoop: "r", setPointA: "k", setPointB: "l", resetPoints: "o" }, downloadSites: { direct: !0, externalDownloader: !0, ytdl: !0 }, downloadSiteCustomization: { externalDownloader: "undefined" != typeof window && window.YouTubePlusConstants ? window.YouTubePlusConstants.DOWNLOAD_SITES.EXTERNAL_DOWNLOADER : { name: "SSYouTube", url: "https://ssyoutube.com/watch?v={videoId}" } }, storageKey: window.YouTubeUtils?.SETTINGS_KEY || "youtube_plus_settings", hideSideGuide: !1 }, _cache: new Map, getElement(selector, useCache = !0) { 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); element && useCache && this._cache.set(selector, element); return element; }, loadSettings() { try { const saved = localStorage.getItem(this.settings.storageKey); if (saved) { const parsed = JSON.parse(saved); if (window.YouTubeUtils && window.YouTubeUtils.safeMerge) { window.YouTubeUtils.safeMerge(this.settings, parsed); } else { for (const key in parsed) { Object.prototype.hasOwnProperty.call(parsed, key) && ![ "__proto__", "constructor", "prototype" ].includes(key) && (this.settings[key] = parsed[key]); } } return; } try { if ("undefined" != typeof window && window.YouTubeUtils && YouTubeUtils.SettingsManager) { const globalSettings = YouTubeUtils.SettingsManager.load(); if (!globalSettings) { return; } const sc = globalSettings.speedControl; sc && "boolean" == typeof sc.enabled && (this.settings.enableSpeedControl = sc.enabled); const ss = globalSettings.screenshot; ss && "boolean" == typeof ss.enabled && (this.settings.enableScreenshot = ss.enabled); const dl = globalSettings.download; dl && "boolean" == typeof dl.enabled && (this.settings.enableDownload = dl.enabled); globalSettings.downloadSites && "object" == typeof globalSettings.downloadSites && (this.settings.downloadSites = { ...this.settings.downloadSites || {}, ...globalSettings.downloadSites }); } } catch {} } catch (e) { console.error("Error loading settings:", e); } }, init() { if (!this._initialized) { this._initialized = !0; try { this.loadSettings(); try { const lh = this.settings.loopHotkeys || {}; let migrated = !1; if ("l" === lh.setPointA) { lh.setPointA = "k"; migrated = !0; } if ("o" === lh.setPointB) { lh.setPointB = "l"; migrated = !0; } if ("k" === lh.resetPoints) { lh.resetPoints = "o"; migrated = !0; } if (migrated) { this.settings.loopHotkeys = lh; try { this.saveSettings(); } catch (e) { console.warn("[YouTube+] Failed to save migrated loop hotkeys", e); } } } catch {} this.settings.speedControlHotkeys = this.settings.speedControlHotkeys || {}; this.settings.speedControlHotkeys.decrease = this.normalizeSpeedHotkey(this.settings.speedControlHotkeys.decrease, "g"); this.settings.speedControlHotkeys.increase = this.normalizeSpeedHotkey(this.settings.speedControlHotkeys.increase, "h"); this.settings.speedControlHotkeys.reset = this.normalizeSpeedHotkey(this.settings.speedControlHotkeys.reset, "b"); try { const savedSpeed = localStorage.getItem(this.speedControl.storageKey); if (null !== savedSpeed) { const parsed = Number(savedSpeed); Number.isFinite(parsed) && parsed > 0 && parsed <= 16 && (this.speedControl.currentSpeed = parsed); } } catch (e) { console.warn("[YouTube+] Speed restore error:", e); } this.settings.loopHotkeys = this.settings.loopHotkeys || {}; this.settings.loopHotkeys.toggleLoop = this.normalizeSpeedHotkey(this.settings.loopHotkeys.toggleLoop, "r"); this.settings.loopHotkeys.setPointA = this.normalizeSpeedHotkey(this.settings.loopHotkeys.setPointA, "k"); this.settings.loopHotkeys.setPointB = this.normalizeSpeedHotkey(this.settings.loopHotkeys.setPointB, "l"); this.settings.loopHotkeys.resetPoints = this.normalizeSpeedHotkey(this.settings.loopHotkeys.resetPoints, "o"); this.loadLoopState(); } catch (error) { console.warn("[YouTube+][Basic]", "Failed to load settings during init:", error); } this.insertStyles(); this.addSettingsButtonToHeader(); this.setupNavigationObserver(); location.href.includes("watch?v=") && this.setupCurrentPage(); YouTubeUtils.cleanupManager.registerListener(document, "visibilitychange", () => { !document.hidden && location.href.includes("watch?v=") && this.setupCurrentPage(); }); try { const screenshotKeyHandler = e => { if (e && e.key && ("s" === e.key || "S" === e.key) && !(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) && !this.isEditableTarget(document.activeElement) && this.settings.enableScreenshot) { try { this.captureFrame(); } catch (err) { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Basic", "Keyboard screenshot failed", err); } } }; YouTubeUtils.cleanupManager.registerListener(document, "keydown", screenshotKeyHandler, !0); } catch (e) { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Basic", "Failed to register screenshot keyboard shortcut", e); } try { const speedHotkeyHandler = e => { if (!this.settings.enableSpeedControl || !e || !e.key) { return; } if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } if (this.isEditableTarget(document.activeElement)) { return; } const key = String(e.key).toLowerCase(); const decreaseKey = this.normalizeSpeedHotkey(this.settings.speedControlHotkeys?.decrease, "g"); const increaseKey = this.normalizeSpeedHotkey(this.settings.speedControlHotkeys?.increase, "h"); const resetKey = this.normalizeSpeedHotkey(this.settings.speedControlHotkeys?.reset, "b"); if (key === decreaseKey) { e.preventDefault(); this.adjustSpeedByStep(-1); } else if (key === increaseKey) { e.preventDefault(); this.adjustSpeedByStep(1); } else if (key === resetKey) { e.preventDefault(); this.changeSpeed(1); } }; YouTubeUtils.cleanupManager.registerListener(document, "keydown", speedHotkeyHandler, !0); } catch (e) { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Basic", "Failed to register speed keyboard shortcuts", e); } try { const loopHotkeyHandler = e => { if (!this.settings.enableLoop || !e || !e.key) { return; } if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } if (this.isEditableTarget(document.activeElement)) { return; } const key = String(e.key).toLowerCase(); const toggleLoopKey = this.normalizeSpeedHotkey(this.settings.loopHotkeys?.toggleLoop, "r"); const setPointAKey = this.normalizeSpeedHotkey(this.settings.loopHotkeys?.setPointA, "k"); const setPointBKey = this.normalizeSpeedHotkey(this.settings.loopHotkeys?.setPointB, "l"); const resetPointsKey = this.normalizeSpeedHotkey(this.settings.loopHotkeys?.resetPoints, "o"); if (key === toggleLoopKey) { e.preventDefault(); this.toggleLoop(); } else if (key === setPointAKey) { e.preventDefault(); this.setLoopPoint("A"); } else if (key === setPointBKey) { e.preventDefault(); this.setLoopPoint("B"); } else if (key === resetPointsKey) { e.preventDefault(); this.resetLoopPoints(); } }; YouTubeUtils.cleanupManager.registerListener(document, "keydown", loopHotkeyHandler, !0); } catch (e) { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Basic", "Failed to register loop keyboard shortcuts", e); } } }, isEditableTarget(target) { const active = target; if (!active) { return !1; } const tag = (active.tagName || "").toLowerCase(); return "input" === tag || "textarea" === tag || "select" === tag || Boolean(active.isContentEditable); }, normalizeSpeedHotkey(value, fallback) { const candidate = "string" == typeof value ? value.trim().toLowerCase() : ""; return candidate ? candidate.slice(0, 1) : String(fallback || "").trim().toLowerCase().slice(0, 1) || "g"; }, adjustSpeedByStep(direction) { const speeds = this.speedControl.availableSpeeds; if (!Array.isArray(speeds) || !speeds.length) { return; } const current = Number(this.speedControl.currentSpeed); let closestIndex = 0; let closestDelta = Number.POSITIVE_INFINITY; for (let i = 0; i < speeds.length; i += 1) { const delta = Math.abs(speeds[i] - current); if (delta < closestDelta) { closestDelta = delta; closestIndex = i; } } const step = direction > 0 ? 1 : -1; const nextIndex = Math.max(0, Math.min(speeds.length - 1, closestIndex + step)); nextIndex !== closestIndex && this.changeSpeed(speeds[nextIndex]); }, toggleLoop() { if (!this.settings.enableLoop) { return; } this.loopControl.enabled = !this.loopControl.enabled; const video = document.querySelector("video"); if (video) { if (this.loopControl.enabled) { if (null === this.loopControl.pointA && null === this.loopControl.pointB) { video.loop = !0; } else { video.loop = !1; this.setupLoopListener(video); } YouTubeUtils.NotificationManager.show(t("loopEnabled") || "Loop enabled", { duration: 1500, type: "success" }); } else { video.loop = !1; this.removeLoopListener(); YouTubeUtils.NotificationManager.show(t("loopDisabled") || "Loop disabled", { duration: 1500, type: "info" }); } this.updateLoopProgressBar(); this.saveLoopState(); } else { this.saveLoopState(); } }, setLoopPoint(point) { if (!this.settings.enableLoop) { return; } const video = document.querySelector("video"); if (!video) { return; } const currentTime = video.currentTime; if ("A" === point) { this.loopControl.pointA = currentTime; YouTubeUtils.NotificationManager.show(`${t("loopPointASet") || "Point A set"}: ${this.formatTime(currentTime)}`, { duration: 1500, type: "success" }); } else if ("B" === point) { this.loopControl.pointB = currentTime; YouTubeUtils.NotificationManager.show(`${t("loopPointBSet") || "Point B set"}: ${this.formatTime(currentTime)}`, { duration: 1500, type: "success" }); } if (this.loopControl.enabled && null !== this.loopControl.pointA && null !== this.loopControl.pointB) { const video = document.querySelector("video"); if (video) { video.loop = !1; this.setupLoopListener(video); } } this.updateLoopProgressBar(); this.saveLoopState(); }, resetLoopPoints() { if (this.settings.enableLoop) { this.loopControl.pointA = null; this.loopControl.pointB = null; if (this.loopControl.enabled) { const video = document.querySelector("video"); if (video) { video.loop = !0; this.removeLoopListener(); } } YouTubeUtils.NotificationManager.show(t("loopPointsReset") || "Loop points reset", { duration: 1500, type: "info" }); this.updateLoopProgressBar(); this.saveLoopState(); } }, setupLoopListener(video) { this.removeLoopListener(); if (null === this.loopControl.pointA || null === this.loopControl.pointB) { return; } const startTime = Math.min(this.loopControl.pointA, this.loopControl.pointB); const endTime = Math.max(this.loopControl.pointA, this.loopControl.pointB); this.loopControl.timeUpdateListener = () => { this.loopControl.enabled && video.currentTime >= endTime && (video.currentTime = startTime); }; video.addEventListener("timeupdate", this.loopControl.timeUpdateListener); }, removeLoopListener() { if (this.loopControl.timeUpdateListener) { const video = document.querySelector("video"); video && video.removeEventListener("timeupdate", this.loopControl.timeUpdateListener); this.loopControl.timeUpdateListener = null; } }, updateLoopProgressBar() { if (null === this.loopControl.pointA && null === this.loopControl.pointB) { const existingIndicator = document.querySelector(".ytp-plus-loop-indicator"); existingIndicator && existingIndicator.remove(); return; } const video = document.querySelector("video"); if (!video || !video.duration) { return; } let progressBar = document.querySelector(".ytp-progress-bar-container") || document.querySelector(".ytp-scrubber-container") || document.querySelector('[role="slider"][aria-label*="video"]') || document.querySelector(".html5-progress-bar"); if (!progressBar) { const playbackUI = document.querySelector(".html5-video-player"); playbackUI && (progressBar = playbackUI.querySelector('[role="slider"]')); } if (!progressBar) { return; } let indicator = document.querySelector(".ytp-plus-loop-indicator"); if (!indicator) { indicator = document.createElement("div"); indicator.className = "ytp-plus-loop-indicator"; try { const compStyle = window.getComputedStyle(progressBar); compStyle && "static" !== compStyle.position || (progressBar.style.position = "relative"); } catch {} progressBar.appendChild(indicator); indicator.style.position = "absolute"; indicator.style.top = "0"; indicator.style.height = "100%"; indicator.style.pointerEvents = "none"; indicator.style.zIndex = "1000"; } if (null !== this.loopControl.pointA && null === this.loopControl.pointB) { const startPercent = this.loopControl.pointA / video.duration * 100; indicator.style.left = `${startPercent}%`; indicator.style.width = "2px"; indicator.style.background = "linear-gradient(90deg,#1976d2,#42a5f5)"; indicator.style.borderLeft = "2px solid #1976d2"; indicator.style.borderRight = "2px solid #1976d2"; indicator.style.display = "block"; return; } if (null !== this.loopControl.pointB && null === this.loopControl.pointA) { const bPercent = this.loopControl.pointB / video.duration * 100; indicator.style.left = `${bPercent}%`; indicator.style.width = "2px"; indicator.style.background = "linear-gradient(90deg,#1976d2,#42a5f5)"; indicator.style.borderLeft = "2px solid #1976d2"; indicator.style.borderRight = "2px solid #1976d2"; indicator.style.display = "block"; return; } const startTime = Math.min(this.loopControl.pointA, this.loopControl.pointB); const endTime = Math.max(this.loopControl.pointA, this.loopControl.pointB); const startPercent = startTime / video.duration * 100; const endPercent = endTime / video.duration * 100; indicator.style.left = `${startPercent}%`; indicator.style.width = `${Math.max(.2, endPercent - startPercent)}%`; indicator.style.background = "linear-gradient(90deg,rgba(25,118,210,0.28) 0%,rgba(66,165,245,0.4) 50%,rgba(25,118,210,0.28) 100%)"; indicator.style.borderLeft = "2px solid #1976d2"; indicator.style.borderRight = "2px solid #1976d2"; indicator.style.display = "block"; }, applyLoopStateToCurrentVideo() { const video = document.querySelector("video"); if (video) { this.removeLoopListener(); if (this.settings.enableLoop && this.loopControl.enabled) { if (null !== this.loopControl.pointA && null !== this.loopControl.pointB) { video.loop = !1; this.setupLoopListener(video); } else { video.loop = !0; } this.updateLoopProgressBar(); } else { video.loop = !1; this.updateLoopProgressBar(); } } }, saveLoopState() { try { const state = { enabled: this.loopControl.enabled, pointA: this.loopControl.pointA, pointB: this.loopControl.pointB }; localStorage.setItem(this.loopControl.storageKey, JSON.stringify(state)); } catch (e) { console.warn("[YouTube+] Failed to save loop state:", e); } }, loadLoopState() { try { const saved = localStorage.getItem(this.loopControl.storageKey); if (saved) { const state = JSON.parse(saved); this.loopControl.enabled = Boolean(state?.enabled); this.loopControl.pointA = "number" == typeof state?.pointA && Number.isFinite(state.pointA) ? state.pointA : null; this.loopControl.pointB = "number" == typeof state?.pointB && Number.isFinite(state.pointB) ? state.pointB : null; setTimeout(() => this.applyLoopStateToCurrentVideo(), 1e3); } } catch (e) { console.warn("[YouTube+] Failed to load loop state:", e); } }, formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; }, saveSettings() { localStorage.setItem(this.settings.storageKey, JSON.stringify(this.settings)); this.updatePageBasedOnSettings(); this.refreshDownloadButton(); try { window.youtubePlus = window.youtubePlus || {}; window.youtubePlus.settings = this.settings; window.dispatchEvent(new CustomEvent("youtube-plus-settings-updated", { detail: this.settings })); } catch (e) { console.warn("[YouTube+] Settings broadcast error:", e); } }, updatePageBasedOnSettings() { Object.entries({ "ytp-screenshot-button": "enableScreenshot", "ytp-download-button": "enableDownload", "speed-control-btn": "enableSpeedControl" }).forEach(([className, setting]) => { const button = this.getElement(`.${className}`, !1); button && (button.style.display = this.settings[setting] ? "" : "none"); }); const speedOptions = document.querySelector(".speed-options"); speedOptions && (speedOptions.style.display = this.settings.enableSpeedControl ? "" : "none"); }, refreshDownloadButton() { if ("undefined" != typeof window && window.YouTubePlusDownloadButton) { const manager = window.YouTubePlusDownloadButton.createDownloadButtonManager({ settings: this.settings, t, getElement: this.getElement.bind(this), YouTubeUtils }); manager.refreshDownloadButton(); } }, setupCurrentPage() { this.waitForElement("#player-container-outer .html5-video-player, .ytp-right-controls", 5e3).then(() => { this.addCustomButtons(); this.setupVideoObserver(); this.applyCurrentSpeed(); this.applyLoopStateToCurrentVideo(); this.updatePageBasedOnSettings(); this.refreshDownloadButton(); }).catch(() => {}); }, insertStyles() { const injectNonCritical = () => { if (!document.getElementById("yt-enhancer-nc-styles")) { const ncEl = document.createElement("style"); ncEl.id = "yt-enhancer-nc-styles"; ncEl.textContent = '\n .ytp-plus-settings-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:100000;backdrop-filter:blur(8px) saturate(140%);-webkit-backdrop-filter:blur(8px) saturate(140%);animation:ytEnhanceFadeIn .25s ease-out;contain:layout style paint;}\n .ytp-plus-settings-panel{background:var(--yt-glass-bg);color:var(--yt-text-primary);border-radius:20px;width:760px;max-width:94%;max-height:60vh;overflow:hidden;box-shadow:0 12px 40px rgba(0,0,0,0.45);animation:ytEnhanceScaleIn .28s cubic-bezier(.4,0,.2,1);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%);border:1.5px solid var(--yt-glass-border);will-change:transform,opacity;display:flex;flex-direction:row;contain:layout style paint;}\n .ytp-plus-settings-sidebar{width:240px;background:var(--yt-header-bg);border-right:1px solid var(--yt-glass-border);display:flex;flex-direction:column;backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);}\n .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;}\n .ytp-plus-settings-title{font-size:18px;font-weight:500;margin:0;color:var(--yt-text-primary);}\n .ytp-plus-settings-sidebar-close{padding:var(--yt-space-md) var(--yt-space-lg);display:flex;justify-content:flex-end;background:transparent;}\n .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;}\n .ytp-plus-settings-close:hover{color:var(--yt-accent);transform:scale(1.25) rotate(90deg);}\n .ytp-plus-settings-nav{flex:1;padding:var(--yt-space-md) 0;}\n .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);}\n .ytp-plus-settings-nav-item:hover{background:var(--yt-hover-bg);}\n .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;}\n .ytp-plus-settings-nav-item svg{width:18px;height:18px;margin-right:12px;opacity:.8;transition:opacity .2s,transform .2s;}\n .ytp-plus-settings-nav-item.active svg{opacity:1;transform:scale(1.1);}\n .ytp-plus-settings-nav-item:hover svg{transform:scale(1.05);}\n .ytp-plus-settings-main{flex:1;display:flex;flex-direction:column;overflow-y:auto;}\n .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);}\n .ytp-plus-settings-content{flex:1;padding:var(--yt-space-md) var(--yt-space-lg);overflow-y:auto;}\n .ytp-plus-settings-section{margin-bottom:var(--yt-space-lg);}\n .ytp-plus-settings-section-title{font-size:16px;font-weight:500;margin-bottom:var(--yt-space-md);color:var(--yt-text-primary);}\n .ytp-plus-settings-section.hidden{display:none !important;}\n .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);}\n .ytp-plus-settings-item:hover{background:var(--yt-hover-bg);transform:translateX(6px);box-shadow:0 2px 8px rgba(0,0,0,.1);}\n .ytp-plus-settings-item-actions{display:flex;align-items:center;gap:10px;margin-left:auto;}\n .ytp-plus-submenu-toggle{width:26px;height:26px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:1px solid var(--yt-glass-border);color:var(--yt-text-primary);cursor:pointer;opacity:.9;transition:transform .15s ease,background-color .15s ease,opacity .15s ease;}\n .ytp-plus-submenu-toggle:hover{background:var(--yt-hover-bg);transform:scale(1.06);}\n .ytp-plus-submenu-toggle:disabled{opacity:.35;cursor:not-allowed;transform:none;}\n .ytp-plus-submenu-toggle svg{width:16px;height:16px;transition:transform .15s ease;}\n .ytp-plus-submenu-toggle[aria-expanded="false"] svg{transform:rotate(-90deg);}\n .ytp-plus-submenu-toggle[aria-expanded="true"] svg{transform:rotate(0deg);}\n .ytp-plus-settings-item-label{flex:1;font-size:14px;color:var(--yt-text-primary);}\n .ytp-plus-settings-item-description{font-size:12px;color:var(--yt-text-secondary);margin-top:4px;}\n .ytp-plus-settings-checkbox{appearance:none;-webkit-appearance:none;-moz-appearance:none;width:20px;height:20px;min-width:20px;min-height:20px;margin-left:auto;border:2px 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;box-sizing:border-box;}\n html:not([dark]) .ytp-plus-settings-checkbox{border-color:rgba(0,0,0,.25);color:#222;}\n .ytp-plus-settings-checkbox:focus-visible{outline:2px solid var(--yt-accent);outline-offset:2px;}\n .ytp-plus-settings-checkbox:hover{background:var(--yt-hover-bg);transform:scale(1.1);}\n .ytp-plus-settings-checkbox::before{content:"";width:5px;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(45deg);top:6px;left:3px;transition:width 100ms ease 50ms,opacity 50ms;transform-origin:0% 0%;opacity:0;}\n .ytp-plus-settings-checkbox::after{content:"";width:0;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(305deg);top:12px;left:7px;transition:width 100ms ease,opacity 50ms;transform-origin:0% 0%;opacity:0;}\n .ytp-plus-settings-checkbox:checked{transform:rotate(0deg) scale(1.15);}\n .ytp-plus-settings-checkbox:checked::before{width:9px;opacity:1;background:#fff;transition:width 150ms ease 100ms,opacity 150ms ease 100ms;}\n .ytp-plus-settings-checkbox:checked::after{width:16px;opacity:1;background:#fff;transition:width 150ms ease 250ms,opacity 150ms ease 250ms;}\n .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;}\n .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);}\n .ytp-plus-button-primary{background:transparent;border:1px solid var(--yt-glass-border);color:var(--yt-text-primary);}\n .ytp-plus-button-primary:hover{background:var(--yt-accent);color:#fff;box-shadow:0 6px 16px rgba(255,0,0,.35);transform:translateY(-2px);}\n .app-icon{fill:var(--yt-text-primary);stroke:var(--yt-text-primary);transition:all .3s;}\n @media(max-width:768px){.ytp-plus-settings-panel{width:95%;max-height:80vh;flex-direction:column;}\n .ytp-plus-settings-sidebar{width:100%;max-height:120px;flex-direction:row;overflow-x:auto;}\n .ytp-plus-settings-nav{display:flex;flex-direction:row;padding:0;}\n .ytp-plus-settings-nav-item{white-space:nowrap;border-left:none;border-bottom:3px solid transparent;}\n .ytp-plus-settings-nav-item.active{border-left:none;border-bottom-color:var(--yt-accent);}\n .ytp-plus-settings-item{padding:10px 12px;}}\n .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;}\n .ytp-plus-settings-section h1:hover{color:var(--yt-accent);-webkit-text-stroke-width:1px;-webkit-text-stroke-color:transparent;}\n .download-options{position:fixed;background:var(--yt-glass-bg);color:var(--yt-text-primary);border-radius:var(--yt-radius-md);width:150px;z-index:2147483647;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);overflow:hidden;opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease;transform:translateY(8px);box-sizing:border-box;}\n .download-options.visible{opacity:1;pointer-events:auto;transform:translateY(0);backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);}\n .download-options-list{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;}\n .download-option-item{cursor:pointer;padding:12px;text-align:center;transition:background .2s,color .2s;width:100%;}\n .download-option-item:hover{background:var(--yt-hover-bg);color:var(--yt-accent);}\n .glass-panel{background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);box-shadow:var(--yt-glass-shadow);}\n .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);box-shadow:var(--yt-shadow);}\n .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;}\n .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;}\n .glass-button:hover{background:var(--yt-hover-bg);transform:translateY(-1px);box-shadow:var(--yt-shadow);}\n .download-submenu{margin:4px 0 12px 12px;}\n .download-submenu-container{display:flex;flex-direction:column;gap:8px;}\n .style-submenu{margin:4px 0 12px 12px;}\n .style-submenu-container{display:flex;flex-direction:column;gap:8px;}\n .speed-submenu{margin:4px 0 12px 12px;}\n .speed-submenu-container{display:flex;flex-direction:column;gap:8px;}\n .speed-hotkeys-row{flex-direction:column!important;align-items:stretch!important;gap:6px;}\n .speed-hotkeys-info{display:flex;flex-direction:column;gap:4px;}\n .speed-hotkeys-fields{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-top:12px;width:100%;}\n .speed-hotkey-field{display:flex;flex-direction:column;align-items:center;gap:8px;font-size:12px;color:var(--yt-text-secondary);flex:1;min-width:80px;}\n .speed-hotkey-field span{text-align:center;width:100%;}\n .speed-hotkey-input{width:100%;height:36px;border-radius:8px;border:1px solid var(--yt-glass-border);background:var(--yt-glass-bg);color:var(--yt-text-primary);text-align:center;text-transform:uppercase;}\n .speed-hotkey-input:focus{background:var(--yt-hover-bg);}\n .loop-submenu-container{display:flex;flex-direction:column;gap:8px;}\n .loop-hotkeys-row{flex-direction:column!important;align-items:stretch!important;gap:6px;}\n .loop-hotkeys-info{display:flex;flex-direction:column;gap:4px;}\n .loop-hotkeys-fields{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-top:12px;width:100%;}\n .loop-hotkey-field{display:flex;flex-direction:column;align-items:center;gap:8px;font-size:12px;color:var(--yt-text-secondary);flex:1;min-width:80px;}\n .loop-hotkey-field span{text-align:center;width:100%;}\n .loop-hotkey-input{width:100%;height:36px;border-radius:8px;border:1px solid var(--yt-glass-border);background:var(--yt-glass-bg);color:var(--yt-text-primary);text-align:center;text-transform:uppercase;}\n .loop-hotkey-input:focus{background:var(--yt-hover-bg);}\n .download-site-option{display:flex;flex-direction:column;align-items:stretch;gap:8px;padding:10px;border-radius:var(--yt-radius-md);transition:background .2s;}\n .download-site-option:hover{background:var(--yt-hover-bg);}\n .download-site-header{display:flex;flex-direction:row;align-items:center;justify-content:space-between;width:100%;gap:12px;}\n .download-site-label{flex:1;cursor:pointer;display:flex;flex-direction:column;}\n .download-site-controls{width:100%;margin-top:4px;padding-top:10px;border-top:1px solid var(--yt-glass-border);}\n .download-site-input{width:95%;margin-top:8px;padding:8px;background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-sm);color:var(--yt-text-primary);font-size:13px;transition:all .2s;}\n .download-site-input:focus{border-color:var(--yt-accent);background:var(--yt-hover-bg);}\n .download-site-input.small{margin-top:6px;font-size:12px;}\n .download-site-cta{display:flex;flex-direction:row;gap:8px;margin-top:10px;}\n .download-site-cta .glass-button{flex:1;justify-content:center;font-size:13px;padding:8px 12px;}\n .download-site-cta .glass-button.danger{background:rgba(255,59,59,0.15);border-color:rgba(255,59,59,0.3);}\n .download-site-cta .glass-button.danger:hover{background:rgba(255,59,59,0.25);}\n .download-site-option .ytp-plus-settings-checkbox{margin:0;}\n .download-site-name{font-weight:500;font-size:15px;color:var(--yt-text-primary);}\n .download-site-desc{font-size:12px;color:var(--yt-text-secondary);margin-top:2px;opacity:0.8;}\n .ytp-plus-settings-panel select,\n .ytp-plus-settings-panel select option {background: var(--yt-panel-bg) !important; color: var(--yt-text-primary) !important;}\n .ytp-plus-settings-panel select {-webkit-appearance: menulist !important; appearance: menulist !important; padding: 6px 8px !important; border-radius: 6px !important; border: 1px solid var(--yt-glass-border) !important;}\n .glass-dropdown{position:relative;display:inline-block;min-width:110px}\n .glass-dropdown__toggle{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:6px 8px;border-radius:8px;background:linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));color:inherit;border:1px solid rgba(255,255,255,0.06);cursor:pointer}\n .glass-dropdown__toggle:focus{outline:2px solid rgba(255,255,255,0.06)}\n .glass-dropdown__label{font-size:12px}\n .glass-dropdown__chev{opacity:0.9}\n .glass-dropdown__list{position:absolute;left:0;right:0;top:calc(100% + 8px);z-index:20000;display:none;margin:0;padding:6px;border-radius:10px;list-style:none;background:var(--yt-header-bg);border:1px solid rgba(255,255,255,0.06);box-shadow:0 8px 30px rgba(0,0,0,0.5);backdrop-filter:blur(10px) saturate(130%);-webkit-backdrop-filter:blur(10px) saturate(130%);max-height:220px;overflow:auto}\n .glass-dropdown__item{padding:8px 10px;border-radius:6px;margin:4px 0;cursor:pointer;color:inherit;font-size:13px}\n .glass-dropdown__item:hover{background:rgba(255,255,255,0.04)}\n .glass-dropdown__item[aria-selected="true"]{background:linear-gradient(90deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));box-shadow:inset 0 0 0 1px rgba(255,255,255,0.02)}\n .ytp-plus-settings-voting-header{margin-bottom:var(--yt-space-lg);}\n .ytp-plus-settings-voting-header h3{font-size:18px;font-weight:500;margin:0 0 8px 0;color:var(--yt-text-primary);}\n .ytp-plus-settings-voting-desc{font-size:13px;color:var(--yt-text-secondary);margin:0;}\n .ytp-plus-voting{display:flex;flex-direction:column;gap:12px;}\n .ytp-plus-voting-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;}\n .ytp-plus-voting-list{display:flex;flex-direction:column;gap:12px;}\n .ytp-plus-voting-item{display:flex;align-items:flex-start;justify-content:space-between;padding:16px;background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);transition:all .2s ease;gap:12px;}\n .ytp-plus-voting-item:hover{background:var(--yt-hover-bg);transform:translateX(4px);}\n .ytp-plus-voting-item-content{flex:1;padding-right:16px;}\n .ytp-plus-voting-item-title{font-size:14px;font-weight:500;color:var(--yt-text-primary);margin-bottom:4px;}\n .ytp-plus-voting-item-desc{font-size:12px;color:var(--yt-text-secondary);line-height:1.4;}\n .ytp-plus-voting-item-status{font-size:11px;padding:2px 8px;border-radius:10px;display:inline-block;margin-top:8px;background:rgba(255,255,255,0.1);color:var(--yt-text-secondary);}\n .ytp-plus-voting-item-status.completed{background:rgba(76,175,80,0.2);color:#4caf50;}\n .ytp-plus-voting-item-status.in-progress{background:rgba(255,193,7,0.2);color:#ffc107;}\n .ytp-plus-voting-item-votes{display:flex;flex-direction:column;align-items:stretch;gap:8px;min-width:120px;}\n .ytp-plus-voting-score{display:flex;align-items:baseline;gap:8px;justify-content:center;}\n .ytp-plus-vote-total{font-size:12px;color:var(--yt-text-secondary);}\n .ytp-plus-voting-buttons{position:relative;display:flex;justify-content:center;gap:0;border:1px solid var(--yt-glass-border);border-radius:20px;overflow:hidden;}\n .ytp-plus-voting-buttons-track{position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;transition:background .4s ease;border-radius:20px;pointer-events:none;}\n .ytp-plus-vote-btn{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:42px;height:32px;border:none;background:transparent;cursor:pointer;transition:color .15s ease,opacity .15s ease;color:var(--yt-text-secondary);opacity:.95}\n .ytp-plus-vote-btn:first-of-type{border-right:1px solid var(--yt-glass-border)}\n .ytp-plus-vote-btn:hover{color:var(--yt-text-primary);opacity:1}\n .ytp-plus-vote-btn.active{color:#fff;opacity:1}\n .ytp-plus-vote-icon{width:20px;height:20px;fill:currentColor;opacity:.92}\n .ytp-plus-vote-btn.active .ytp-plus-vote-icon,.ytp-plus-vote-btn:hover .ytp-plus-vote-icon{opacity:1}\n .ytp-plus-voting-loading,.ytp-plus-voting-empty{text-align:center;padding:24px;color:var(--yt-text-secondary);font-size:13px;}\n .ytp-plus-voting-add-btn{background:var(--yt-accent);color:#fff;border:none;padding:8px 16px;border-radius:18px;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;}\n .ytp-plus-voting-add-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,0,0,.3);}\n .ytp-plus-voting-add-form{margin-top:16px;padding:16px;background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);}\n .ytp-plus-voting-add-form input,.ytp-plus-voting-add-form textarea{width:100%;padding:10px 12px;margin-bottom:12px;background:var(--yt-header-bg);border:1px solid var(--yt-glass-border);border-radius:8px;color:var(--yt-text-primary);font-size:13px;box-sizing:border-box;}\n .ytp-plus-voting-add-form input:focus,.ytp-plus-voting-add-form textarea:focus{border-color:var(--yt-accent);outline:none;}\n .ytp-plus-voting-add-form textarea{min-height:80px;resize:vertical;}\n .ytp-plus-voting-form-actions{display:flex;gap:8px;justify-content:flex-end;}\n .ytp-plus-voting-cancel{background:transparent;border:1px solid var(--yt-glass-border);color:var(--yt-text-primary);padding:8px 16px;border-radius:18px;font-size:13px;cursor:pointer;transition:all .2s ease;}\n .ytp-plus-voting-cancel:hover{background:var(--yt-hover-bg);}\n .ytp-plus-voting-submit{background:var(--yt-accent);color:#fff;border:none;padding:8px 16px;border-radius:18px;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;}\n .ytp-plus-voting-submit:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,0,0,.3);}\n @media (max-width: 680px){.ytp-plus-voting-item{flex-direction:column;align-items:stretch}.ytp-plus-voting-item-content{padding-right:0}.ytp-plus-voting-item-votes{min-width:0;width:100%}}\n .ytp-plus-voting-preview{margin-bottom:20px;}\n .ytp-plus-ba-container{position:relative;width:100%;height:260px;overflow:hidden;border-radius:var(--yt-radius-md);border:1px solid var(--yt-glass-border);user-select:none;cursor:ew-resize;background:#000;}\n .ytp-plus-ba-before,.ytp-plus-ba-after{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;}\n .ytp-plus-ba-before img,.ytp-plus-ba-after img{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;display:block;pointer-events:none;}\n .ytp-plus-ba-after{clip-path:inset(0 0 0 50%);}\n .ytp-plus-ba-divider{position:absolute;top:0;left:50%;transform:translateX(-50%);width:8px;height:100%;background:transparent;pointer-events:auto;z-index:3;cursor:ew-resize;transition:left .6s linear}\n .ytp-plus-ba-divider::after{content:\'\';position:absolute;left:50%;top:0;transform:translateX(-50%);width:2px;height:100%;background:var(--yt-accent,#f00);} \n .ytp-plus-ba-divider.autoplay{animation:ytpPlusSlideDivider 6s linear infinite}\n @keyframes ytpPlusSlideDivider{0%{left:10%}50%{left:90%}100%{left:10%}}\n .ytp-plus-ba-label{position:absolute;top:10px;padding:4px 10px;border-radius:4px;font-size:12px;font-weight:600;color:#fff;background:rgba(0,0,0,.55);pointer-events:none;z-index:5;}\n .ytp-plus-ba-label-before{left:10px;}\n .ytp-plus-ba-label-after{right:10px;}\n .ytp-plus-vote-bar-section{margin-top:12px;display:flex;flex-direction:column;align-items:center;gap:6px;}\n .ytp-plus-vote-bar-buttons{position:relative;display:flex;gap:0;border-radius:20px;overflow:hidden;border:1px solid var(--yt-glass-border);}\n .ytp-plus-vote-bar-track{position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;transition:background .4s ease;background:linear-gradient(to right, #4caf50 50%, #f44336 50%);border-radius:20px;}\n .ytp-plus-vote-bar-btn{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;padding:8px 18px;background:transparent;border:none;color:var(--yt-text-secondary);cursor:pointer;transition:color .15s;font-size:14px;}\n .ytp-plus-vote-bar-btn:first-of-type{border-right:1px solid var(--yt-glass-border);}\n .ytp-plus-vote-bar-btn:hover{color:var(--yt-text-primary);}\n .ytp-plus-vote-bar-btn.active{color:#fff;}\n .ytp-plus-vote-bar-btn svg{fill:currentColor;}\n .ytp-plus-vote-bar-count{font-size:12px;color:var(--yt-text-secondary);}'; (document.head || document.documentElement).appendChild(ncEl); } }; this.ensureNonCriticalStyles = injectNonCritical; document.getElementById("yt-enhancer-main") || YouTubeUtils.StyleManager.add("yt-enhancer-main", ":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%);}\n 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;}\n 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;}\n .ytp-screenshot-button,.ytp-cobalt-button,.ytp-pip-button{position:relative;width:44px;height:100%;display:inline-flex;align-items:center;justify-content:center;vertical-align:top;transition:opacity .15s,transform .15s;}\n .ytp-screenshot-button:hover,.ytp-cobalt-button:hover,.ytp-pip-button:hover{transform:scale(1.1);}\n .speed-control-btn{width:4em!important;position:relative!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;height:100%!important;vertical-align:top!important;text-align:center!important;border-radius:var(--yt-radius-sm);font-size:13px;color:var(--yt-text-primary);cursor:pointer;user-select:none;font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:color .2s;}\n .speed-control-btn:hover{color:var(--yt-accent);font-weight:bold;}\n .speed-options{position:fixed!important;background:var(--yt-glass-bg)!important;color:var(--yt-text-primary)!important;border-radius:var(--yt-radius-md)!important;display:flex!important;flex-direction:column!important;align-items:stretch!important;gap:0!important;transform:translate(-50%,12px)!important;width:92px!important;z-index:2147483647!important;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);overflow:hidden;opacity:0;pointer-events:none!important;transition:opacity .18s ease,transform .18s ease;box-sizing:border-box;}\n .speed-options.visible{opacity:1;pointer-events:auto!important;transform:translate(-50%,0)!important;backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);}\n .speed-option-item{cursor:pointer!important;height:28px!important;line-height:28px!important;font-size:12px!important;text-align:center!important;transition:background-color .15s,color .15s;}\n .speed-option-active,.speed-option-item:hover{color:var(--yt-accent)!important;font-weight:bold!important;background:var(--yt-hover-bg)!important;}\n #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);border:1px solid var(--yt-glass-border);}\n .youtube-enhancer-notification-container{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:10px;z-index:2147483647;pointer-events:none;max-width:calc(100% - 32px);width:100%;box-sizing:border-box;padding:0 16px;}\n .youtube-enhancer-notification{position:relative;max-width:700px;width:auto;background:var(--yt-glass-bg);color:var(--yt-text-primary);padding:8px 14px;font-size:13px;border-radius:var(--yt-radius-md);z-index:inherit;transition:opacity .35s,transform .32s;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);font-weight:500;box-sizing:border-box;display:flex;align-items:center;gap:10px;pointer-events:auto;}\n .ytp-plus-loop-indicator{position:absolute;height:100%;background:linear-gradient(90deg,rgba(25,118,210,0.28) 0%,rgba(66,165,245,0.4) 50%,rgba(25,118,210,0.28) 100%);border-left:2px solid #1976d2;border-right:2px solid #1976d2;display:none;pointer-events:none;top:0;z-index:1000;box-shadow:inset 0 0 4px rgba(25,118,210,0.25);}\n .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:none;display:flex;align-items:center;justify-content:center;transition:background-color .2s,transform .2s;}\n .ytp-plus-settings-button svg{width:24px;height:24px;}\n .ytp-plus-settings-button:hover{transform:rotate(30deg);color:var(--yt-text-secondary);}\n .ytp-download-button{position:relative!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;height:100%!important;vertical-align:top!important;cursor:pointer!important;}\n @keyframes ytEnhanceFadeIn{from{opacity:0;}to{opacity:1;}}\n @keyframes ytEnhanceScaleIn{from{opacity:0;transform:scale(.92) translateY(10px);}to{opacity:1;transform:scale(1) translateY(0);}}\n .ytSearchboxComponentInputBox { background: transparent !important; }"); "function" == typeof requestIdleCallback ? requestIdleCallback(injectNonCritical, { timeout: 5e3 }) : setTimeout(injectNonCritical, 1e3); }, addSettingsButtonToHeader() { this.waitForElement("ytd-masthead #end", 5e3).then(headerEnd => { if (!this.getElement(".ytp-plus-settings-button")) { const settingsButton = document.createElement("div"); settingsButton.className = "ytp-plus-settings-button"; settingsButton.setAttribute("title", t("youtubeSettings")); settingsButton.innerHTML = _createHTML('\n <svg width="24" height="24" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">\n <path d="M39.23,26a16.52,16.52,0,0,0,.14-2,16.52,16.52,0,0,0-.14-2l4.33-3.39a1,1,0,0,0,.25-1.31l-4.1-7.11a1,1,0,0,0-1.25-.44l-5.11,2.06a15.68,15.68,0,0,0-3.46-2l-.77-5.43a1,1,0,0,0-1-.86H19.9a1,1,0,0,0-1,.86l-.77,5.43a15.36,15.36,0,0,0-3.46,2L9.54,9.75a1,1,0,0,0-1.25.44L4.19,17.3a1,1,0,0,0,.25,1.31L8.76,22a16.66,16.66,0,0,0-.14,2,16.52,16.52,0,0,0,.14,2L4.44,29.39a1,1,0,0,0-.25,1.31l4.1,7.11a1,1,0,0,0,1.25.44l5.11-2.06a15.68,15.68,0,0,0,3.46,2l.77,5.43a1,1,0,0,0,1,.86h8.2a1,1,0,0,0,1-.86l.77-5.43a15.36,15.36,0,0,0,3.46-2l5.11,2.06a1,1,0,0,0,1.25-.44l4.1-7.11a1,1,0,0,0-.25-1.31ZM24,31.18A7.18,7.18,0,1,1,31.17,24,7.17,7.17,0,0,1,24,31.18Z"/>\n </svg>\n '); settingsButton.addEventListener("click", this.openSettingsModal.bind(this)); const avatarButton = headerEnd.querySelector("ytd-topbar-menu-button-renderer"); avatarButton ? headerEnd.insertBefore(settingsButton, avatarButton) : headerEnd.appendChild(settingsButton); } }).catch(() => {}); }, handleModalClickActions(target, modal, handlers, markDirty, context, translate) { const navItem = target.classList && target.classList.contains("ytp-plus-settings-nav-item") ? target : target.closest && target.closest(".ytp-plus-settings-nav-item"); if (navItem) { handlers.handleSidebarNavigation(navItem, modal); } else if ("ytp-plus-save-settings" !== target.id && "ytp-plus-save-settings-icon" !== target.id) { "download-externalDownloader-save" !== target.id ? "download-externalDownloader-reset" === target.id && handlers.handleExternalDownloaderReset(modal, this.settings, this.saveSettings.bind(this), this.showNotification.bind(this), translate) : handlers.handleExternalDownloaderSave(target, this.settings, this.saveSettings.bind(this), this.showNotification.bind(this), translate); } else { this.saveSettings(); modal.remove(); this.showNotification(translate("settingsSaved")); } }, createSettingsModal() { const modal = document.createElement("div"); modal.className = "ytp-plus-settings-modal"; const helpers = window.YouTubePlusSettingsHelpers; const handlers = window.YouTubePlusModalHandlers; modal.innerHTML = _createHTML(`<div class="ytp-plus-settings-panel">${helpers.createSettingsSidebar(t)}${helpers.createMainContent(this.settings, t)}</div>`); let dirty = !1; const saveIconBtn = modal.querySelector("#ytp-plus-save-settings-icon"); saveIconBtn && (saveIconBtn.style.display = "none"); const markDirty = () => { if (!dirty) { dirty = !0; saveIconBtn && (saveIconBtn.style.display = ""); } }; const context = { settings: this.settings, getElement: this.getElement.bind(this), addDownloadButton: this.addDownloadButton.bind(this), addSpeedControlButton: this.addSpeedControlButton.bind(this), refreshDownloadButton: this.refreshDownloadButton.bind(this), updatePageBasedOnSettings: this.updatePageBasedOnSettings.bind(this) }; modal.addEventListener("click", e => { const {target} = e; const submenuToggleBtn = target.closest(".ytp-plus-submenu-toggle"); if (submenuToggleBtn) { try { if (submenuToggleBtn instanceof HTMLElement && "BUTTON" === submenuToggleBtn.tagName && submenuToggleBtn.hasAttribute("disabled")) { return; } const submenuKey = submenuToggleBtn.dataset?.submenu; if (!submenuKey) { return; } const panel = submenuToggleBtn.closest(".ytp-plus-settings-panel"); if (!panel) { return; } const submenuSelector = "music" === submenuKey ? `.music-submenu[data-submenu="${submenuKey}"]` : "download" === submenuKey ? `.download-submenu[data-submenu="${submenuKey}"]` : "style" === submenuKey ? `.style-submenu[data-submenu="${submenuKey}"]` : "speed" === submenuKey ? `.speed-submenu[data-submenu="${submenuKey}"]` : "loop" === submenuKey ? `.loop-submenu[data-submenu="${submenuKey}"]` : "pip" === submenuKey ? `.pip-submenu[data-submenu="${submenuKey}"]` : "timecode" === submenuKey ? `.timecode-submenu[data-submenu="${submenuKey}"]` : "enhanced" === submenuKey ? `.enhanced-submenu[data-submenu="${submenuKey}"]` : `[data-submenu="${submenuKey}"]`; const submenuEl = panel.querySelector(submenuSelector); if (!(submenuEl instanceof HTMLElement)) { return; } const computedDisplay = window.getComputedStyle(submenuEl).display; const currentlyHidden = "none" === computedDisplay || submenuEl.hidden; const nextHidden = !currentlyHidden; submenuEl.style.display = nextHidden ? "none" : ""; submenuToggleBtn.setAttribute("aria-expanded", nextHidden ? "false" : "true"); try { const submenuStates = JSON.parse(localStorage.getItem("ytp-plus-submenu-states") || "{}"); submenuStates[submenuKey] = !nextHidden; localStorage.setItem("ytp-plus-submenu-states", JSON.stringify(submenuStates)); } catch {} } catch (e) { console.warn("[YouTube+] Submenu toggle error:", e); } } else { target !== modal ? "ytp-plus-close-settings" === target.id || "ytp-plus-close-settings-icon" === target.id || target.classList.contains("ytp-plus-settings-close") || target.closest(".ytp-plus-settings-close") || target.closest("#ytp-plus-close-settings") || target.closest("#ytp-plus-close-settings-icon") ? modal.remove() : "open-ytdl-github" === target.id || target.closest("#open-ytdl-github") ? window.open("https://github.com/diorhc/YTDL", "_blank") : this.handleModalClickActions(target, modal, handlers, markDirty, context, t) : modal.remove(); } }); modal.addEventListener("change", e => { const {target} = e; if (!target.classList.contains("ytp-plus-settings-checkbox")) { return; } const {dataset} = target; const {setting} = dataset; if (setting) { if (setting.startsWith("downloadSite_")) { const key = setting.replace("downloadSite_", ""); handlers.handleDownloadSiteToggle(target, key, this.settings, markDirty, this.saveSettings.bind(this)); return; } handlers.isMusicSetting && handlers.isMusicSetting(setting) ? handlers.handleMusicSettingToggle(target, setting, this.showNotification.bind(this), t) : handlers.handleSimpleSettingToggle(target, setting, this.settings, context, markDirty, this.saveSettings.bind(this), modal); } }); modal.addEventListener("input", e => { const {target} = e; if (target.classList.contains("speed-hotkey-input")) { const keyType = target.dataset?.speedHotkey; if ("decrease" !== keyType && "increase" !== keyType && "reset" !== keyType) { return; } markDirty(); return; } if (target.classList.contains("loop-hotkey-input")) { const keyType = target.dataset?.loopHotkey; if ("setPointA" !== keyType && "setPointB" !== keyType && "resetPoints" !== keyType) { return; } markDirty(); return; } if (target.classList.contains("download-site-input")) { const {dataset} = target; const {site, field} = dataset; if (!site || !field) { return; } handlers.handleDownloadSiteInput(target, site, field, this.settings, markDirty, t); } }); modal.addEventListener("blur", e => { const {target} = e; if (target.classList.contains("speed-hotkey-input")) { const keyType = target.dataset?.speedHotkey; if ("decrease" !== keyType && "increase" !== keyType && "reset" !== keyType) { return; } const input = target; const fallback = "decrease" === keyType ? "g" : "increase" === keyType ? "h" : "b"; const normalized = this.normalizeSpeedHotkey(input.value, fallback); this.settings.speedControlHotkeys = this.settings.speedControlHotkeys || { decrease: "g", increase: "h", reset: "b" }; this.settings.speedControlHotkeys[keyType] = normalized; input.value = normalized; this.saveSettings(); return; } if (target.classList.contains("loop-hotkey-input")) { const keyType = target.dataset?.loopHotkey; if ("setPointA" !== keyType && "setPointB" !== keyType && "resetPoints" !== keyType) { return; } const input = target; const fallback = "setPointA" === keyType ? "k" : "setPointB" === keyType ? "l" : "o"; const normalized = this.normalizeSpeedHotkey(input.value, fallback); this.settings.loopHotkeys = this.settings.loopHotkeys || { toggleLoop: "r", setPointA: "k", setPointB: "l", resetPoints: "o" }; this.settings.loopHotkeys[keyType] = normalized; input.value = normalized; this.saveSettings(); return; } }, !0); try { if ("undefined" != typeof window && window.youtubePlusReport && "function" == typeof window.youtubePlusReport.render) { try { window.youtubePlusReport.render(modal); } catch (e) { YouTubeUtils.logError("Report", "report.render failed", e); } } } catch (e) { YouTubeUtils.logError("Report", "Failed to initialize report section", e); } let submenuStates = {}; try { submenuStates = JSON.parse(localStorage.getItem("ytp-plus-submenu-states") || "{}"); Object.entries(submenuStates).forEach(([key, expanded]) => { const toggleBtn = modal.querySelector(`.ytp-plus-submenu-toggle[data-submenu="${key}"]`); if (toggleBtn instanceof HTMLElement && !toggleBtn.hasAttribute("disabled")) { const submenuSelector = "music" === key ? `.music-submenu[data-submenu="${key}"]` : "download" === key ? `.download-submenu[data-submenu="${key}"]` : "style" === key ? `.style-submenu[data-submenu="${key}"]` : "speed" === key ? `.speed-submenu[data-submenu="${key}"]` : "pip" === key ? `.pip-submenu[data-submenu="${key}"]` : "timecode" === key ? `.timecode-submenu[data-submenu="${key}"]` : "enhanced" === key ? `.enhanced-submenu[data-submenu="${key}"]` : `[data-submenu="${key}"]`; const submenuEl = modal.querySelector(submenuSelector); if (submenuEl instanceof HTMLElement) { const isExpanded = !!expanded; submenuEl.style.display = isExpanded ? "" : "none"; toggleBtn.setAttribute("aria-expanded", isExpanded ? "true" : "false"); } } }); } catch {} try { const advancedSection = modal.querySelector('.ytp-plus-settings-section[data-section="advanced"]'); if (advancedSection instanceof HTMLElement) { const ensureVisibleWhenEnabled = (key, setting, submenuSelector) => { if (Object.prototype.hasOwnProperty.call(submenuStates, key)) { return; } const checkbox = advancedSection.querySelector(`.ytp-plus-settings-checkbox[data-setting="${setting}"]`); const submenu = advancedSection.querySelector(submenuSelector); const toggleBtn = advancedSection.querySelector(`.ytp-plus-submenu-toggle[data-submenu="${key}"]`); if (checkbox instanceof Element && checkbox.classList.contains("ytp-plus-settings-checkbox") && checkbox.checked && submenu instanceof HTMLElement) { submenu.style.display = ""; toggleBtn instanceof HTMLElement && toggleBtn.setAttribute("aria-expanded", "true"); } }; ensureVisibleWhenEnabled("enhanced", "enableEnhanced", '.enhanced-submenu[data-submenu="enhanced"]'); ensureVisibleWhenEnabled("music", "enableMusic", '.music-submenu[data-submenu="music"]'); } } catch {} try { const savedSection = localStorage.getItem("ytp-plus-active-nav-section"); if (savedSection) { const navItem = modal.querySelector(`.ytp-plus-settings-nav-item[data-section="${savedSection}"]`); if (navItem) { modal.querySelectorAll(".ytp-plus-settings-nav-item").forEach(item => item.classList.remove("active")); modal.querySelectorAll(".ytp-plus-settings-section").forEach(s => s.classList.add("hidden")); navItem.classList.add("active"); const targetSection = modal.querySelector(`.ytp-plus-settings-section[data-section="${savedSection}"]`); targetSection && targetSection.classList.remove("hidden"); } } } catch {} return modal; }, openSettingsModal() { const existingModal = this.getElement(".ytp-plus-settings-modal", !1); existingModal && existingModal.remove(); "function" == typeof this.ensureNonCriticalStyles && this.ensureNonCriticalStyles(); document.body.appendChild(this.createSettingsModal()); if (window.YouTubePlus?.Voting) { const votingContainer = document.getElementById("ytp-plus-voting-container"); if (votingContainer) { window.YouTubePlus.Voting.init(); window.YouTubePlus.Voting.createUI(votingContainer); window.YouTubePlus.Voting.loadFeatures(); } const votingSection = document.querySelector('.ytp-plus-settings-section[data-section="voting"]'); votingSection && !votingSection.classList.contains("hidden") && requestAnimationFrame(() => window.YouTubePlus.Voting?.initSlider?.()); } try { document.dispatchEvent(new CustomEvent("youtube-plus-settings-modal-opened", { bubbles: !0 })); } catch {} }, waitForElement: (selector, timeout = 5e3) => YouTubeUtils.waitForElement(selector, timeout), addCustomButtons() { const controls = this.getElement(".ytp-right-controls"); if (controls) { this.getElement(".ytp-screenshot-button") || this.addScreenshotButton(controls); this.getElement(".ytp-download-button") || this.addDownloadButton(controls); 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"); player && player.appendChild(indicator); } this.handleFullscreenChange(); } }, addScreenshotButton(controls) { const button = document.createElement("button"); button.className = "ytp-button ytp-screenshot-button"; button.setAttribute("title", t("takeScreenshot")); button.setAttribute("aria-label", t("takeScreenshot")); button.innerHTML = _createHTML('\n <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" style="display:block;margin:auto;vertical-align:middle;">\n <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>\n <circle cx="12" cy="13" r="4"></circle>\n </svg>\n '); button.addEventListener("click", this.captureFrame.bind(this)); controls.insertBefore(button, controls.firstChild); }, addDownloadButton(controls) { if ("undefined" != typeof window && window.YouTubePlusDownloadButton) { const manager = window.YouTubePlusDownloadButton.createDownloadButtonManager({ settings: this.settings, t, getElement: this.getElement.bind(this), YouTubeUtils }); manager.addDownloadButton(controls); } else { console.warn("[YouTube+] Download button module not loaded"); } }, addSpeedControlButton(controls) { if (!this.settings.enableSpeedControl) { return; } const speedBtn = document.createElement("button"); speedBtn.type = "button"; speedBtn.className = "ytp-button speed-control-btn"; speedBtn.setAttribute("aria-label", t("speedControl")); speedBtn.setAttribute("aria-haspopup", "true"); speedBtn.setAttribute("aria-expanded", "false"); speedBtn.innerHTML = _createHTML(`<span>${this.speedControl.currentSpeed}×</span>`); const speedOptions = document.createElement("div"); speedOptions.className = "speed-options"; speedOptions.setAttribute("role", "menu"); const selectSpeed = speed => { this.changeSpeed(speed); hideDropdown(); }; this.speedControl.availableSpeeds.forEach(speed => { const option = document.createElement("div"); option.className = "speed-option-item" + (Number(speed) === this.speedControl.currentSpeed ? " speed-option-active" : ""); option.textContent = `${speed}x`; option.dataset.speed = String(speed); option.setAttribute("role", "menuitem"); option.tabIndex = 0; option.addEventListener("click", () => selectSpeed(speed)); option.addEventListener("keydown", event => { if ("Enter" === event.key || " " === event.key) { event.preventDefault(); selectSpeed(speed); } }); speedOptions.appendChild(option); }); speedBtn.appendChild(speedOptions); const existingSpeed = document.querySelector(".speed-options"); existingSpeed && existingSpeed.remove(); try { document.body.appendChild(speedOptions); } catch {} const positionDropdown = () => { const rect = speedBtn.getBoundingClientRect(); speedOptions.style.left = `${rect.left + rect.width / 2}px`; speedOptions.style.bottom = window.innerHeight - rect.top + 8 + "px"; }; const hideDropdown = () => { speedOptions.classList.remove("visible"); speedBtn.setAttribute("aria-expanded", "false"); }; const showDropdown = () => { positionDropdown(); speedOptions.classList.add("visible"); speedBtn.setAttribute("aria-expanded", "true"); }; let documentClickKey; documentClickKey = YouTubeUtils.cleanupManager.registerListener(document, "click", event => { if (speedBtn.isConnected) { speedOptions.classList.contains("visible") && (speedBtn.contains(event.target) || speedOptions.contains(event.target) || hideDropdown()); } else if (documentClickKey) { YouTubeUtils.cleanupManager.unregisterListener(documentClickKey); documentClickKey = void 0; } }, !0); YouTubeUtils.cleanupManager.registerListener(document, "keydown", event => { if ("Escape" === event.key && speedOptions.classList.contains("visible")) { hideDropdown(); speedBtn.focus(); } }, !0); YouTubeUtils.cleanupManager.registerListener(window, "resize", () => { speedOptions.classList.contains("visible") && positionDropdown(); }); YouTubeUtils.cleanupManager.registerListener(window, "scroll", () => { speedOptions.classList.contains("visible") && positionDropdown(); }, !0); let speedHideTimer; speedBtn.addEventListener("mouseenter", () => { clearTimeout(speedHideTimer); showDropdown(); }); speedBtn.addEventListener("mouseleave", () => { clearTimeout(speedHideTimer); speedHideTimer = setTimeout(hideDropdown, 200); }); speedOptions.addEventListener("mouseenter", () => { clearTimeout(speedHideTimer); showDropdown(); }); speedOptions.addEventListener("mouseleave", () => { clearTimeout(speedHideTimer); speedHideTimer = setTimeout(hideDropdown, 200); }); speedBtn.addEventListener("keydown", event => { if ("Enter" === event.key || " " === event.key) { event.preventDefault(); speedOptions.classList.contains("visible") ? hideDropdown() : showDropdown(); } else { "Escape" === event.key && hideDropdown(); } }); controls.insertBefore(speedBtn, controls.firstChild); }, applyGuideVisibility() { try { const enabled = Boolean(YouTubeUtils.storage.get("ytplus.hideGuide", !1)); document.documentElement.classList.toggle("ytp-hide-guide", enabled); const btn = document.getElementById("ytplus-guide-toggle-btn"); if (btn) { btn.setAttribute("aria-pressed", String(enabled)); const label = enabled ? "Show side guide" : "Hide side guide"; btn.title = label; btn.setAttribute("aria-label", label); } } catch (e) { console.warn("[YouTube+] applyGuideVisibility failed:", e); } }, toggleSideGuide() { try { const current = Boolean(YouTubeUtils.storage.get("ytplus.hideGuide", !1)); const next = !current; YouTubeUtils.storage.set("ytplus.hideGuide", next); this.applyGuideVisibility(); } catch (e) { console.warn("[YouTube+] toggleSideGuide failed:", e); } }, createGuideToggleButton() { try { if (document.getElementById("ytplus-guide-toggle-btn")) { return; } const btn = document.createElement("button"); btn.id = "ytplus-guide-toggle-btn"; btn.type = "button"; btn.style.cssText = "position:fixed;right:12px;bottom:12px;z-index:100000;background:var(--yt-spec-call-to-action);color:#fff;border:none;border-radius:8px;padding:8px 10px;box-shadow:0 6px 18px rgba(0,0,0,0.3);cursor:pointer;opacity:0.95;font-size:13px;"; btn.setAttribute("aria-pressed", "false"); btn.setAttribute("aria-label", "Hide side guide"); btn.title = "Hide side guide"; btn.textContent = "Toggle Guide"; btn.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); this.toggleSideGuide(); }); btn.addEventListener("keydown", e => { if ("Enter" === e.key || " " === e.key) { e.preventDefault(); this.toggleSideGuide(); } }); document.body.appendChild(btn); this.applyGuideVisibility(); } catch (e) { console.warn("[YouTube+] createGuideToggleButton failed:", e); } }, captureFrame() { const video = this.getElement("video", !1); 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`; try { link.click(); try { const translated = "function" == typeof t ? t("screenshotSaved") : null; const message = translated && "screenshotSaved" !== translated ? translated : "Screenshot saved"; this.showNotification(message, 2e3); } catch { this.showNotification("Screenshot saved", 2e3); } } catch (err) { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Basic", "Screenshot download failed", err); try { const translatedFail = "function" == typeof t ? t("screenshotFailed") : null; const failMsg = translatedFail && "screenshotFailed" !== translatedFail ? translatedFail : "Screenshot failed"; this.showNotification(failMsg, 3e3); } catch { this.showNotification("Screenshot failed", 3e3); } } }, showNotification(message, duration = 2e3) { YouTubeUtils.NotificationManager.show(message, { duration, type: "info" }); }, handleFullscreenChange() { document.fullscreenElement || document; document.querySelectorAll(".ytp-screenshot-button, .ytp-cobalt-button").forEach(button => { button.style.bottom = "0px"; }); }, changeSpeed(speed) { const numericSpeed = Number(speed); this.speedControl.currentSpeed = numericSpeed; localStorage.setItem(this.speedControl.storageKey, String(numericSpeed)); const speedBtn = this.getElement(".speed-control-btn span", !1); speedBtn && (speedBtn.textContent = `${numericSpeed}×`); document.querySelectorAll(".speed-option-item").forEach(option => { option.classList.toggle("speed-option-active", parseFloat(option.dataset.speed) === numericSpeed); }); this.applyCurrentSpeed(); this.showSpeedIndicator(numericSpeed); }, applyCurrentSpeed() { const videos = window.YouTubeDOMCache && "function" == typeof window.YouTubeDOMCache.getAll ? window.YouTubeDOMCache.getAll("video") : document.querySelectorAll("video"); videos.forEach(video => { video && video.playbackRate !== this.speedControl.currentSpeed && (video.playbackRate = this.speedControl.currentSpeed); }); }, setupVideoObserver() { this._speedInterval && clearInterval(this._speedInterval); this._speedInterval = null; if (!this._mouseHoldTracked) { this._mouseHoldTracked = !0; this._mouseButtonHeld = !1; YouTubeUtils.cleanupManager.registerListener(document, "mousedown", e => { 0 === e.button && (this._mouseButtonHeld = !0); }, { passive: !0, capture: !0 }); YouTubeUtils.cleanupManager.registerListener(document, "mouseup", e => { 0 === e.button && (this._mouseButtonHeld = !1); }, { passive: !0, capture: !0 }); } const applySpeed = () => this.applyCurrentSpeed(); const updateLoopBar = () => this.updateLoopProgressBar(); const applyLoop = () => this.applyLoopStateToCurrentVideo(); const attachSpeedListeners = video => { if (video._ytpSpeedListenerAttached) { return; } video._ytpSpeedListenerAttached = !0; video.addEventListener("loadedmetadata", applySpeed); video.addEventListener("loadedmetadata", updateLoopBar); video.addEventListener("loadedmetadata", applyLoop); video.addEventListener("playing", applySpeed); let _settingRate = !1; video.addEventListener("ratechange", () => { if (!(_settingRate || this._mouseButtonHeld && video.playbackRate > this.speedControl.currentSpeed || video.playbackRate === this.speedControl.currentSpeed)) { _settingRate = !0; video.playbackRate = this.speedControl.currentSpeed; _settingRate = !1; } }); applySpeed(); }; const mainPlayer = document.querySelector("#movie_player") || document.querySelector("ytd-player"); mainPlayer ? mainPlayer.querySelectorAll("video").forEach(attachSpeedListeners) : document.querySelectorAll("video").forEach(attachSpeedListeners); const videoObserver = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { "VIDEO" === node.nodeName && attachSpeedListeners(node); node instanceof Element && node.querySelectorAll?.("video").forEach(attachSpeedListeners); } } }); const playerRoot = document.querySelector("#movie_player") || document.querySelector("ytd-player") || document.body; playerRoot && videoObserver.observe(playerRoot, { childList: !0, subtree: !0 }); YouTubeUtils.cleanupManager.registerObserver(videoObserver); }, setupNavigationObserver() { let lastUrl = location.href; YouTubeUtils.cleanupManager.registerListener(document, "fullscreenchange", this.handleFullscreenChange.bind(this)); YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", () => { location.href.includes("watch?v=") && this.setupCurrentPage(); this.addSettingsButtonToHeader(); }); const checkUrlChange = () => { if (lastUrl !== location.href) { lastUrl = location.href; location.href.includes("watch?v=") && setTimeout(() => this.setupCurrentPage(), 500); this.addSettingsButtonToHeader(); } }; YouTubeUtils.cleanupManager.registerListener(window, "popstate", checkUrlChange); YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-start", checkUrlChange); }, 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(.8 * (1 - progress)); if (progress < 1) { this.speedControl.activeAnimationId = YouTubeUtils.cleanupManager.registerAnimationFrame(requestAnimationFrame(fadeOut)); } else { indicator.style.display = "none"; this.speedControl.activeAnimationId = null; } }; this.speedControl.activeAnimationId = YouTubeUtils.cleanupManager.registerAnimationFrame(requestAnimationFrame(fadeOut)); } }; const initFunction = YouTubeEnhancer.init.bind(YouTubeEnhancer); "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", initFunction) : initFunction(); })(); !(function() { "use strict"; const CircuitState_CLOSED = "closed", CircuitState_OPEN = "open", CircuitState_HALF_OPEN = "half_open"; const ErrorBoundaryConfig = { maxErrors: 10, errorWindow: 6e4, enableLogging: !0, enableRecovery: !0, storageKey: "youtube_plus_errors", circuitBreaker: { enabled: !0, failureThreshold: 5, resetTimeout: 3e4, halfOpenAttempts: 3 } }; const errorState = { errors: [], errorCount: 0, lastErrorTime: 0, isRecovering: !1, circuitState: CircuitState_CLOSED, circuitFailureCount: 0, circuitLastFailureTime: 0, circuitSuccessCount: 0 }; const ErrorSeverity_LOW = "low", ErrorSeverity_MEDIUM = "medium", ErrorSeverity_HIGH = "high", ErrorSeverity_CRITICAL = "critical"; const categorizeSeverity = error => { const message = error.message?.toLowerCase() || ""; return message.includes("cannot read") || message.includes("undefined") || message.includes("null") ? ErrorSeverity_MEDIUM : message.includes("network") || message.includes("fetch") || message.includes("timeout") ? ErrorSeverity_LOW : message.includes("syntax") || message.includes("reference") || message.includes("type") ? ErrorSeverity_HIGH : message.includes("security") || message.includes("csp") ? ErrorSeverity_CRITICAL : ErrorSeverity_MEDIUM; }; const logError = (error, context = {}) => { if (!ErrorBoundaryConfig.enableLogging) { return; } (success => { if (!ErrorBoundaryConfig.circuitBreaker.enabled) { return !0; } const now = Date.now(); const {circuitBreaker} = ErrorBoundaryConfig; if (errorState.circuitState === CircuitState_OPEN && now - errorState.circuitLastFailureTime >= circuitBreaker.resetTimeout) { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+] Circuit breaker transitioning to HALF_OPEN"); errorState.circuitState = CircuitState_HALF_OPEN; errorState.circuitSuccessCount = 0; } if (success) { if (errorState.circuitState === CircuitState_HALF_OPEN) { errorState.circuitSuccessCount++; if (errorState.circuitSuccessCount >= circuitBreaker.halfOpenAttempts) { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+] Circuit breaker CLOSED - system recovered"); errorState.circuitState = CircuitState_CLOSED; errorState.circuitFailureCount = 0; errorState.circuitSuccessCount = 0; } } else { errorState.circuitState === CircuitState_CLOSED && (errorState.circuitFailureCount = Math.max(0, errorState.circuitFailureCount - 1)); } return !0; } errorState.circuitFailureCount++; errorState.circuitLastFailureTime = now; if (errorState.circuitState === CircuitState_CLOSED) { if (errorState.circuitFailureCount >= circuitBreaker.failureThreshold) { console.error("[YouTube+] Circuit breaker OPEN - too many failures"); errorState.circuitState = CircuitState_OPEN; return !1; } } else if (errorState.circuitState === CircuitState_HALF_OPEN) { console.error("[YouTube+] Circuit breaker reopened - recovery failed"); errorState.circuitState = CircuitState_OPEN; errorState.circuitSuccessCount = 0; return !1; } })(!1); const fallbackMessage = error.message?.trim() || ""; if (!(fallbackMessage && "(no message)" !== fallbackMessage || error.stack || context.filename)) { return; } const displayMessage = fallbackMessage || (context.filename ? `Error in ${context.filename}:${context.lineno}` : "Unknown error"); const errorInfo = { timestamp: (new Date).toISOString(), message: displayMessage, stack: error.stack, severity: categorizeSeverity(error), context: { url: window.location.href, userAgent: navigator.userAgent, ...context } }; console.error("[YouTube+][Error Boundary]", `${errorInfo.message}`, errorInfo); errorState.errors.push(errorInfo); errorState.errors.length > 50 && errorState.errors.shift(); try { const stored = JSON.parse(localStorage.getItem(ErrorBoundaryConfig.storageKey) || "[]"); stored.push(errorInfo); stored.length > 20 && stored.shift(); localStorage.setItem(ErrorBoundaryConfig.storageKey, JSON.stringify(stored)); } catch {} }; const isErrorRateExceeded = () => { const now = Date.now(); const windowStart = now - ErrorBoundaryConfig.errorWindow; const recentErrors = errorState.errors.filter(e => new Date(e.timestamp).getTime() > windowStart); return recentErrors.length >= ErrorBoundaryConfig.maxErrors; }; const getErrorRate = () => { const now = Date.now(); const oneMinuteAgo = now - 6e4; const recentErrors = errorState.errors.filter(e => new Date(e.timestamp).getTime() > oneMinuteAgo); return recentErrors.length; }; const showErrorNotification = error => { try { const Y = window.YouTubeUtils; if (!Y || !Y.NotificationManager || "function" != typeof Y.NotificationManager.show) { return; } const severity = categorizeSeverity(error); let message = "An error occurred"; let duration = 3e3; switch (severity) { case ErrorSeverity_LOW: message = "A minor issue occurred. Functionality should continue normally."; duration = 2e3; break; case ErrorSeverity_MEDIUM: message = "An error occurred. Some features may not work correctly."; duration = 3e3; break; case ErrorSeverity_HIGH: message = "A serious error occurred. Please refresh the page if issues persist."; duration = 5e3; break; case ErrorSeverity_CRITICAL: message = "A critical error occurred. YouTube+ may not function properly. Please report this issue."; duration = 7e3; } Y.NotificationManager.show(message, { duration, type: "error" }); } catch (notificationError) { console.error("[YouTube+] Failed to show error notification:", notificationError); } }; const attemptRecovery = (error, context) => { if (!ErrorBoundaryConfig.enableRecovery || errorState.isRecovering) { return; } const severity = categorizeSeverity(error); if (severity !== ErrorSeverity_CRITICAL) { errorState.isRecovering = !0; try { severity === ErrorSeverity_LOW || (error => { const rate = getErrorRate(); if (rate > 5) { return !0; } const tenSecondsAgo = Date.now() - 1e4; const recentSimilar = errorState.errors.filter(e => new Date(e.timestamp).getTime() > tenSecondsAgo && e.message === error.message && e.severity === categorizeSeverity(error)); return recentSimilar.length > 0; })(error) || showErrorNotification(error); const RecoveryUtils = window.YouTubePlusErrorRecovery; RecoveryUtils && RecoveryUtils.attemptRecovery ? RecoveryUtils.attemptRecovery(error, context) : performLegacyRecovery(error, context); setTimeout(() => { errorState.isRecovering = !1; }, 5e3); } catch (recoveryError) { console.error("[YouTube+] Recovery attempt failed:", recoveryError); errorState.isRecovering = !1; } } else { console.error("[YouTube+] Critical error detected. Script may not function properly."); showErrorNotification(error); } }; const performLegacyRecovery = (error, context) => { if (context.module) { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug(`[YouTube+] Attempting recovery for module: ${context.module}`); window; error.message && (error.message.includes("null") || error.message.includes("undefined")) && context.element && window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+] Attempting to re-query DOM element"); } }; const handleError = event => { const error = event.error || new Error(event.message); const message = (error.message || event.message || "").trim(); if (message.includes("ResizeObserver loop")) { return !1; } const source = event.filename || ""; const isCrossOriginSource = source && !source.startsWith(window.location.origin) && !/YouTube\+/.test(source); if (!message && isCrossOriginSource) { return !1; } if (!message || "(no message)" === message && isCrossOriginSource) { return !1; } errorState.errorCount++; errorState.lastErrorTime = Date.now(); logError(error, { type: "uncaught", filename: event.filename, lineno: event.lineno, colno: event.colno }); if (isErrorRateExceeded()) { console.error("[YouTube+] Error rate exceeded! Too many errors in short period. Some features may be disabled."); return !1; } attemptRecovery(error, { type: "uncaught" }); return !1; }; const handleUnhandledRejection = event => { const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason)); logError(error, { type: "unhandledRejection", promise: event.promise }); isErrorRateExceeded() ? console.error("[YouTube+] Promise rejection rate exceeded!") : attemptRecovery(error, { type: "unhandledRejection" }); }; const withErrorBoundary = (fn, context = "unknown") => function(...args) { try { const fnAny = fn; return fnAny.call(this, ...args); } catch (error) { logError(error, { module: context, args }); attemptRecovery(error, { module: context }); return null; } }; const withAsyncErrorBoundary = (fn, context = "unknown") => async function(...args) { try { const fnAny = fn; return await fnAny.call(this, ...args); } catch (error) { logError(error, { module: context, args }); attemptRecovery(error, { module: context }); return null; } }; const getErrorStats = () => ({ totalErrors: errorState.errorCount, recentErrors: errorState.errors.length, lastErrorTime: errorState.lastErrorTime, isRecovering: errorState.isRecovering, errorsByType: errorState.errors.reduce((acc, e) => { acc[e.severity] = (acc[e.severity] || 0) + 1; return acc; }, {}) }); const clearErrors = () => { errorState.errors = []; try { localStorage.removeItem(ErrorBoundaryConfig.storageKey); } catch {} }; if ("undefined" != typeof window) { window.addEventListener("error", handleError, !0); window.addEventListener("unhandledrejection", handleUnhandledRejection, !0); window.YouTubeErrorBoundary = { withErrorBoundary, withAsyncErrorBoundary, getErrorStats, clearErrors, logError, getErrorRate, config: ErrorBoundaryConfig }; window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+][Error Boundary]", "Error boundary initialized"); } })(); !(function() { "use strict"; const PerformanceConfig = { enabled: !0, sampleRate: .01, storageKey: "youtube_plus_performance", metricsRetention: 100, enableConsoleOutput: !1, logLevel: "info" }; const isTestEnv = (() => { try { return "undefined" != typeof process && !!process?.env?.JEST_WORKER_ID; } catch { return !1; } })(); PerformanceConfig.sampleRate = isTestEnv ? 1 : (() => { try { const cfg = window.YouTubePlusConfig; const explicit = cfg?.performance?.sampleRate ?? cfg?.performanceSampleRate ?? cfg?.perfSampleRate ?? void 0; if ("number" == typeof explicit && isFinite(explicit)) { return Math.min(1, Math.max(0, explicit)); } } catch {} return PerformanceConfig.sampleRate; })(); try { !isTestEnv && PerformanceConfig.sampleRate < 1 && Math.random() > PerformanceConfig.sampleRate && (PerformanceConfig.enabled = !1); } catch {} const metrics = { timings: new Map, marks: new Map, measures: [], resources: [], webVitals: { LCP: null, CLS: 0, FID: null, INP: null, FCP: null, TTFB: null } }; const mark = name => { if (PerformanceConfig.enabled) { try { "undefined" != typeof performance && performance.mark && performance.mark(name); metrics.marks.set(name, Date.now()); } catch (e) { console.warn("[YouTube+ Perf] Failed to create mark:", e); } } }; const measure = (name, startMark, endMark) => { if (!PerformanceConfig.enabled) { return 0; } try { const startTime = metrics.marks.get(startMark); if (!startTime) { 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); metrics.measures.length > PerformanceConfig.metricsRetention && metrics.measures.shift(); PerformanceConfig.enableConsoleOutput && window.YouTubeUtils?.logger?.debug?.(`[YouTube+ Perf] ${name}: ${duration.toFixed(2)}ms`); if ("undefined" != typeof performance && performance.measure) { try { performance.measure(name, startMark, endMark); } catch {} } return duration; } catch (e) { console.warn("[YouTube+ Perf] Failed to measure:", e); return 0; } }; const timeFunction = (name, fn) => PerformanceConfig.enabled ? function(...args) { const startMark = `${name}-start-${Date.now()}`; mark(startMark); try { const fnAny = fn; const result = fnAny.apply(this, args); if (result && "function" == typeof result.then) { return result.finally(() => { measure(name, startMark, void 0); }); } measure(name, startMark, void 0); return result; } catch (error) { measure(name, startMark, void 0); throw error; } } : fn; const timeAsyncFunction = (name, fn) => PerformanceConfig.enabled ? async function(...args) { const startMark = `${name}-start-${Date.now()}`; mark(startMark); try { const fnAny = fn; const result = await fnAny.apply(this, args); measure(name, startMark, void 0); return result; } catch (error) { measure(name, startMark, void 0); throw error; } } : fn; const recordMetric = (name, value, metadata = {}) => { if (!PerformanceConfig.enabled) { return; } const metric = { name, value, timestamp: Date.now(), ...metadata }; metrics.timings.set(name, metric); PerformanceConfig.enableConsoleOutput && window.YouTubeUtils?.logger?.debug?.(`[YouTube+ Perf] ${name}: ${value}`, metadata); }; const getStats = metricName => { if (metricName) { const filtered = metrics.measures.filter(m => m.name === metricName); if (0 === filtered.length) { return null; } const durations = filtered.map(m => m.duration); return { name: metricName, count: durations.length, min: Math.min(...durations), max: Math.max(...durations), avg: durations.reduce((a, b) => a + b, 0) / durations.length, latest: durations[durations.length - 1] }; } const allMetrics = {}; const metricNames = [ ...new Set(metrics.measures.map(m => m.name)) ]; metricNames.forEach(name => { allMetrics[name] = getStats(name); }); return { metrics: allMetrics, webVitals: { ...metrics.webVitals }, totalMeasures: metrics.measures.length, totalMarks: metrics.marks.size, customMetrics: Object.fromEntries(metrics.timings) }; }; const getMemoryUsage = () => { if ("undefined" == typeof performance || !performance.memory) { return null; } try { const memory = performance.memory; return { usedJSHeapSize: memory.usedJSHeapSize, totalJSHeapSize: memory.totalJSHeapSize, jsHeapSizeLimit: memory.jsHeapSizeLimit, usedPercent: (memory.usedJSHeapSize / memory.jsHeapSizeLimit * 100).toFixed(2) }; } catch { return null; } }; const trackMemory = () => { const memory = getMemoryUsage(); memory && recordMetric("memory-usage", memory.usedJSHeapSize, { totalJSHeapSize: memory.totalJSHeapSize, usedPercent: memory.usedPercent }); }; const checkThresholds = thresholds => { const violations = []; const allStats = getStats(void 0); if (!allStats || !allStats.metrics) { return violations; } Object.entries(thresholds).forEach(([metricName, threshold]) => { const stat = allStats.metrics[metricName]; stat && stat.avg > threshold && violations.push({ metric: metricName, threshold, actual: stat.avg, exceeded: stat.avg - threshold }); }); return violations; }; const exportMetrics = () => { const data = { timestamp: (new Date).toISOString(), userAgent: navigator.userAgent, url: window.location.href, memory: getMemoryUsage(), stats: getStats(void 0), measures: metrics.measures, customMetrics: Object.fromEntries(metrics.timings), webVitals: metrics.webVitals }; return JSON.stringify(data, null, 2); }; const exportToFile = (filename = "youtube-plus-performance.json") => { try { const data = exportMetrics(); if ("undefined" == typeof Blob) { console.warn("[YouTube+ Perf] Blob API not available"); return !1; } const blob = new Blob([ data ], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); return !0; } catch (e) { console.error("[YouTube+ Perf] Failed to export to file:", e); return !1; } }; const aggregateByPeriod = (periodMs = 6e4) => { const periods = new Map; metrics.measures.forEach(measure => { const periodStart = Math.floor(measure.timestamp / periodMs) * periodMs; periods.has(periodStart) || periods.set(periodStart, []); periods.get(periodStart).push(measure); }); const aggregated = []; periods.forEach((measures, periodStart) => { const durations = measures.map(m => m.duration); aggregated.push({ period: new Date(periodStart).toISOString(), count: durations.length, min: Math.min(...durations), max: Math.max(...durations), avg: durations.reduce((a, b) => a + b, 0) / durations.length }); }); return aggregated; }; const clearMetrics = () => { metrics.timings.clear(); metrics.marks.clear(); metrics.measures = []; metrics.resources = []; metrics.webVitals = { LCP: null, CLS: 0, FID: null, INP: null, FCP: null, TTFB: null }; try { localStorage.removeItem(PerformanceConfig.storageKey); } catch {} if ("undefined" != typeof performance && performance.clearMarks) { try { performance.clearMarks(); performance.clearMeasures(); } catch {} } }; const monitorMutations = (element, name) => { if (!PerformanceConfig.enabled) { return null; } let mutationCount = 0; const startTime = Date.now(); const observer = new MutationObserver(mutations => { mutationCount += mutations.length; recordMetric(`${name}-mutations`, mutationCount, { elapsed: Date.now() - startTime }); }); observer.observe(element, { childList: !0, subtree: !0, attributes: !0 }); return observer; }; const getPerformanceEntries = type => { if ("undefined" == typeof performance || !performance.getEntriesByType) { return []; } try { return performance.getEntriesByType(type); } catch { return []; } }; const logPageLoadMetrics = () => { if (PerformanceConfig.enabled) { 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); } } }; if ("undefined" != typeof window) { "complete" === document.readyState ? logPageLoadMetrics() : window.addEventListener("load", logPageLoadMetrics, { once: !0 }); PerformanceConfig.enabled && (() => { if ("undefined" != typeof PerformanceObserver) { try { new PerformanceObserver(entryList => { const entries = entryList.getEntries(); const lastEntry = entries[entries.length - 1]; metrics.webVitals.LCP = lastEntry.startTime; PerformanceConfig.enableConsoleOutput && console.warn(`[YouTube+ Perf] LCP: ${lastEntry.startTime.toFixed(2)}ms`, lastEntry); }).observe({ type: "largest-contentful-paint", buffered: !0 }); new PerformanceObserver(entryList => { for (const entry of entryList.getEntries()) { entry.hadRecentInput || (metrics.webVitals.CLS += entry.value); } PerformanceConfig.enableConsoleOutput && "debug" === PerformanceConfig.logLevel && console.warn(`[YouTube+ Perf] CLS: ${metrics.webVitals.CLS.toFixed(4)}`); }).observe({ type: "layout-shift", buffered: !0 }); new PerformanceObserver(entryList => { const firstInput = entryList.getEntries()[0]; metrics.webVitals.FID = firstInput.processingStart - firstInput.startTime; PerformanceConfig.enableConsoleOutput && console.warn(`[YouTube+ Perf] FID: ${metrics.webVitals.FID.toFixed(2)}ms`); }).observe({ type: "first-input", buffered: !0 }); try { new PerformanceObserver(entryList => { const entries = entryList.getEntries(); const maxDuration = Math.max(...entries.map(e => e.duration)); metrics.webVitals.INP = maxDuration; }).observe({ type: "event", buffered: !0, durationThreshold: 16 }); } catch (e) {} } catch (e) { console.warn("[YouTube+ Perf] Failed to init PerformanceObserver:", e); } } })(); const RAFScheduler = (() => { let rafId = null; const callbacks = new Set; const flush = () => { rafId = null; Array.from(callbacks).forEach(cb => { try { cb(); } catch (e) { console.error("[RAF] Error:", e); } }); callbacks.clear(); }; return { schedule: callback => { callbacks.add(callback); rafId || (rafId = requestAnimationFrame(flush)); return () => callbacks.delete(callback); }, cancelAll: () => { rafId && cancelAnimationFrame(rafId); rafId = null; callbacks.clear(); } }; })(); const LazyLoader = (() => { const observers = new Map; return { create: (options = {}) => { const {root = null, rootMargin = "50px", threshold = .01, onIntersect} = options; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { onIntersect(entry.target, entry); observer.unobserve(entry.target); } }); }, { root, rootMargin, threshold }); observers.set(observer, new Set); return { observe: el => { if (el instanceof Element) { observer.observe(el); observers.get(observer).add(el); } }, unobserve: el => { if (el instanceof Element) { observer.unobserve(el); observers.get(observer)?.delete(el); } }, disconnect: () => { observer.disconnect(); observers.delete(observer); } }; }, disconnectAll: () => { observers.forEach((_, o) => o.disconnect()); observers.clear(); } }; })(); const DOMBatcher = (() => { const batches = new Map; return { batch: (container, elements) => { batches.has(container) || batches.set(container, []); batches.get(container).push(...elements); }, flush: () => { RAFScheduler.schedule(() => { batches.forEach((elements, container) => { if (!container.isConnected) { batches.delete(container); return; } const frag = document.createDocumentFragment(); elements.forEach(el => frag.appendChild(el)); container.appendChild(frag); }); batches.clear(); }); }, clear: container => batches.delete(container) }; })(); const ElementCache = (() => { const cache = new WeakMap; return { get: (el, key) => cache.get(el)?.[key], set: (el, key, val) => { let data = cache.get(el); if (!data) { data = {}; cache.set(el, data); } data[key] = val; }, has: (el, key) => { const data = cache.get(el); return !!data && key in data; }, delete: (el, key) => { const data = cache.get(el); data && delete data[key]; } }; })(); window.YouTubePerformance = { mark, measure, timeFunction, timeAsyncFunction, recordMetric, getStats, exportMetrics, exportToFile, clearMetrics, monitorMutations, getPerformanceEntries, getMemoryUsage, trackMemory, checkThresholds, aggregateByPeriod, config: PerformanceConfig, RAFScheduler, LazyLoader, DOMBatcher, ElementCache }; const yieldToMain = () => new Promise(resolve => { "scheduler" in window && "function" == typeof window.scheduler?.yield ? window.scheduler.yield().then(resolve) : setTimeout(resolve, 0); }); const runChunkedTasks = async (tasks, yieldInterval = 50) => { let lastYield = performance.now(); for (const task of tasks) { task(); const now = performance.now(); if (now - lastYield > yieldInterval) { await yieldToMain(); lastYield = performance.now(); } } }; const wrapForINP = (handler, options = {}) => { const {maxBlockTime = 50} = options; return async function(...args) { const start = performance.now(); let result; try { result = handler.apply(this, args); result && "function" == typeof result.then && (result = await result); } finally { const elapsed = performance.now() - start; elapsed > maxBlockTime && recordMetric("long-task", elapsed, { handler: handler.name || "anonymous" }); } return result; }; }; window.YouTubePerformance.yieldToMain = yieldToMain; window.YouTubePerformance.runChunkedTasks = runChunkedTasks; window.YouTubePerformance.wrapForINP = wrapForINP; const injectResourceHints = () => { const origins = [ "https://www.youtube.com", "https://i.ytimg.com", "https://yt3.ggpht.com", "https://fonts.googleapis.com", "https://www.gstatic.com", "https://play.google.com" ]; const head = document.head; if (!head) { return; } const existingHrefs = new Set; head.querySelectorAll('link[rel="preconnect"]').forEach(el => { existingHrefs.add(el.href); }); for (const origin of origins) { if (existingHrefs.has(origin) || existingHrefs.has(origin + "/")) { continue; } const link = document.createElement("link"); link.rel = "preconnect"; link.href = origin; link.crossOrigin = "anonymous"; head.appendChild(link); } }; const boostLCPElement = () => { const path = location.pathname; let lcpSelector; lcpSelector = "/watch" === path || path.startsWith("/shorts/") ? "#movie_player .ytp-cued-thumbnail-overlay-image, #movie_player video, ytd-player #ytd-player .html5-video-container" : "/playlist" === path ? "ytd-playlist-video-renderer:first-child img.yt-core-image" : "ytd-rich-item-renderer:first-child img.yt-core-image, ytd-rich-grid-media img.yt-core-image"; lcpSelector && requestAnimationFrame(() => { const el = document.querySelector(lcpSelector); if (el && "IMG" === el.tagName) { el.setAttribute("fetchpriority", "high"); el.setAttribute("loading", "eager"); "lazy" === el.loading && (el.loading = "eager"); } }); }; const injectContentVisibilityCSS = () => { const cssId = "ytp-perf-content-visibility"; if (document.getElementById(cssId)) { return; } const style = document.createElement("style"); style.id = cssId; style.textContent = '\n /* ── YouTube+ LCP Performance Optimizations ── */\n\n /* Off-screen section rendering deferral */\n ytd-comments#comments { content-visibility: auto; contain-intrinsic-size: auto 800px; }\n #secondary ytd-compact-video-renderer:nth-child(n+6) { content-visibility: auto; contain-intrinsic-size: auto 94px; }\n ytd-watch-next-secondary-results-renderer ytd-item-section-renderer { content-visibility: auto; contain-intrinsic-size: auto 600px; }\n\n /* Main/browse feed - defer items below first viewport */\n ytd-rich-grid-renderer #contents > ytd-rich-item-renderer:nth-child(n+9) { content-visibility: auto; contain-intrinsic-size: auto 360px; }\n ytd-section-list-renderer > #contents > ytd-item-section-renderer:nth-child(n+3) { content-visibility: auto; contain-intrinsic-size: auto 500px; }\n\n /* Playlist page - defer items beyond visible viewport */\n ytd-playlist-video-list-renderer #contents > ytd-playlist-video-renderer:nth-child(n+12) { content-visibility: auto; contain-intrinsic-size: auto 90px; }\n\n /* Note: contain:layout is intentionally omitted here — it breaks position:sticky\n for chips-wrapper and tabs-container on browse/channel pages. */\n\n /* Guide sidebar - not needed for LCP */\n ytd-mini-guide-renderer { content-visibility: auto; contain-intrinsic-size: auto 100vh; }\n tp-yt-app-drawer#guide { content-visibility: auto; contain-intrinsic-size: 240px 100vh; }\n\n /* Below-the-fold metadata */\n ytd-watch-metadata #description { content-visibility: auto; contain-intrinsic-size: auto 120px; }\n ytd-structured-description-content-renderer { content-visibility: auto; contain-intrinsic-size: auto 200px; }\n\n /* Shorts shelf on browse pages */\n ytd-reel-shelf-renderer { content-visibility: auto; contain-intrinsic-size: auto 320px; }\n\n /* Comments container on main watch - contain:style only, not layout (preserves sticky) */\n ytd-item-section-renderer#sections { contain: style; }\n\n /* Reduce paint complexity for non-visible items */\n ytd-rich-grid-row:nth-child(n+4) { content-visibility: auto; contain-intrinsic-size: auto 240px; }\n\n /* Engagement panels - safe deferral only when fully hidden */\n ytd-engagement-panel-section-list-renderer[visibility="ENGAGEMENT_PANEL_VISIBILITY_HIDDEN"] { content-visibility: auto; contain-intrinsic-size: auto 0px; }\n\n /* Optimize image decoding */\n ytd-thumbnail img, yt-image img, .yt-core-image { content-visibility: auto; }\n '; (document.head || document.documentElement).appendChild(style); }; const setupDeferredImageLoading = () => { const imgObserver = new IntersectionObserver(entries => { for (const entry of entries) { if (entry.isIntersecting) { const img = entry.target; const dataSrc = img.getAttribute("data-ytp-deferred-src"); if (dataSrc) { img.src = dataSrc; img.removeAttribute("data-ytp-deferred-src"); } imgObserver.unobserve(img); } } }, { rootMargin: "200px 0px" }); let imgTimer = null; const scheduleObserve = () => { imgTimer || (imgTimer = setTimeout(() => { imgTimer = null; (() => { const belowFold = document.querySelectorAll("ytd-rich-item-renderer:nth-child(n+5) img[src]:not([data-ytp-img-observed]),ytd-compact-video-renderer:nth-child(n+4) img[src]:not([data-ytp-img-observed])"); belowFold.forEach(img => { img.setAttribute("data-ytp-img-observed", "1"); }); })(); }, 500)); }; const _pCm = window.YouTubeUtils?.cleanupManager; _pCm?.registerListener ? _pCm.registerListener(window, "yt-navigate-finish", scheduleObserve, { passive: !0 }) : window.addEventListener("yt-navigate-finish", scheduleObserve, { passive: !0 }); "loading" !== document.readyState ? scheduleObserve() : document.addEventListener("DOMContentLoaded", scheduleObserve, { once: !0 }); }; const SharedMutationManager = (() => { let observer = null; const callbacks = new Map; let scheduled = !1; const pending = []; const flush = () => { scheduled = !1; const entries = [ ...pending ]; pending.length = 0; for (const [, {callback, filter}] of callbacks) { const filtered = filter ? entries.filter(filter) : entries; if (filtered.length > 0) { try { callback(filtered); } catch (e) { console.warn("[YouTube+ Perf] SharedMutation callback error:", e); } } } }; return { register(key, callback, filter) { callbacks.set(key, { callback, filter }); 1 === callbacks.size && (() => { if (observer) { return; } observer = new MutationObserver(mutations => { pending.push(...mutations); if (!scheduled) { scheduled = !0; queueMicrotask(flush); } }); const target = document.body || document.documentElement; target && observer.observe(target, { childList: !0, subtree: !0 }); })(); }, unregister(key) { callbacks.delete(key); if (0 === callbacks.size && observer) { observer.disconnect(); observer = null; } }, getCallbackCount: () => callbacks.size }; })(); const IdleScheduler = (() => { const queue = []; let running = !1; const processQueue = deadline => { for (;queue.length > 0 && (!deadline || deadline.timeRemaining() > 5); ) { const task = queue.shift(); try { task.fn(); } catch (e) { console.warn("[YouTube+ Perf] Idle task error:", e); } if (!deadline) { break; } } queue.length > 0 ? scheduleNext() : running = !1; }; const scheduleNext = () => { "function" == typeof requestIdleCallback ? requestIdleCallback(processQueue, { timeout: 3e3 }) : setTimeout(() => processQueue(null), 50); }; return { schedule(fn, priority = 0) { queue.push({ fn, priority }); queue.sort((a, b) => b.priority - a.priority); if (!running) { running = !0; scheduleNext(); } }, pending: () => queue.length }; })(); const initLongTaskMonitor = () => { if ("undefined" != typeof PerformanceObserver) { try { const longTasks = []; new PerformanceObserver(list => { for (const entry of list.getEntries()) { longTasks.push({ duration: entry.duration, startTime: entry.startTime, name: entry.name }); longTasks.length > 50 && longTasks.shift(); } recordMetric("long-tasks-count", longTasks.length); const totalBlocking = longTasks.reduce((sum, t) => sum + Math.max(0, t.duration - 50), 0); recordMetric("total-blocking-time", totalBlocking); }).observe({ type: "longtask", buffered: !0 }); } catch {} } }; const initNavigationTracking = () => { const _pCm2 = window.YouTubeUtils?.cleanupManager; const _addL = (t, ev, fn, o) => { _pCm2?.registerListener ? _pCm2.registerListener(t, ev, fn, o) : t.addEventListener(ev, fn, o); }; _addL(window, "yt-navigate-start", () => { mark("yt-navigate-start"); }, { passive: !0 }); _addL(window, "yt-navigate-finish", () => { mark("yt-navigate-finish"); measure("yt-navigation-duration", "yt-navigate-start"); requestAnimationFrame(() => { boostLCPElement(); }); }, { passive: !0 }); }; const initLCPOptimizations = () => { try { injectResourceHints(); injectContentVisibilityCSS(); boostLCPElement(); queueMicrotask(() => { initNavigationTracking(); initLongTaskMonitor(); }); IdleScheduler.schedule(() => setupDeferredImageLoading(), 2); } catch (e) { console.warn("[YouTube+ Perf] LCP optimization init error:", e); } }; initLCPOptimizations(); window.YouTubePerformance.SharedMutationManager = SharedMutationManager; window.YouTubePerformance.IdleScheduler = IdleScheduler; window.YouTubePerformance.boostLCPElement = boostLCPElement; window.YouTubePerformance.injectResourceHints = injectResourceHints; window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+] Performance monitoring initialized"); } })(); !(function() { "use strict"; const OptimizedSelectors = { player: "#movie_player", video: "video.video-stream.html5-main-video", videoAlt: "#movie_player video", chromeBottom: ".ytp-chrome-bottom", watchFlexy: "ytd-watch-flexy", secondary: "#secondary", rightTabs: "#right-tabs", playlistPanel: "ytd-playlist-panel-renderer", tabInfo: "#tab-info", tabComments: "#tab-comments", tabVideos: "#tab-videos", likeButton: "like-button-view-model button", dislikeButton: "dislike-button-view-model button", subscribeButton: "#subscribe-button", shorts: "ytd-shorts", activeReel: "ytd-reel-video-renderer[is-active]", masthead: "ytd-masthead", ytdApp: "ytd-app" }; function batchQuery(queries) { return queries.map(({selector, multi = !1, context = document}) => multi ? Array.from(context.querySelectorAll(selector)) : context.querySelector(selector)); } const globalCache = new class DOMCache { constructor() { this.cache = new Map; this.multiCache = new Map; this.maxAge = 5e3; this.nullMaxAge = 1e3; this.maxSize = 500; this.cleanupInterval = null; this.enabled = !0; this.stats = { hits: 0, misses: 0, evictions: 0 }; this.contextUids = new WeakMap; this.uidCounter = 0; this.observerCallbacks = new Set; this.sharedObserver = null; this.sharedObserverPending = !1; this.startCleanup(); } getContextUid(ctx) { if (ctx === document) { return "doc"; } let uid = this.contextUids.get(ctx); if (!uid) { uid = ++this.uidCounter; this.contextUids.set(ctx, uid); } return uid; } querySelector(selector, context = document, skipCache = !1) { if (!this.enabled || skipCache) { return context.querySelector(selector); } const cacheKey = `${selector}::${this.getContextUid(context)}`; const cached = this.cache.get(cacheKey); const now = Date.now(); const ttl = cached && cached.element ? this.maxAge : this.nullMaxAge; if (cached && now - cached.timestamp < ttl) { if (!cached.element) { this.stats.hits++; return null; } if (this.isElementInDOM(cached.element)) { this.stats.hits++; return cached.element; } } this.stats.misses++; if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); this.stats.evictions++; } const element = context.querySelector(selector); this.cache.set(cacheKey, { element, timestamp: now }); return element; } querySelectorAll(selector, context = document, skipCache = !1) { if (!this.enabled || skipCache) { return context.querySelectorAll(selector); } const cacheKey = `ALL::${selector}::${this.getContextUid(context)}`; const cached = this.multiCache.get(cacheKey); if (cached && this.areElementsValid(cached)) { return cached; } const elements = Array.from(context.querySelectorAll(selector)); this.multiCache.set(cacheKey, elements); const ttl = elements.length > 0 ? this.maxAge : this.nullMaxAge; const timeoutId = setTimeout(() => this.multiCache.delete(cacheKey), ttl); "undefined" != typeof window && window.YouTubeUtils?.cleanupManager?.registerTimeout && window.YouTubeUtils.cleanupManager.registerTimeout(timeoutId); return elements; } getElementById(id) { if (!this.enabled) { return document.getElementById(id); } const cacheKey = `ID::${id}`; const cached = this.cache.get(cacheKey); const now = Date.now(); if (cached && now - cached.timestamp < this.maxAge && cached.element && this.isElementInDOM(cached.element)) { return cached.element; } const element = document.getElementById(id); this.cache.set(cacheKey, { element, timestamp: now }); return element; } isElementInDOM(element) { return element && document.contains(element); } areElementsValid(elements) { return !elements || 0 === elements.length || this.isElementInDOM(elements[0]) && this.isElementInDOM(elements[elements.length - 1]); } invalidate(selector) { if (selector) { for (const key of this.cache.keys()) { key.includes(selector) && this.cache.delete(key); } for (const key of this.multiCache.keys()) { key.includes(selector) && this.multiCache.delete(key); } } else { this.cache.clear(); this.multiCache.clear(); } } startCleanup() { if (this.cleanupInterval) { return; } const cleanupFn = () => { const now = Date.now(); let deletedCount = 0; for (const [key, value] of this.cache.entries()) { if (now - value.timestamp > this.maxAge || value.element && !this.isElementInDOM(value.element)) { this.cache.delete(key); deletedCount++; if (deletedCount >= 50) { break; } } } }; this.cleanupInterval = setInterval(() => { "undefined" != typeof requestIdleCallback ? requestIdleCallback(cleanupFn, { timeout: 1e3 }) : cleanupFn(); }, 5e3); window.YouTubeUtils?.cleanupManager?.registerInterval && window.YouTubeUtils.cleanupManager.registerInterval(this.cleanupInterval); } destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.cache.clear(); this.multiCache.clear(); if (this.sharedObserver) { this.sharedObserver.disconnect(); this.sharedObserver = null; } this.observerCallbacks.clear(); } getStats() { return { size: this.cache.size, multiSize: this.multiCache.size, enabled: this.enabled }; } initSharedObserver() { if (!this.sharedObserver) { this.sharedObserver = new MutationObserver(() => { if (0 === this.observerCallbacks.size) { return; } if (this.sharedObserverPending) { return; } this.sharedObserverPending = !0; const flush = () => { this.sharedObserverPending = !1; for (const callback of this.observerCallbacks) { try { callback(); } catch (e) { "undefined" != typeof window && window.YouTubeUtils?.logError && window.YouTubeUtils.logError("DOMCache", "Observer callback error", e); } } }; "function" == typeof requestAnimationFrame ? requestAnimationFrame(flush) : setTimeout(flush, 0); }); this.sharedObserver.observe(document.body || document.documentElement, { childList: !0, subtree: !0 }); } } }; const scopedCache = new class ScopedDOMCache { constructor() { this.scopedCaches = new Map; } getScope(scope) { this.scopedCaches.has(scope) || this.scopedCaches.set(scope, new WeakMap); return this.scopedCaches.get(scope); } set(scope, element, value) { this.getScope(scope).set(element, value); } get(scope, element) { return this.getScope(scope).get(element); } has(scope, element) { return this.getScope(scope).has(element); } }; function waitForElement(selector, timeout = 5e3, context = document) { return new Promise(resolve => { const existing = context.querySelector(selector); if (existing) { resolve(existing); return; } const isPlaylistPage = "undefined" != typeof window && window.location && "string" == typeof window.location.pathname && "/playlist" === window.location.pathname; if (isPlaylistPage && (context === document || context === document.body)) { const interval = 250; const start = Date.now(); const timerId = setInterval(() => { const element = context.querySelector(selector); if (element) { clearInterval(timerId); resolve(element); } else if (Date.now() - start >= timeout) { clearInterval(timerId); resolve(null); } }, interval); return; } const useShared = context === document || context === document.body; if (useShared) { globalCache.initSharedObserver(); const checkCallback = () => { const element = context.querySelector(selector); if (element) { globalCache.observerCallbacks.delete(checkCallback); resolve(element); return !0; } return !1; }; globalCache.observerCallbacks.add(checkCallback); setTimeout(() => { globalCache.observerCallbacks.delete(checkCallback); resolve(null); }, timeout); } else { const observer = new MutationObserver(() => { const element = context.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(context, { childList: !0, subtree: !0 }); setTimeout(() => { observer.disconnect(); resolve(null); }, timeout); } }); } if ("undefined" != typeof window) { window.YouTubeDOMCache = globalCache; window.YouTubeScopedCache = scopedCache; window.YouTubeSelectors = OptimizedSelectors; window.batchQueryDOM = batchQuery; window.waitForElement = waitForElement; if (window.YouTubeUtils) { window.YouTubeUtils.domCache = globalCache; window.YouTubeUtils.scopedCache = scopedCache; window.YouTubeUtils.selectors = OptimizedSelectors; window.YouTubeUtils.batchQuery = batchQuery; window.YouTubeUtils.waitFor = waitForElement; } } if ("undefined" != typeof window && window.addEventListener) { window.addEventListener("yt-navigate-finish", () => { globalCache.invalidate(); }); window.addEventListener("spfdone", () => { globalCache.invalidate(); }); } "undefined" != typeof window && window.addEventListener && window.addEventListener("beforeunload", () => { globalCache.destroy(); }); })(); !(function() { "use strict"; class EventDelegator { constructor() { this.delegatedHandlers = new Map; this.registeredDelegators = new Map; this.stats = { totalDelegations: 0, totalHandlers: 0 }; } delegate(parent, eventType, selector, handler, options = {}) { if (!(parent && eventType && selector && handler)) { console.warn("[EventDelegator] Invalid parameters"); return; } const parentKey = this._getElementKey(parent); const delegationKey = `${parentKey}:${eventType}`; this.delegatedHandlers.has(delegationKey) || this.delegatedHandlers.set(delegationKey, new Map); const handlersForSelector = this.delegatedHandlers.get(delegationKey); handlersForSelector.has(selector) || handlersForSelector.set(selector, new Set); handlersForSelector.get(selector).add(handler); this.stats.totalHandlers++; this.registeredDelegators.has(parent) || this.registeredDelegators.set(parent, new Map); const parentDelegators = this.registeredDelegators.get(parent); if (!parentDelegators.has(eventType)) { const delegatedListener = event => { this._handleDelegatedEvent(parent, eventType, event); }; parent.addEventListener(eventType, delegatedListener, options); parentDelegators.set(eventType, delegatedListener); this.stats.totalDelegations++; window.YouTubeUtils?.logger?.debug?.(`[EventDelegator] Created delegation on ${parentKey} for ${eventType}`); } } undelegate(parent, eventType, selector, handler) { const parentKey = this._getElementKey(parent); const delegationKey = `${parentKey}:${eventType}`; const handlersForSelector = this.delegatedHandlers.get(delegationKey); if (!handlersForSelector) { return; } const handlers = handlersForSelector.get(selector); if (handlers) { handlers.delete(handler); this.stats.totalHandlers--; 0 === handlers.size && handlersForSelector.delete(selector); if (0 === handlersForSelector.size) { this._removeParentListener(parent, eventType); this.delegatedHandlers.delete(delegationKey); } } } _handleDelegatedEvent(parent, eventType, event) { const parentKey = this._getElementKey(parent); const delegationKey = `${parentKey}:${eventType}`; const handlersForSelector = this.delegatedHandlers.get(delegationKey); if (handlersForSelector) { for (const [selector, handlers] of handlersForSelector.entries()) { const target = event.target.closest(selector); if (target && parent.contains(target)) { for (const handler of handlers) { try { handler.call(target, event, target); } catch (error) { console.error("[EventDelegator] Handler error:", error); window.YouTubeUtils?.logger?.error?.("[EventDelegator] Handler error", error); } } } } } } _removeParentListener(parent, eventType) { const parentDelegators = this.registeredDelegators.get(parent); if (!parentDelegators) { return; } const listener = parentDelegators.get(eventType); if (listener) { parent.removeEventListener(eventType, listener); parentDelegators.delete(eventType); this.stats.totalDelegations--; } 0 === parentDelegators.size && this.registeredDelegators.delete(parent); } _getElementKey(element) { if (element === document) { return "document"; } if (element === window) { return "window"; } if (element === document.body) { return "body"; } if (!this._elementKeyMap) { this._elementKeyMap = new WeakMap; this._elementKeyCounter = 0; } if (element.id) { return element.id; } let key = this._elementKeyMap.get(element); if (!key) { key = `${element.tagName || "ELEM"}_${++this._elementKeyCounter}`; this._elementKeyMap.set(element, key); } return key; } getStats() { return { ...this.stats, uniqueDelegations: this.registeredDelegators.size, delegationKeys: this.delegatedHandlers.size }; } clear() { for (const [parent, delegators] of this.registeredDelegators.entries()) { for (const [eventType, listener] of delegators.entries()) { try { parent.removeEventListener(eventType, listener); } catch {} } } this.delegatedHandlers.clear(); this.registeredDelegators.clear(); if (this._elementKeyMap) { this._elementKeyMap = new WeakMap; this._elementKeyCounter = 0; } this.stats = { totalDelegations: 0, totalHandlers: 0 }; } } const eventDelegator = new EventDelegator; const on = (parent, eventType, selector, handler, options) => { eventDelegator.delegate(parent, eventType, selector, handler, options); }; const off = (parent, eventType, selector, handler) => { eventDelegator.undelegate(parent, eventType, selector, handler); }; "undefined" != typeof window && (window.YouTubePlusEventDelegation = { EventDelegator, on, off, getStats: () => eventDelegator.getStats(), clear: () => eventDelegator.clear() }); "undefined" != typeof module && module.exports && (module.exports = { EventDelegator, on, off }); })(); !(function() { "use strict"; class LazyLoader { constructor() { this.modules = new Map; this.loadedModules = new Set; this.stats = { totalModules: 0, loadedModules: 0 }; this.isIdle = !1; this.idleCallbackId = null; } register(name, fn, options = {}) { if (this.modules.has(name)) { window.YouTubeUtils?.logger?.warn?.(`[LazyLoader] Module "${name}" already registered`); return; } const moduleConfig = { fn, priority: options.priority || 0, delay: options.delay || 0, dependencies: options.dependencies || [], loaded: !1 }; this.modules.set(name, moduleConfig); this.stats.totalModules++; window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Registered module "${name}" (priority: ${moduleConfig.priority})`); } async load(name) { const module = this.modules.get(name); if (!module) { window.YouTubeUtils?.logger?.warn?.(`[LazyLoader] Module "${name}" not found`); return !1; } if (module.loaded) { window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Module "${name}" already loaded`); return !0; } for (const dep of module.dependencies) { if (!this.loadedModules.has(dep)) { window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Loading dependency "${dep}" for "${name}"`); await this.load(dep); } } module.delay > 0 && await new Promise(resolve => setTimeout(resolve, module.delay)); try { window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Loading module "${name}"`); const startTime = performance.now(); await module.fn(); const loadTime = performance.now() - startTime; window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Module "${name}" loaded in ${loadTime.toFixed(2)}ms`); module.loaded = !0; this.loadedModules.add(name); this.stats.loadedModules++; return !0; } catch (error) { console.error(`[LazyLoader] Failed to load module "${name}":`, error); window.YouTubeUtils?.logger?.error?.(`[LazyLoader] Module "${name}" load failed`, error); return !1; } } async loadAll() { const sortedModules = Array.from(this.modules.entries()).sort((a, b) => b[1].priority - a[1].priority); let loadedCount = 0; for (const [name, module] of sortedModules) { if (!module.loaded) { const success = await this.load(name); success && loadedCount++; } } return loadedCount; } loadOnIdle(timeout = 2e3) { if (this.isIdle) { window.YouTubeUtils?.logger?.debug?.("[LazyLoader] Idle loading already scheduled"); return; } this.isIdle = !0; const loadModules = async () => { window.YouTubeUtils?.logger?.debug?.("[LazyLoader] Starting idle loading"); const count = await this.loadAll(); window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Loaded ${count} modules during idle`); }; this.idleCallbackId = "undefined" != typeof requestIdleCallback ? requestIdleCallback(loadModules, { timeout }) : setTimeout(loadModules, timeout); } cancelIdleLoading() { if (this.isIdle) { void 0 !== window.cancelIdleCallback && this.idleCallbackId ? window.cancelIdleCallback(this.idleCallbackId) : this.idleCallbackId && clearTimeout(this.idleCallbackId); this.isIdle = !1; this.idleCallbackId = null; } } isLoaded(name) { return this.loadedModules.has(name); } getStats() { return { ...this.stats, loadingPercentage: this.stats.totalModules > 0 ? this.stats.loadedModules / this.stats.totalModules * 100 : 0, unloadedModules: this.stats.totalModules - this.stats.loadedModules }; } clear() { this.cancelIdleLoading(); this.modules.clear(); this.loadedModules.clear(); this.stats = { totalModules: 0, loadedModules: 0 }; } } const lazyLoader = new LazyLoader; "undefined" != typeof window && (window.YouTubePlusLazyLoader = { LazyLoader, register: (name, fn, options) => lazyLoader.register(name, fn, options), load: name => lazyLoader.load(name), loadAll: () => lazyLoader.loadAll(), loadOnIdle: timeout => lazyLoader.loadOnIdle(timeout), isLoaded: name => lazyLoader.isLoaded(name), getStats: () => lazyLoader.getStats(), clear: () => lazyLoader.clear() }); "undefined" != typeof module && module.exports && (module.exports = { LazyLoader }); })(); "undefined" != typeof trustedTypes && null == trustedTypes.defaultPolicy && trustedTypes.createPolicy("default", { createHTML: s => "string" != typeof s ? String(s) : s.replace(/<script\b[\s\S]*?<\/script\s*>/gi, "").replace(/\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/javascript\s*:/gi, ""), createScriptURL: s => { if ("string" != typeof s) { return String(s); } try { const url = new URL(s, location.origin); if (url.origin === location.origin) { return s; } if (url.hostname.endsWith(".googleapis.com") || url.hostname.endsWith(".youtube.com")) { return s; } } catch {} return s; }, createScript: s => "string" != typeof s ? String(s) : s }); const defaultPolicy = "undefined" != typeof trustedTypes && trustedTypes.defaultPolicy || { createHTML: s => s }; function createHTML(s) { return defaultPolicy.createHTML(s); } "undefined" != typeof window && (window._ytplusCreateHTML = createHTML); let trustHTMLErr = null; try { document.createElement("div").innerHTML = createHTML("1"); } catch (e) { trustHTMLErr = e; } if (trustHTMLErr) { console.error("trustHTMLErr", trustHTMLErr); throw trustHTMLErr; } const executionScript = () => { const YouTubeUtils = window.YouTubeUtils || {}; const createHTML = window._ytplusCreateHTML || (s => s); try { if ("undefined" == typeof CustomElementRegistry) { return; } if (CustomElementRegistry.prototype.define000) { return; } if ("function" != typeof CustomElementRegistry.prototype.define) { return; } const HTMLElement_ = HTMLElement.prototype.constructor; const qsOne = (elm, selector) => window.YouTubeDOMCache && "function" == typeof window.YouTubeDOMCache.querySelector ? window.YouTubeDOMCache.querySelector(selector, elm) : HTMLElement_.prototype.querySelector.call(elm, selector); const _qs = selector => window.YouTubeDOMCache && "function" == typeof window.YouTubeDOMCache.get ? window.YouTubeDOMCache.get(selector) : document.querySelector(selector); function qs(a, b) { return 1 === arguments.length ? _qs(a) : qsOne(a, b); } const qsAll = (selector, context) => { const ctx = context || document; return window.YouTubeDOMCache && "function" == typeof window.YouTubeDOMCache.getAll ? window.YouTubeDOMCache.getAll(selector) : Array.from(ctx.querySelectorAll(selector)); }; const defineProperties = (p, o) => { if (p) { for (const k of Object.keys(o)) { if (!o[k]) { console.warn(`defineProperties ERROR: Property ${k} is undefined`); delete o[k]; } } return Object.defineProperties(p, o); } console.warn("defineProperties ERROR: Prototype is undefined"); }; const replaceChildrenPolyfill = function replaceChildren(...new_children) { for (;this.firstChild; ) { this.removeChild(this.firstChild); } this.append(...new_children); }; const pdsBaseDF = Object.getOwnPropertyDescriptors(DocumentFragment.prototype); pdsBaseDF.replaceChildren ? defineProperties(DocumentFragment.prototype, { replaceChildren000: pdsBaseDF.replaceChildren }) : DocumentFragment.prototype.replaceChildren000 = replaceChildrenPolyfill; const pdsBaseNode = Object.getOwnPropertyDescriptors(Node.prototype); pdsBaseNode.appendChild000 || pdsBaseNode.insertBefore000 || defineProperties(Node.prototype, { appendChild000: pdsBaseNode.appendChild, insertBefore000: pdsBaseNode.insertBefore }); const pdsBaseElement = Object.getOwnPropertyDescriptors(Element.prototype); if (!pdsBaseElement.setAttribute000 && !pdsBaseElement.querySelector000) { const nPdsElement = { setAttribute000: pdsBaseElement.setAttribute, getAttribute000: pdsBaseElement.getAttribute, hasAttribute000: pdsBaseElement.hasAttribute, removeAttribute000: pdsBaseElement.removeAttribute, querySelector000: pdsBaseElement.querySelector }; pdsBaseElement.replaceChildren ? nPdsElement.replaceChildren000 = pdsBaseElement.replaceChildren : Element.prototype.replaceChildren000 = replaceChildrenPolyfill; defineProperties(Element.prototype, nPdsElement); } Element.prototype.setAttribute111 = function(p, v) { v = `${v}`; this.getAttribute000(p) !== v && this.setAttribute000(p, v); }; Element.prototype.incAttribute111 = function(p) { let v = +this.getAttribute000(p) || 0; v = v > 1e9 ? v + 1 : 9; this.setAttribute000(p, `${v}`); return v; }; Element.prototype.assignChildren111 = function(previousSiblings, node, nextSiblings) { let nodeList = []; for (let t = this.firstChild; t instanceof Node; t = t.nextSibling) { t !== node && nodeList.push(t); } inPageRearrange = !0; if (node.parentNode === this) { let fm = new DocumentFragment; nodeList.length > 0 && fm.replaceChildren000(...nodeList); if (previousSiblings && previousSiblings.length > 0) { fm.replaceChildren000(...previousSiblings); this.insertBefore000(fm, node); } if (nextSiblings && nextSiblings.length > 0) { fm.replaceChildren000(...nextSiblings); this.appendChild000(fm); } fm.replaceChildren000(); fm = null; } else { previousSiblings || (previousSiblings = []); nextSiblings || (nextSiblings = []); this.replaceChildren000(...previousSiblings, node, ...nextSiblings); } inPageRearrange = !1; if (nodeList.length > 0) { for (const t of nodeList) { t instanceof Element && !1 === t.isConnected && t.remove(); } } nodeList.length = 0; nodeList = null; }; let secondaryInnerHold = 0; const secondaryInnerFn = cb => { if (secondaryInnerHold) { secondaryInnerHold++; let err, r; try { r = cb(); } catch (e) { err = e; } secondaryInnerHold--; if (err) { throw err; } return r; } { const ea = qs("#secondary-inner"); const eb = qs("secondary-wrapper#secondary-inner-wrapper"); if (ea && eb) { secondaryInnerHold++; let err, r; ea.id = "secondary-inner-"; eb.id = "secondary-inner"; try { r = cb(); } catch (e) { err = e; } ea.id = "secondary-inner"; eb.id = "secondary-inner-wrapper"; secondaryInnerHold--; if (err) { throw err; } return r; } return cb(); } }; const DISABLE_FLAGS_SHADYDOM_FREE = !0; (() => { 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); } } let a = (async () => {})().constructor, i = e._ytConfigHacks = new n, l = () => { const t = e.ytcsi.originalYtcsi; t && (e.ytcsi = t, l = null); }, c = null, 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); } } } }, f = 1, 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 = !1; window._ytConfigHacks.add(config_ => { if (configOnce) { return; } configOnce = !0; const EXPERIMENT_FLAGS = config_.EXPERIMENT_FLAGS || 0; const EXPERIMENTS_FORCED_FLAGS = config_.EXPERIMENTS_FORCED_FLAGS || 0; for (const flags of [ EXPERIMENT_FLAGS, EXPERIMENTS_FORCED_FLAGS ]) { if (flags) { flags.web_watch_chat_hide_button_killswitch = !1; flags.web_watch_theater_chat = !1; flags.suppress_error_204_logging = !0; flags.kevlar_watch_grid = !1; if (DISABLE_FLAGS_SHADYDOM_FREE) { flags.enable_shadydom_free_scoped_node_methods = !1; flags.enable_shadydom_free_scoped_query_methods = !1; flags.enable_shadydom_free_scoped_readonly_properties_batch_one = !1; flags.enable_shadydom_free_parent_node = !1; flags.enable_shadydom_free_children = !1; flags.enable_shadydom_free_last_child = !1; } } } }); const mWeakRef = "function" == typeof WeakRef ? o => o ? new WeakRef(o) : null : o => o || null; const kRef = wr => wr && wr.deref ? wr.deref() : wr; const Promise = (async () => {})().constructor; const delayPn = delay => new Promise(fn => setTimeout(fn, delay)); const insp = o => o ? o.polymerController || o.inst || o || 0 : o || 0; const setTimeout_ = setTimeout.bind(window); const PromiseExternal = ((resolve_, reject_) => { const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject; }; return class PromiseExternal extends Promise { constructor(cb = h) { super(cb); if (cb === h) { this.resolve = resolve_; this.reject = reject_; } } }; })(); var nextBrowserTick = void 0 !== nextBrowserTick && nextBrowserTick.version >= 2 ? nextBrowserTick : (() => { "use strict"; const e = "undefined" != typeof globalThis ? globalThis : "undefined" != typeof window ? window : this; let t = !0; if (!(function n(s) { return s ? t = !1 : e.postMessage && !e.importScripts && e.addEventListener ? (e.addEventListener("message", n, !1), e.postMessage("$$$", "*"), e.removeEventListener("message", n, !1), t) : void 0; })()) { return void console.warn("Your browser environment cannot use nextBrowserTick"); } const n = (async () => {})().constructor; let s = null; const o = new Map, {floor: r, random: i} = Math; let l; do { l = `$$nextBrowserTick$$${(i() + 8).toString().slice(2)}$$`; } while (l in e); const a = l, c = a.length + 9; e[a] = 1; e.addEventListener("message", e => { if (0 !== o.size) { const t = (e || 0).data; if ("string" == typeof t && t.length === c && e.source === (e.target || 1)) { const e = o.get(t); e && ("p" === t[0] && (s = null), o.delete(t), e()); } } }, !1); const d = (t = o) => { if (t === o) { if (s) { return s; } let t; do { t = `p${a}${r(314159265359 * i() + 314159265359).toString(36)}`; } while (o.has(t)); return s = new n(e => { o.set(t, e); }), e.postMessage(t, "*"), t = null, s; } { let n; do { n = `f${a}${r(314159265359 * i() + 314159265359).toString(36)}`; } while (o.has(n)); o.set(n, t), e.postMessage(n, "*"); } }; return d.version = 2, d; })(); const isPassiveArgSupport = "function" == typeof IntersectionObserver; const capturePassive = !isPassiveArgSupport || { capture: !0, passive: !0 }; class Attributer { constructor(list) { this.list = list; this.flag = 0; } makeString() { let k = 1; let s = ""; let i = 0; for (;this.flag >= k; ) { this.flag & k && (s += this.list[i]); i++; k <<= 1; } return s; } } const mLoaded = new Attributer("icp"); const wrSelfMap = new WeakMap; const elements = new Proxy({ related: null, comments: null, infoExpander: null }, { get: (target, prop) => 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 !0; } }); 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; }; let pageType = null; function getWord(tag) { try { if ("undefined" != typeof window && window.YouTubePlusI18n) { const translation = window.YouTubePlusI18n.t(`tabs.${tag}`); if (translation && translation !== `tabs.${tag}`) { return translation; } } const fallbackWords = { info: "Info", videos: "Videos", playlist: "Playlist" }; return fallbackWords[tag] || tag; } catch (error) { console.warn("[YouTube+][Main] Translation error:", error); const englishWords = { info: "Info", videos: "Videos", playlist: "Playlist" }; return englishWords[tag] || tag; } } const svgComments = '<path d="M80 27H12A12 12 90 0 0 0 39v42a12 12 90 0 0 12 12h12v20a2 2 90 0 0 3.4 2L47 93h33a12 \n 12 90 0 0 12-12V39a12 12 90 0 0-12-12zM20 47h26a2 2 90 1 1 0 4H20a2 2 90 1 1 0-4zm52 28H20a2 2 90 1 1 0-4h52a2 2 90 \n 1 1 0 4zm0-12H20a2 2 90 1 1 0-4h52a2 2 90 1 1 0 4zm36-58H40a12 12 90 0 0-12 12v6h52c9 0 16 7 16 16v42h0v4l7 7a2 2 90 \n 0 0 3-1V71h2a12 12 90 0 0 12-12V17a12 12 90 0 0-12-12z"/>'.trim(); const svgVideos = '<path d="M89 10c0-4-3-7-7-7H7c-4 0-7 3-7 7v70c0 4 3 7 7 7h75c4 0 7-3 7-7V10zm-62 2h13v10H27V12zm-9 \n 66H9V68h9v10zm0-56H9V12h9v10zm22 56H27V68h13v10zm-3-25V36c0-2 2-3 4-2l12 8c2 1 2 4 0 5l-12 8c-2 1-4 0-4-2zm25 \n 25H49V68h13v10zm0-56H49V12h13v10zm18 56h-9V68h9v10zm0-56h-9V12h9v10z"/>'.trim(); const svgInfo = '<path d="M30 0C13.3 0 0 13.3 0 30s13.3 30 30 30 30-13.3 30-30S46.7 0 30 0zm6.2 46.6c-1.5.5-2.6 \n 1-3.6 1.3a10.9 10.9 0 0 1-3.3.5c-1.7 0-3.3-.5-4.3-1.4a4.68 4.68 0 0 1-1.6-3.6c0-.4.2-1 .2-1.5a20.9 20.9 90 0 1 \n .3-2l2-6.8c.1-.7.3-1.3.4-1.9a8.2 8.2 90 0 0 .3-1.6c0-.8-.3-1.4-.7-1.8s-1-.5-2-.5a4.53 4.53 0 0 0-1.6.3c-.5.2-1 \n .2-1.3.4l.6-2.1c1.2-.5 2.4-1 3.5-1.3s2.3-.6 3.3-.6c1.9 0 3.3.6 4.3 1.3s1.5 2.1 1.5 3.5c0 .3 0 .9-.1 1.6a10.4 10.4 \n 90 0 1-.4 2.2l-1.9 6.7c-.2.5-.2 1.1-.4 1.8s-.2 1.3-.2 1.6c0 .9.2 1.6.6 1.9s1.1.5 2.1.5a6.1 6.1 90 0 0 1.5-.3 9 9 90 \n 0 0 1.4-.4l-.6 2.2zm-3.8-35.2a1 1 0 010 8.6 1 1 0 010-8.6z"/>'.trim(); const svgPlayList = '<path d="M0 3h12v2H0zm0 4h12v2H0zm0 4h8v2H0zm16 0V7h-2v4h-4v2h4v4h2v-4h4v-2z"/>'.trim(); const svgElm = (w, h, vw, vh, p, m) => `<svg${m ? ` class=${m}` : ""} width="${w}" height="${h}" viewBox="0 0 ${vw} ${vh}" preserveAspectRatio="xMidYMid meet">${p}</svg>`; const hiddenTabsByUserCSS = 0; const langWords = { ar: !0, be: !0, bg: !0, cn: !0, de: !0, du: !0, en: !0, es: !0, fr: !0, hi: !0, id: !0, it: !0, jp: !0, kk: !0, kr: !0, ky: !0, pl: !0, pt: !0, ru: !0, tr: !0, tw: !0, uk: !0, uz: !0, vi: !0, ng: !0 }; function getLangForPage() { !(function getLang() { try { if (window.YouTubePlusI18n && "function" == typeof window.YouTubePlusI18n.getLanguage) { const detected = window.YouTubePlusI18n.getLanguage(); if (detected && langWords[detected]) { return detected; } } } catch {} const htmlLang = ((document || 0).documentElement || 0).lang || ""; return { de: "du", "de-de": "du", "de-at": "du", "de-ch": "du", fr: "fr", "fr-fr": "fr", "fr-ca": "fr", "fr-be": "fr", "fr-ch": "fr", "zh-hant": "tw", "zh-hant-hk": "tw", "zh-hant-tw": "tw", "zh-tw": "tw", "zh-hk": "tw", "zh-hans": "cn", "zh-hans-cn": "cn", "zh-cn": "cn", zh: "cn", "zh-sg": "cn", ja: "jp", "ja-jp": "jp", ko: "kr", "ko-kr": "kr", ru: "ru", "ru-ru": "ru", uk: "uk", "uk-ua": "uk", be: "be", "be-by": "be", bg: "bg", "bg-bg": "bg", es: "es", "es-es": "es", "es-419": "es", "es-mx": "es", pt: "pt", "pt-pt": "pt", "pt-br": "pt", it: "it", "it-it": "it", pl: "pl", "pl-pl": "pl", nl: "du", "nl-nl": "du", "nl-be": "du", ar: "ar", "ar-sa": "ar", "ar-ae": "ar", "ar-eg": "ar", hi: "hi", "hi-in": "hi", id: "id", "id-id": "id", ng: "ng", "en-ng": "ng", pcm: "ng", "pcm-ng": "ng", tr: "tr", "tr-tr": "tr", vi: "vi", "vi-vn": "vi", uz: "uz", "uz-uz": "uz", kk: "kk", "kk-kz": "kk", ky: "ky" }[htmlLang.toLowerCase()] || "en"; })(); } const _locks = {}; const lockGet = new Proxy(_locks, { get: (target, prop) => target[prop] || 0, set: () => !0 }); const lockSet = new Proxy(_locks, { get(target, prop) { target[prop] > 1e9 && (target[prop] = 9); return target[prop] = (target[prop] || 0) + 1; }, set: () => !0 }); const videosElementProvidedPromise = new PromiseExternal; const navigateFinishedPromise = new PromiseExternal; let isRightTabsInserted = !1; const rightTabsProvidedPromise = new PromiseExternal; const infoExpanderElementProvidedPromise = new PromiseExternal; const pluginsDetected = {}; let pluginDetectDebounceTimer = null; const pluginDetectObserver = new MutationObserver(mutations => { pluginDetectDebounceTimer || (pluginDetectDebounceTimer = setTimeout(() => { pluginDetectDebounceTimer = null; processPluginDetectMutations(mutations); }, 50)); }); const processPluginDetectMutations = mutations => { let changeOnRoot = !1; const newPlugins = []; const attributeChangedSet = new Set; for (const mutation of mutations) { mutation.target === document && (changeOnRoot = !0); let detected = ""; switch (mutation.attributeName) { case "data-ytlstm-new-layout": case "data-ytlstm-overlay-text-shadow": case "data-ytlstm-theater-mode": detected = "external.ytlstm"; attributeChangedSet.add(detected); } if (detected && !pluginsDetected[detected]) { pluginsDetected[detected] = !0; newPlugins.push(detected); } } elements.flexy && attributeChangedSet.has("external.ytlstm") && elements.flexy.setAttribute("tyt-external-ytlstm", qs("[data-ytlstm-theater-mode]") ? "1" : "0"); changeOnRoot && pluginDetectObserver.observe(document.body, { attributes: !0, attributeFilter: [ "data-ytlstm-new-layout", "data-ytlstm-overlay-text-shadow", "data-ytlstm-theater-mode" ] }); for (const detected of newPlugins) { const pluginItem = plugin[`${detected}`]; pluginItem ? pluginItem.activate() : console.warn(`No Plugin Activator for ${detected}`); } }; const pluginAttributeFilter = [ "data-ytlstm-new-layout", "data-ytlstm-overlay-text-shadow", "data-ytlstm-theater-mode" ]; pluginDetectObserver.observe(document.documentElement, { attributes: !0, attributeFilter: pluginAttributeFilter }); document.body && pluginDetectObserver.observe(document.body, { attributes: !0, attributeFilter: pluginAttributeFilter }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(pluginDetectObserver); YouTubeUtils?.ObserverRegistry?.track && YouTubeUtils.ObserverRegistry.track(); navigateFinishedPromise.then(() => { pluginDetectObserver.observe(document.documentElement, { attributes: !0, attributeFilter: pluginAttributeFilter }); document.body && pluginDetectObserver.observe(document.body, { attributes: !0, attributeFilter: pluginAttributeFilter }); }); const funcCanCollapse = function() { const content = this.content || this.$.content; this.canToggle = this.shouldUseNumberOfLines && (this.alwaysCollapsed || this.collapsed || !1 === this.isToggled) ? 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"); isChatCollapsed ? ytdFlexyElm.setAttribute111("tyt-chat-collapsed", "") : 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; let doAttributeChange = 0; playlistElm && ytdFlexyElm ? doAttributeChange = playlistElm.closest("[hidden]") || playlistElm.hasAttribute000("collapsed") ? 2 : 1 : ytdFlexyElm && (doAttributeChange = 2); 1 === doAttributeChange ? "" !== ytdFlexyElm.getAttribute000("tyt-playlist-expanded") && ytdFlexyElm.setAttribute111("tyt-playlist-expanded", "") : 2 === doAttributeChange && ytdFlexyElm.hasAttribute000("tyt-playlist-expanded") && ytdFlexyElm.removeAttribute000("tyt-playlist-expanded"); }; const aoChat = new MutationObserver(() => { Promise.resolve(lockSet.aoChatAttrAsyncLock).then(aoChatAttrChangeFn).catch(console.warn); }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(aoChat); YouTubeUtils?.ObserverRegistry?.track && YouTubeUtils.ObserverRegistry.track(); const aoPlayList = new MutationObserver(() => { Promise.resolve(lockSet.aoPlayListAttrAsyncLock).then(aoPlayListAttrChangeFn).catch(console.warn); }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(aoPlayList); YouTubeUtils?.ObserverRegistry?.track && YouTubeUtils.ObserverRegistry.track(); let aoCommentThrottleTimer = null; let aoCommentPendingMutations = []; const aoComment = new MutationObserver(async mutations => { aoCommentPendingMutations.push(...mutations); aoCommentThrottleTimer || (aoCommentThrottleTimer = setTimeout(() => { aoCommentThrottleTimer = null; const allMutations = aoCommentPendingMutations; aoCommentPendingMutations = []; processCommentMutations(allMutations); }, 50)); }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(aoComment); YouTubeUtils?.ObserverRegistry?.track && YouTubeUtils.ObserverRegistry.track(); const processCommentMutations = async mutations => { const commentsArea = elements.comments; const ytdFlexyElm = elements.flexy; if (!commentsArea) { return; } let bfHidden = !1; let bfCommentsVideoId = !1; let bfCommentDisabled = !1; for (const mutation of mutations) { "hidden" === mutation.attributeName && mutation.target === commentsArea ? bfHidden = !0 : "tyt-comments-video-id" === mutation.attributeName && mutation.target === commentsArea ? bfCommentsVideoId = !0 : "tyt-comments-data-status" === mutation.attributeName && mutation.target === commentsArea && (bfCommentDisabled = !0); } if (bfHidden) { commentsArea.hasAttribute000("hidden") || Promise.resolve(commentsArea).then(eventMap.settingCommentsVideoId).catch(console.warn); Promise.resolve(lockSet.removeKeepCommentsScrollerLock).then(removeKeepCommentsScroller).catch(console.warn); } if ((bfHidden || bfCommentsVideoId || bfCommentDisabled) && ytdFlexyElm) { const commentsDataStatus = +commentsArea.getAttribute000("tyt-comments-data-status"); 2 === commentsDataStatus ? ytdFlexyElm.setAttribute111("tyt-comment-disabled", "") : 1 === commentsDataStatus && ytdFlexyElm.removeAttribute000("tyt-comment-disabled"); Promise.resolve(lockSet.checkCommentsShouldBeHiddenLock).then(eventMap.checkCommentsShouldBeHidden).catch(console.warn); const lockId = lockSet.rightTabReadyLock01; await rightTabsProvidedPromise.then(); if (lockGet.rightTabReadyLock01 !== lockId) { return; } if (elements.comments !== commentsArea) { return; } if (!1 === commentsArea.isConnected) { return; } if (commentsArea.closest("#tab-comments")) { const shouldTabVisible = !commentsArea.closest("[hidden]"); document.querySelector('[tyt-tab-content="#tab-comments"]').classList.toggle("tab-btn-hidden", !shouldTabVisible); } } }; const ioComment = new IntersectionObserver(entries => { requestAnimationFrame(() => { for (const entry of entries) { const target = entry.target; const cnt = insp(target); if (entry.isIntersecting && target instanceof HTMLElement_ && "function" == typeof cnt.calculateCanCollapse) { cnt.calculateCanCollapse(!0); target.setAttribute111("io-intersected", ""); const ytdFlexyElm = elements.flexy; ytdFlexyElm && !ytdFlexyElm.hasAttribute000("keep-comments-scroller") && ytdFlexyElm.setAttribute111("keep-comments-scroller", ""); } else { target.hasAttribute000("io-intersected") && target.removeAttribute000("io-intersected"); } } }); }, { threshold: [ 0 ], rootMargin: "100px" }); let bFixForResizedTabLater = !1; let lastRoRightTabsWidth = 0; let resizeDebounceTimer = null; const roRightTabs = new ResizeObserver(entries => { resizeDebounceTimer || (resizeDebounceTimer = setTimeout(() => { resizeDebounceTimer = null; const entry = entries[entries.length - 1]; const width = Math.round(entry.borderBoxSize.inlineSize); if (lastRoRightTabsWidth !== width) { lastRoRightTabsWidth = width; if (2 & ~tabAStatus) { bFixForResizedTabLater = !0; } else { bFixForResizedTabLater = !1; Promise.resolve(1).then(eventMap.fixForTabDisplay); } } }, 100)); }); let cachedTabLinks = null; const cachedTabContents = new Map; const switchToTab = activeLink => { "string" == typeof activeLink && (activeLink = qs(`a[tyt-tab-content="${activeLink}"]`) || null); const ytdFlexyElm = elements.flexy; if (!cachedTabLinks || 0 === cachedTabLinks.length || !cachedTabLinks[0].isConnected) { cachedTabLinks = qsAll("#material-tabs a[tyt-tab-content]"); cachedTabContents.clear(); } const links = cachedTabLinks; for (const link of links) { let content = cachedTabContents.get(link); if (!content || !content.isConnected) { content = qs(link.getAttribute000("tyt-tab-content")); content && cachedTabContents.set(link, content); } if (link && content) { if (link !== activeLink) { link.classList.remove("active"); link.setAttribute("aria-selected", "false"); content.classList.add("tab-content-hidden"); content.hasAttribute000("tyt-hidden") || content.setAttribute111("tyt-hidden", ""); } else { link.classList.add("active"); link.setAttribute("aria-selected", "true"); content.hasAttribute000("tyt-hidden") && content.removeAttribute000("tyt-hidden"); content.classList.remove("tab-content-hidden"); } } } const switchingTo = activeLink ? activeLink.getAttribute000("tyt-tab-content") : ""; switchingTo && (lastTab = lastPanel = switchingTo); "" === ytdFlexyElm.getAttribute000("tyt-chat") && ytdFlexyElm.removeAttribute000("tyt-chat"); ytdFlexyElm.setAttribute111("tyt-tab", switchingTo); if (switchingTo) { bFixForResizedTabLater = !1; Promise.resolve(0).then(eventMap.fixForTabDisplay); } }; let tabAStatus = 0; const calculationFn = (r = 0, flag) => { const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) { return r; } if (1 & flag) { r |= 1; ytdFlexyElm.hasAttribute000("theater") || (r -= 1); } if (2 & flag) { r |= 2; ytdFlexyElm.getAttribute000("tyt-tab") || (r -= 2); } if (4 & flag) { r |= 4; "-" !== ytdFlexyElm.getAttribute000("tyt-chat") && (r -= 4); } if (8 & flag) { r |= 8; "+" !== ytdFlexyElm.getAttribute000("tyt-chat") && (r -= 8); } if (16 & flag) { r |= 16; ytdFlexyElm.hasAttribute000("is-two-columns_") || (r -= 16); } if (32 & flag) { r |= 32; ytdFlexyElm.hasAttribute000("tyt-egm-panel_") || (r -= 32); } if (64 & flag) { r |= 64; document.fullscreenElement || (r -= 64); } if (128 & flag) { r |= 128; ytdFlexyElm.hasAttribute000("tyt-playlist-expanded") || (r -= 128); } if (4096 & flag) { r |= 4096; "1" !== ytdFlexyElm.getAttribute("tyt-external-ytlstm") && (r -= 4096); } return r; }; function isTheater() { const ytdFlexyElm = elements.flexy; return ytdFlexyElm && ytdFlexyElm.hasAttribute000("theater"); } function isZenTheaterOverlayActive() { try { const raw = localStorage.getItem(window.YouTubeUtils?.SETTINGS_KEY || "youtube_plus_settings"); if (!raw) { return !0; } const s = JSON.parse(raw); return !1 !== s?.enableZenStyles && !1 !== s?.zenStyles?.theaterEnhancements; } catch { return !0; } } function ytBtnCancelTheater() { if (!isZenTheaterOverlayActive() && isTheater()) { const sizeBtn = qs("ytd-watch-flexy #ytd-player button.ytp-size-button"); sizeBtn && sizeBtn.click(); } } function getSuitableElement(selector) { const elements = qsAll(selector); let j = -1, h = -1; for (let i = 0, l = elements.length; i < l; i++) { const d = elements[i].getElementsByTagName("*").length; if (d > h) { h = d; j = i; } } return j >= 0 ? elements[j] : null; } function ytBtnExpandChat() { const dom = getSuitableElement("ytd-live-chat-frame#chat"); const cnt = insp(dom); if (cnt && "boolean" == typeof cnt.collapsed) { if ("function" == typeof cnt.setCollapsedState) { cnt.setCollapsedState({ setLiveChatCollapsedStateAction: { collapsed: !1 } }); if (!1 === cnt.collapsed) { return; } } cnt.collapsed = !1; if (!1 === cnt.collapsed) { return; } if (!0 === cnt.isHiddenByUser && !0 === cnt.collapsed) { cnt.isHiddenByUser = !1; cnt.collapsed = !1; } } let button = qs("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 && button.click(); } } function ytBtnCollapseChat() { if (isZenTheaterOverlayActive() && isTheater()) { return; } const dom = getSuitableElement("ytd-live-chat-frame#chat"); const cnt = insp(dom); if (cnt && "boolean" == typeof cnt.collapsed) { if ("function" == typeof cnt.setCollapsedState) { cnt.setCollapsedState({ setLiveChatCollapsedStateAction: { collapsed: !0 } }); if (!0 === cnt.collapsed) { return; } } cnt.collapsed = !0; if (!0 === cnt.collapsed) { return; } if (!1 === cnt.isHiddenByUser && !1 === cnt.collapsed) { cnt.isHiddenByUser = !0; cnt.collapsed = !0; } } let button = qs("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 && button.click(); } } function ytBtnEgmPanelCore(arr) { if (!arr) { return; } "length" in arr || (arr = [ arr ]); const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) { return; } let actions = []; for (const entry of arr) { if (!entry) { continue; } const panelId = entry.panelId; const toHide = entry.toHide; const toShow = entry.toShow; !0 !== toHide || toShow ? !0 !== toShow || toHide || actions.push({ showEngagementPanelEndpoint: { panelIdentifier: panelId } }) : actions.push({ changeEngagementPanelVisibilityAction: { targetId: panelId, visibility: "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN" } }); if (actions.length > 0) { const cnt = insp(ytdFlexyElm); cnt.resolveCommand({ signalServiceEndpoint: { signal: "CLIENT_SIGNAL", actions } }, {}, !1); } actions = null; } } function ytBtnCloseEngagementPanels() { const actions = []; for (const panelElm of qsAll("ytd-watch-flexy[tyt-tab] #panels.ytd-watch-flexy ytd-engagement-panel-section-list-renderer[target-id][visibility]:not([hidden])")) { "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED" !== panelElm.getAttribute("visibility") || panelElm.closest("[hidden]") || actions.push({ panelId: panelElm.getAttribute000("target-id"), toHide: !0 }); } ytBtnEgmPanelCore(actions); } function ytBtnClosePlaylist() { const cnt = insp(elements.playlist); cnt && "boolean" == typeof cnt.collapsed && (cnt.collapsed = !0); } const updateChatLocation498 = function() { "ytd-watch-grid" !== this.is && secondaryInnerFn(() => { this.updatePageMediaQueries(); this.schedulePlayerSizeUpdate_(); }); }; const mirrorNodeWS = new WeakMap; const dummyNode = document.createElement("noscript"); const __j4836__ = Symbol(); const __j5744__ = Symbol(); const __j5733__ = Symbol(); const monitorDataChangedByDOMMutation = async function() { const node = kRef(this); if (!node) { return; } const cnt = insp(node); const __lastChanged__ = cnt[__j5733__]; const val = cnt.data ? cnt.data[__j4836__] || 1 : 0; if (__lastChanged__ !== val) { cnt[__j5733__] = val > 0 ? cnt.data[__j4836__] = Date.now() : 0; await Promise.resolve(); attributeInc(node, "tyt-data-change-counter"); } }; const moChangeReflection = function(mutations) { const node = kRef(this); 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 = !1; for (const mutation of mutations) { ("tyt-clone-refresh-count" === mutation.attributeName && mutation.target === originElement || "tyt-data-change-counter" === mutation.attributeName && mutation.target === originElement) && (bfDataChangeCounter = !0); } if (bfDataChangeCounter && oriCnt.data) { node.replaceWith(dummyNode); cnt.data = Object.assign({}, oriCnt.data); dummyNode.replaceWith(node); } } }; const attributeInc = (elm, prop) => { let v = (+elm.getAttribute000(prop) || 0) + 1; v > 1e9 && (v = 9); elm.setAttribute000(prop, v); return v; }; const isChannelId = x => "string" == typeof x && 24 === x.length && /UC[-_a-zA-Z0-9+=.]{22}/.test(x); const infoFix = lockId => { if (null !== lockId && lockGet.infoFixLock !== lockId) { return; } const infoExpander = elements.infoExpander; const infoContainer = (infoExpander ? infoExpander.parentNode : null) || qs("#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 = [ ...qsAll('ytd-watch-metadata.ytd-watch-flexy div[slot="extra-content"] > *, ytd-watch-metadata.ytd-watch-flexy #extra-content > *') ].filter(elm => "string" == typeof elm.is).map(elm => { const is = elm.is; for (;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 && "string" == typeof elm.is); const source = requireElements.map(entry => { const inst = insp(entry); return { data: inst.data, tag: inst.is, elm: entry }; }); let noscript_ = qs("noscript#aythl"); if (!noscript_) { noscript_ = document.createElement("noscript"); noscript_.id = "aythl"; inPageRearrange = !0; ytdFlexyElm.insertBefore000(noscript_, ytdFlexyElm.firstChild); inPageRearrange = !1; } const noscript = noscript_; let requiredUpdate = !1; const mirrorElmSet = new Set; const targetParent = infoContainer; for (const {data, tag, elm: s} of source) { let mirrorNode = mirrorNodeWS.get(s); mirrorNode = mirrorNode ? kRef(mirrorNode) : mirrorNode; if (mirrorNode) { mirrorNode.parentNode !== targetParent && (requiredUpdate = !0); } else { 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: !0, attributeFilter: [ "tyt-clone-refresh-count", "tyt-data-change-counter" ] }); s.jy8432 = 1; if (cProto instanceof Node || cProto._dataChanged496 || "function" != typeof cProto._createPropertyObserver) { if (!(cProto instanceof Node) && !cProto._dataChanged496 && !0 === cProto.useSignals && insp(s).signalProxy) { const dataSignal = cnt?.signalProxy?.signalCache?.data; if (dataSignal && "function" == typeof dataSignal.setWithPath && !dataSignal.setWithPath573 && !dataSignal.controller573) { dataSignal.controller573 = mWeakRef(cnt); dataSignal.setWithPath573 = dataSignal.setWithPath; dataSignal.setWithPath = function() { const cnt = kRef(this.controller573 || null) || null; cnt && "function" == typeof cnt._dataChanged496k && Promise.resolve(cnt).then(cnt._dataChanged496k).catch(console.warn); return this.setWithPath573(...arguments); }; cProto._dataChanged496 = function() { const node = this.hostElement || this; node.jy8432 && attributeInc(node, "tyt-data-change-counter"); }; cProto._dataChanged496k = cnt => cnt._dataChanged496(); } } } else { cProto._dataChanged496 = function() { const node = this.hostElement || this; node.jy8432 && attributeInc(node, "tyt-data-change-counter"); }; cProto._createPropertyObserver("data", "_dataChanged496", void 0); } cProto._dataChanged496 || new MutationObserver(monitorDataChangedByDOMMutation.bind(mirrorNode[__j5744__])).observe(s, { attributes: !0, childList: !0, subtree: !0 }); mirrorNodeWS.set(s, nodeWR); requiredUpdate = !0; } if (!requiredUpdate) { const cloneNodeCnt = insp(mirrorNode); cloneNodeCnt.data !== data && (requiredUpdate = !0); } mirrorElmSet.add(mirrorNode); source.mirrored = mirrorNode; } const mirroElmArr = [ ...mirrorElmSet ]; mirrorElmSet.clear(); if (!requiredUpdate) { let e = infoExpander ? -1 : 0; for (let n = targetParent.firstChild; n instanceof Node; n = n.nextSibling) { const target = e < 0 ? infoExpander : mirroElmArr[e]; e++; if (n !== target) { requiredUpdate = !0; break; } } requiredUpdate || e === mirroElmArr.length + 1 || (requiredUpdate = !0); } if (requiredUpdate) { infoExpander ? targetParent.assignChildren111(null, infoExpander, mirroElmArr) : targetParent.replaceChildren000(...mirroElmArr); for (const mirrorElm of mirroElmArr) { const j = attributeInc(mirrorElm, "tyt-clone-refresh-count"); const oriElm = kRef(mirrorElm[__j5744__] || null) || null; oriElm && oriElm.setAttribute111("tyt-clone-refresh-count", j); } } mirroElmArr.length = 0; source.length = 0; }; const layoutFix = lockId => { if (lockGet.layoutFixLock !== lockId) { return; } const secondaryWrapper = qs("#secondary-inner.style-scope.ytd-watch-flexy > secondary-wrapper"); if (secondaryWrapper) { const secondaryInner = secondaryWrapper.parentNode; const chatContainer = qs("#columns.style-scope.ytd-watch-flexy [tyt-chat-container]"); const hasExtraNodes = () => { for (let node = secondaryInner.firstChild; node; node = node.nextSibling) { if (node !== secondaryWrapper && node !== chatContainer && (3 !== node.nodeType || node.textContent.trim())) { return !0; } } return !1; }; if (hasExtraNodes() || chatContainer && !chatContainer.closest("secondary-wrapper")) { const w = []; const w2 = []; for (let node = secondaryInner.firstChild; node instanceof Node; node = node.nextSibling) { if (node === chatContainer && chatContainer) {} else if (node === secondaryWrapper) { for (let node2 = secondaryWrapper.firstChild; node2 instanceof Node; node2 = node2.nextSibling) { if (node2 === chatContainer && chatContainer) {} else { "right-tabs" === node2.id && chatContainer && w2.push(chatContainer); w2.push(node2); } } } else { w.push(node); } } inPageRearrange = !0; secondaryWrapper.replaceChildren000(...w, ...w2); inPageRearrange = !1; const chatElm = elements.chat; const chatCnt = insp(chatElm); chatCnt && "function" == typeof chatCnt.urlChanged && secondaryWrapper.contains(chatElm) && ("function" == typeof chatCnt.urlChangedAsync12 ? chatCnt.urlChanged() : setTimeout(() => chatCnt.urlChanged(), 136)); } } }; let lastPanel = ""; let lastTab = ""; let egmPanelsDebounceTimer = null; const aoEgmPanels = new MutationObserver(() => { egmPanelsDebounceTimer || (egmPanelsDebounceTimer = setTimeout(() => { egmPanelsDebounceTimer = null; Promise.resolve(lockSet.updateEgmPanelsLock).then(updateEgmPanels).catch(console.warn); }, 16)); }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(aoEgmPanels); YouTubeUtils?.ObserverRegistry?.track && YouTubeUtils.ObserverRegistry.track(); const removeKeepCommentsScroller = async lockId => { if (lockGet.removeKeepCommentsScrollerLock !== lockId) { return; } await Promise.resolve(); if (lockGet.removeKeepCommentsScrollerLock !== lockId) { return; } const ytdFlexyFlm = elements.flexy; ytdFlexyFlm && ytdFlexyFlm.removeAttribute000("keep-comments-scroller"); }; const egmPanelsCache = new Set; const updateEgmPanels = async lockId => { if (lockId !== lockGet.updateEgmPanelsLock) { return; } await navigateFinishedPromise.then().catch(console.warn); if (lockId !== lockGet.updateEgmPanelsLock) { return; } const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) { return; } let newVisiblePanels = []; let newHiddenPanels = []; let allVisiblePanels = []; const panels = egmPanelsCache; for (const panelElm of panels) { if (!panelElm.isConnected) { egmPanelsCache.delete(panelElm); continue; } const visibility = panelElm.getAttribute000("visibility"); if ("ENGAGEMENT_PANEL_VISIBILITY_HIDDEN" === visibility || panelElm.closest("[hidden]")) { if (panelElm.hasAttribute000("tyt-visible-at")) { panelElm.removeAttribute000("tyt-visible-at"); newHiddenPanels.push(panelElm); } } else if ("ENGAGEMENT_PANEL_VISIBILITY_EXPANDED" === visibility && !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) { panelElm !== targetVisible && actions.push({ panelId: panelElm.getAttribute000("target-id"), toHide: !0 }); } actions.length >= 1 && ytBtnEgmPanelCore(actions); } allVisiblePanels.length >= 1 ? ytdFlexyElm.setAttribute111("tyt-egm-panel_", "") : ytdFlexyElm.removeAttribute000("tyt-egm-panel_"); newVisiblePanels.length = 0; newVisiblePanels = null; newHiddenPanels.length = 0; newHiddenPanels = null; allVisiblePanels.length = 0; allVisiblePanels = null; }; const checkElementExist = (css, exclude) => { const elms = window.YouTubeDOMCache ? window.YouTubeDOMCache.querySelectorAll(css, document) : qsAll(css); for (const p of elms) { if (!p.closest(exclude)) { return p; } } return null; }; let fixInitialTabStateK = 0; const {handleNavigateFactory} = (() => { let isLoadStartListened = !1; function findLcComment(lc) { if (1 === arguments.length) { const element = qs(`#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 (0 === arguments.length) { const element = qs("#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); m && (lc = m[1]); } } } if (commentRendererElm && lc) { return { lc, commentRendererElm }; } } } return null; } 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 1 === done; } const loadStartFx = async evt => { const media = (evt || 0).target || 0; if ("VIDEO" !== media.nodeName && "AUDIO" !== media.nodeName) { return; } const newMedia = media; const media1 = common.getMediaElement(0); const media2 = common.getMediaElements(2); if (null !== media1 && media2.length > 0) { if (newMedia !== media1 && !1 === media1.paused) { isVideoPlaying(media1) && Promise.resolve(newMedia).then(video => !1 === video.paused && video.pause()).catch(console.warn); } else if (newMedia === media1) { for (const s of media2) { if (!1 === s.paused) { Promise.resolve(s).then(s => !1 === s.paused && s.pause()).catch(console.warn); break; } } } else { Promise.resolve(media1).then(video1 => !1 === video1.paused && video1.pause()).catch(console.warn); } } }; const getBrowsableEndPoint = req => { let valid = !1; 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 ("string" == typeof videoId && "string" == typeof url && url.indexOf("lc=") > 0) { const m = /^\/watch\?v=([\w_-]+)&lc=([\w_.-]+)$/.exec(url); if (m && m[1] === videoId) { const targetLc = findLcComment(m[2]); const currentLc = targetLc ? findLcComment() : null; if (targetLc && currentLc) { const done = targetLc.lc === currentLc.lc || (function lcSwapFuncA(targetLcId, currentLcId) { let done = 0; try { const r1 = findLcComment(currentLcId).commentRendererElm; const r2 = findLcComment(targetLcId).commentRendererElm; if ("object" == typeof insp(r1).data.linkedCommentBadge && void 0 === insp(r2).data.linkedCommentBadge) { const p = Object.assign({}, insp(r1).data.linkedCommentBadge); ((p || 0).metadataBadgeRenderer || 0).trackingParams && delete p.metadataBadgeRenderer.trackingParams; const v1 = findContentsRenderer(r1); const v2 = findContentsRenderer(r2); if (v1.parent !== v2.parent || "YTD-COMMENTS" !== v2.parent.nodeName && "YTD-ITEM-SECTION-RENDERER" !== v2.parent.nodeName) { return !1; } if (v2.index >= 0) { if ("YTD-COMMENT-REPLIES-RENDERER" === v2.parent.nodeName) { lcSwapFuncB(targetLcId, currentLcId, p) && (done = 1); done = 1; } else { const v2pCnt = insp(v2.parent); const v2Conents = (v2pCnt.data || 0).contents || 0; 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)) }); lcSwapFuncB(targetLcId, currentLcId, p) && (done = 1); } } } } catch (e) { console.warn(e); } return 1 === done; })(targetLc.lc, currentLc.lc) ? 1 : 0; if (1 === done) { common.xReplaceState(history.state, url); return; } } } } } if (endpoint && (endpoint.commandMetadata || 0).webCommandMetadata && endpoint.browseEndpoint && isChannelId(endpoint.browseEndpoint.browseId)) { valid = !0; } else if (endpoint && (endpoint.browseEndpoint || endpoint.searchEndpoint) && !endpoint.urlEndpoint && !endpoint.watchEndpoint) { if (endpoint.browseEndpoint && "FEwhat_to_watch" === endpoint.browseEndpoint.browseId) { const playerMedia = common.getMediaElement(1); playerMedia && !1 === playerMedia.paused && (valid = !0); } else if (endpoint.commandMetadata && endpoint.commandMetadata.webCommandMetadata) { const meta = endpoint.commandMetadata.webCommandMetadata; meta && meta.url && meta.webPageType && (valid = !0); } } valid || (endpoint = null); return endpoint; }; const shouldUseMiniPlayer = () => { const isSubTypeExist = qs("ytd-page-manager#page-manager > ytd-browse[page-subtype]"); if (isSubTypeExist) { return !0; } const movie_player = qsAll("#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 && !1 === media.paused) { return !0; } } return !1; }; let u38 = 0; return { handleNavigateFactory: handleNavigate => function(req) { u38 > 1e9 && (u38 = 9); const t38 = ++u38; const $arguments = arguments; let endpoint = null; (req => { const command = req ? req.command : null; if (command) { if (command && (command.commandMetadata || 0).webCommandMetadata && command.watchEndpoint) {} else if (command && (command.commandMetadata || 0).webCommandMetadata && command.browseEndpoint && isChannelId(command.browseEndpoint.browseId)) {} else if (!command || !command.browseEndpoint && !command.searchEndpoint || command.urlEndpoint || command.watchEndpoint) { return !1; } return !!shouldUseMiniPlayer() && "watch" === pageType; } })(req) && (endpoint = getBrowsableEndPoint(req)); if (!endpoint || !shouldUseMiniPlayer()) { return handleNavigate.apply(this, $arguments); } const ytdAppElm = qs("ytd-app"); const ytdAppCnt = insp(ytdAppElm); let object = null; try { object = ytdAppCnt.data.response.currentVideoEndpoint.watchEndpoint || null; } catch { object = null; } "object" != typeof object && (object = null); const once = { once: !0 }; if (null !== object && !("playlistId" in object)) { let wObject = mWeakRef(object); const N = 3; let count = 0; Object.defineProperty(kRef(wObject) || {}, "playlistId", { get() { count++; count === N && delete this.playlistId; return "*"; }, set(value) { delete this.playlistId; this.playlistId = value; }, enumerable: !1, configurable: !0 }); let playlistClearout = null; let timeoutid = 0; Promise.race([ new Promise(r => { timeoutid = setTimeout(r, 4e3); }), new Promise(r => { playlistClearout = () => { if (timeoutid > 0) { clearTimeout(timeoutid); timeoutid = 0; } r(); }; document.addEventListener("yt-page-type-changed", playlistClearout, once); }) ]).then(() => { if (0 !== timeoutid) { 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 = !0; YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "loadstart", loadStartFx, !0) : document.addEventListener("loadstart", loadStartFx, !0); } const endpointURL = `${endpoint?.commandMetadata?.webCommandMetadata?.url || ""}`; endpointURL && endpointURL.endsWith("/about") && /\/channel\/UC[-_a-zA-Z0-9+=.]{22}\/about/.test(endpointURL) && (async t38 => { let promise = new PromiseExternal; const f = () => { promise && promise.resolve(); promise = null; }; document.addEventListener("yt-navigate-finish", f, !1); await promise.then(); promise = null; document.removeEventListener("yt-navigate-finish", f, !1); t38 === u38 && setTimeout(() => { const currentAbout = qsAll("ytd-about-channel-renderer").filter(e => !e.closest("[hidden]"))[0]; let okay = !1; if (currentAbout) { const popupContainer = currentAbout.closest("ytd-popup-container"); if (popupContainer) { const cnt = insp(popupContainer); let arr = null; try { arr = cnt.handleGetOpenedPopupsAction_(); } catch {} arr && 0 === arr.length && (okay = !0); } else { okay = !1; } } else { okay = !0; } if (okay) { const descriptionModel = [ ...qsAll("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]; button && button.click(); } } }, 80); })(t38); handleNavigate.apply(this, $arguments); } }; })(); const common = (() => { let mediaModeLock = 0; const _getMediaElement = i => { if (0 === mediaModeLock) { const e = qs(".video-stream.html5-main-video") || qs("#movie_player video, #movie_player audio") || qs("body video[src], body audio[src]"); e && ("VIDEO" === e.nodeName ? mediaModeLock = 1 : "AUDIO" === e.nodeName && (mediaModeLock = 2)); } if (!mediaModeLock) { return null; } if (1 === mediaModeLock) { switch (i) { case 1: return "ytd-player#ytd-player video[src]"; case 2: return 'ytd-browse[role="main"] video[src]'; default: return "#movie_player video[src]"; } } else if (2 === mediaModeLock) { 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]'; default: return "#movie_player audio.video-stream.html5-main-video[src]"; } } return null; }; return { xReplaceState(s, u) { try { history.replaceState(s, "", u); } catch {} if (s.endpoint) { try { const ytdAppElm = qs("ytd-app"); const ytdAppCnt = insp(ytdAppElm); ytdAppCnt.replaceState(s.endpoint, "", u); } catch {} } }, getMediaElement(i) { const s = _getMediaElement(i) || ""; return s ? qs(s) : null; }, getMediaElements(i) { const s = _getMediaElement(i) || ""; return s ? qsAll(s) : []; } }; })(); let inPageRearrange = !1; let tmpLastVideoId = ""; const getCurrentVideoId = () => { const ytdFlexyElm = elements.flexy; const ytdFlexyCnt = insp(ytdFlexyElm); if (ytdFlexyCnt && "string" == typeof ytdFlexyCnt.videoId && ytdFlexyCnt.videoId) { return ytdFlexyCnt.videoId; } if (ytdFlexyElm && "string" == typeof ytdFlexyElm.videoId && ytdFlexyElm.videoId) { return ytdFlexyElm.videoId; } try { const params = new URLSearchParams(location.search); const v = params.get("v"); if (v && /^[\w-]{11}$/.test(v)) { return v; } } catch {} return ""; }; const fixInlineExpanderDisplay = inlineExpanderCnt => { try { inlineExpanderCnt.updateIsAttributedExpanded(); } catch (e) {} try { inlineExpanderCnt.updateIsFormattedExpanded(); } catch (e) {} try { inlineExpanderCnt.updateTextOnSnippetTypeChange(); } catch (e) {} try { inlineExpanderCnt.updateStyles(); } catch (e) {} }; const setExpand = cnt => { if ("function" == typeof cnt.set) { cnt.set("isExpanded", !0); "function" == typeof cnt.isExpandedChanged && cnt.isExpandedChanged(); } else if (!1 === cnt.isExpanded) { cnt.isExpanded = !0; "function" == typeof cnt.isExpandedChanged && cnt.isExpandedChanged(); } }; const cloneMethods = { updateTextOnSnippetTypeChange() { !1 === this.isResetMutation && (this.isResetMutation = !0); !0 === this.isExpanded && (this.isExpanded = !1); setExpand(this, !0); !1 === this.isResetMutation && (this.isResetMutation = !0); }, collapse() {}, computeExpandButtonOffset: () => 0, dataChanged() {} }; const fixInlineExpanderMethods = inlineExpanderCnt => { if (inlineExpanderCnt && !inlineExpanderCnt.__$$idncjk8487$$__) { inlineExpanderCnt.__$$idncjk8487$$__ = !0; inlineExpanderCnt.dataChanged = cloneMethods.dataChanged; inlineExpanderCnt.updateTextOnSnippetTypeChange = cloneMethods.updateTextOnSnippetTypeChange; "function" == typeof inlineExpanderCnt.collapse && (inlineExpanderCnt.collapse = cloneMethods.collapse); "function" == typeof inlineExpanderCnt.computeExpandButtonOffset && (inlineExpanderCnt.computeExpandButtonOffset = cloneMethods.computeExpandButtonOffset); "boolean" == typeof inlineExpanderCnt.isResetMutation && (inlineExpanderCnt.isResetMutation = !0); "string" == typeof inlineExpanderCnt.collapseLabel && (inlineExpanderCnt.collapseLabel = ""); fixInlineExpanderDisplay(inlineExpanderCnt); } }; const fixInlineExpanderContent = () => { const mainInfo = getMainInfo(); if (!mainInfo) { return; } const inlineExpanderElm = mainInfo.querySelector("ytd-text-inline-expander"); const inlineExpanderCnt = insp(inlineExpanderElm); fixInlineExpanderMethods(inlineExpanderCnt); }; const plugin = { minibrowser: { activated: !1, toUse: !0, activate() { if (this.activated) { return; } const isPassiveArgSupport = "function" == typeof IntersectionObserver; if (!isPassiveArgSupport) { return; } this.activated = !0; const ytdAppElm = qs("ytd-app"); const ytdAppCnt = insp(ytdAppElm); if (!ytdAppCnt) { return; } const cProto = ytdAppCnt.constructor.prototype; if (cProto.handleNavigate && !cProto.handleNavigate.__ma355__) { cProto.handleNavigate = handleNavigateFactory(cProto.handleNavigate); cProto.handleNavigate.__ma355__ = 1; } } }, autoExpandInfoDesc: { activated: !1, toUse: !1, mo: null, promiseReady: new PromiseExternal, moFn(lockId) { if (lockGet.autoExpandInfoDescAttrAsyncLock !== lockId) { return; } const mainInfo = getMainInfo(); if (mainInfo) { switch (((mainInfo || 0).nodeName || "").toLowerCase()) { case "ytd-expander": if (mainInfo.hasAttribute000("collapsed")) { let success = !1; try { insp(mainInfo).handleMoreTap(new Event("tap")); success = !0; } catch {} 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); inlineExpanderCnt && !1 === inlineExpanderCnt.isExpanded && setExpand(inlineExpanderCnt, !0); } } }, activate() { if (!this.activated) { this.moFn = this.moFn.bind(this); this.mo = new MutationObserver(() => { Promise.resolve(lockSet.autoExpandInfoDescAttrAsyncLock).then(this.moFn).catch(console.warn); }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(this.mo); this.activated = !0; this.promiseReady.resolve(); } }, async onMainInfoSet(mainInfo) { await this.promiseReady.then(); "ytd-expander" === mainInfo.nodeName.toLowerCase() ? this.mo.observe(mainInfo, { attributes: !0, attributeFilter: [ "collapsed", "attr-8ifv7" ] }) : this.mo.observe(mainInfo, { attributes: !0, attributeFilter: [ "attr-8ifv7" ] }); mainInfo.incAttribute111("attr-8ifv7"); } }, fullChannelNameOnHover: { activated: !1, toUse: !0, mo: null, ro: null, promiseReady: new PromiseExternal, checkResize: 0, mouseEnterFn(evt) { const target = evt ? evt.target : null; if (!(target instanceof HTMLElement_)) { return; } const metaDataElm = target.closest("ytd-watch-metadata"); metaDataElm.classList.remove("tyt-metadata-hover-resized"); this.checkResize = Date.now() + 300; metaDataElm.classList.add("tyt-metadata-hover"); }, mouseLeaveFn(evt) { const target = evt ? evt.target : null; if (!(target instanceof HTMLElement_)) { return; } const metaDataElm = target.closest("ytd-watch-metadata"); metaDataElm.classList.remove("tyt-metadata-hover-resized"); metaDataElm.classList.remove("tyt-metadata-hover"); }, moFn(lockId) { if (lockGet.fullChannelNameOnHoverAttrAsyncLock !== lockId) { return; } const uploadInfo = qs("#primary.ytd-watch-flexy ytd-watch-metadata #upload-info"); if (!uploadInfo) { return; } const evtOpt = { passive: !0, capture: !1 }; 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 = qs("#primary.ytd-watch-flexy ytd-watch-metadata #upload-info"); if (uploadInfo) { this.mo.observe(uploadInfo, { attributes: !0, attributeFilter: [ "hidden", "attr-3wb0k" ] }); uploadInfo.incAttribute111("attr-3wb0k"); this.ro.observe(uploadInfo); } }, activate() { if (this.activated) { return; } const isPassiveArgSupport = "function" == typeof IntersectionObserver; if (isPassiveArgSupport) { this.activated = !0; 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); }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(this.mo); this.ro = new ResizeObserver(mutations => { if (!(Date.now() > this.checkResize)) { 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"); metaDataElm.classList.contains("tyt-metadata-hover") && metaDataElm.classList.add("tyt-metadata-hover-resized"); break; } } } }); this.promiseReady.resolve(); } } }, "external.ytlstm": { activated: !1, toUse: !0, activate() { if (!this.activated) { this.activated = !0; document.documentElement.classList.add("external-ytlstm"); } } } }; sessionStorage.__$$tmp_UseAutoExpandInfoDesc$$__ && (plugin.autoExpandInfoDesc.toUse = !0); const __attachedSymbol__ = Symbol(); const makeInitAttached = tag => { const inPageRearrange_ = inPageRearrange; inPageRearrange = !1; for (const elm of qsAll(`${tag}`)) { const cnt = insp(elm) || 0; "function" != typeof cnt.attached498 || 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 = qs("#columns.style-scope.ytd-watch-flexy ytd-live-chat-frame#chat"); if (t instanceof Element) { return t; } i > 0 && await delayPn(200); } return null; }; const nsTemplateObtain = () => { let nsTemplate = qs("ytd-watch-flexy noscript[ns-template]"); if (!nsTemplate) { nsTemplate = document.createElement("noscript"); nsTemplate.setAttribute("ns-template", ""); qs("ytd-watch-flexy").appendChild(nsTemplate); } return nsTemplate; }; const isPageDOM = (elm, selector) => !!(elm && elm instanceof Element && elm.nodeName) && (!!elm.closest(selector) && !0 === elm.isConnected); const invalidFlexyParent = hostElement => { if (hostElement instanceof HTMLElement) { const hasFlexyParent = HTMLElement.prototype.closest.call(hostElement, "ytd-watch-flexy"); if (!hasFlexyParent) { return !0; } const currentFlexy = elements.flexy; if (currentFlexy && currentFlexy !== hasFlexyParent) { return !0; } } return !1; }; 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(console.warn); retrieveCE("ytd-comments-header-renderer").then(eventMap["ytd-comments-header-renderer::defined"]).catch(console.warn); retrieveCE("ytd-live-chat-frame").then(eventMap["ytd-live-chat-frame::defined"]).catch(console.warn); retrieveCE("ytd-comments").then(eventMap["ytd-comments::defined"]).catch(console.warn); retrieveCE("ytd-engagement-panel-section-list-renderer").then(eventMap["ytd-engagement-panel-section-list-renderer::defined"]).catch(console.warn); retrieveCE("ytd-watch-metadata").then(eventMap["ytd-watch-metadata::defined"]).catch(console.warn); retrieveCE("ytd-playlist-panel-renderer").then(eventMap["ytd-playlist-panel-renderer::defined"]).catch(console.warn); retrieveCE("ytd-expandable-video-description-body-renderer").then(eventMap["ytd-expandable-video-description-body-renderer::defined"]).catch(console.warn); }, fixForTabDisplay: isResize => { bFixForResizedTabLater = !1; const runLowPriority = () => { for (const element of qsAll("[io-intersected]")) { const cnt = insp(element); if (element instanceof HTMLElement_ && "function" == typeof cnt.calculateCanCollapse) { try { cnt.calculateCanCollapse(!0); } catch {} } } }; "function" == typeof requestIdleCallback ? requestIdleCallback(runLowPriority, { timeout: 100 }) : setTimeout(runLowPriority, 0); isResize || "#tab-info" !== lastTab || requestAnimationFrame(() => { for (const element of qsAll("#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_ && "function" == typeof cnt.notifyResize) { try { cnt.notifyResize(); } catch {} } } for (const element of qsAll("#tab-info ytd-text-inline-expander")) { const cnt = insp(element); element instanceof HTMLElement_ && "function" == typeof cnt.resize && cnt.resize(!1); fixInlineExpanderDisplay(cnt); } }); if (!isResize && "string" == typeof lastTab && lastTab.startsWith("#tab-")) { const tabContent = qs(".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 ("function" == typeof cnt.notifyResize) { try { cnt.notifyResize(); } catch {} } } } } }, "ytd-watch-flexy::defined": cProto => { if (!cProto.updateChatLocation498 && "function" == typeof cProto.updateChatLocation && 0 === cProto.updateChatLocation.length) { cProto.updateChatLocation498 = cProto.updateChatLocation; cProto.updateChatLocation = updateChatLocation498; } if (!cProto.isTwoColumnsChanged498_ && "function" == typeof cProto.isTwoColumnsChanged_ && 2 === cProto.isTwoColumnsChanged_.length) { cProto.isTwoColumnsChanged498_ = cProto.isTwoColumnsChanged_; cProto.isTwoColumnsChanged_ = function(arg1, arg2, ...args) { const r = secondaryInnerFn(() => { const r = this.isTwoColumnsChanged498_(arg1, arg2, ...args); return r; }); return r; }; } if (!cProto.defaultTwoColumnLayoutChanged498 && "function" == typeof cProto.defaultTwoColumnLayoutChanged && 0 === cProto.defaultTwoColumnLayoutChanged.length) { cProto.defaultTwoColumnLayoutChanged498 = cProto.defaultTwoColumnLayoutChanged; cProto.defaultTwoColumnLayoutChanged = function(...args) { const r = secondaryInnerFn(() => { const r = this.defaultTwoColumnLayoutChanged498(...args); return r; }); return r; }; } if (!cProto.updatePlayerLocation498 && "function" == typeof cProto.updatePlayerLocation && 0 === cProto.updatePlayerLocation.length) { cProto.updatePlayerLocation498 = cProto.updatePlayerLocation; cProto.updatePlayerLocation = function(...args) { const r = secondaryInnerFn(() => { const r = this.updatePlayerLocation498(...args); return r; }); return r; }; } if (!cProto.updateCinematicsLocation498 && "function" == typeof cProto.updateCinematicsLocation && 0 === cProto.updateCinematicsLocation.length) { cProto.updateCinematicsLocation498 = cProto.updateCinematicsLocation; cProto.updateCinematicsLocation = function(...args) { const r = secondaryInnerFn(() => { const r = this.updateCinematicsLocation498(...args); return r; }); return r; }; } if (!cProto.updatePanelsLocation498 && "function" == typeof cProto.updatePanelsLocation && 0 === cProto.updatePanelsLocation.length) { cProto.updatePanelsLocation498 = cProto.updatePanelsLocation; cProto.updatePanelsLocation = function(...args) { const r = secondaryInnerFn(() => { const r = this.updatePanelsLocation498(...args); return r; }); return r; }; } if (!cProto.swatcherooUpdatePanelsLocation498 && "function" == typeof cProto.swatcherooUpdatePanelsLocation && 6 === cProto.swatcherooUpdatePanelsLocation.length) { cProto.swatcherooUpdatePanelsLocation498 = cProto.swatcherooUpdatePanelsLocation; cProto.swatcherooUpdatePanelsLocation = function(arg1, arg2, arg3, arg4, arg5, arg6, ...args) { const r = secondaryInnerFn(() => { const r = this.swatcherooUpdatePanelsLocation498(arg1, arg2, arg3, arg4, arg5, arg6, ...args); return r; }); return r; }; } if (!cProto.updateErrorScreenLocation498 && "function" == typeof cProto.updateErrorScreenLocation && 0 === cProto.updateErrorScreenLocation.length) { cProto.updateErrorScreenLocation498 = cProto.updateErrorScreenLocation; cProto.updateErrorScreenLocation = function(...args) { const r = secondaryInnerFn(() => { const r = this.updateErrorScreenLocation498(...args); return r; }); return r; }; } if (!cProto.updateFullBleedElementLocations498 && "function" == typeof cProto.updateFullBleedElementLocations && 0 === cProto.updateFullBleedElementLocations.length) { cProto.updateFullBleedElementLocations498 = cProto.updateFullBleedElementLocations; cProto.updateFullBleedElementLocations = function(...args) { const r = secondaryInnerFn(() => { const r = this.updateFullBleedElementLocations498(...args); return r; }); return r; }; } }, "ytd-watch-next-secondary-results-renderer::defined": cProto => { if (!cProto.attached498 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-watch-next-secondary-results-renderer::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { 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)) { hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (hostElement instanceof HTMLElement_ && hostElement.classList.length > 0 && !hostElement.closest("noscript") && !0 === hostElement.isConnected && hostElement instanceof HTMLElement_ && hostElement.matches("#columns #related ytd-watch-next-secondary-results-renderer") && !hostElement.matches("#right-tabs ytd-watch-next-secondary-results-renderer, [hidden] ytd-watch-next-secondary-results-renderer")) { elements.related = hostElement.closest("#related"); hostElement.setAttribute111("tyt-videos-list", ""); } } }, "ytd-watch-next-secondary-results-renderer::detached": hostElement => { if (hostElement instanceof HTMLElement_ && !hostElement.closest("noscript") && !1 === hostElement.isConnected && hostElement.hasAttribute000("tyt-videos-list")) { elements.related = null; hostElement.removeAttribute000("tyt-videos-list"); } }, 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 || !0 !== hostElement.isConnected || !0 !== cnt.isAttached || !cnt.data || !1 !== cnt.hidden) { return; } const ytdFlexyElm = elements.flexy; const ytdFlexyCnt = ytdFlexyElm ? insp(ytdFlexyElm) : null; ytdFlexyCnt && ytdFlexyCnt.videoId ? hostElement.setAttribute111("tyt-comments-video-id", ytdFlexyCnt.videoId) : hostElement.removeAttribute000("tyt-comments-video-id"); }, checkCommentsShouldBeHidden: lockId => { if (lockGet.checkCommentsShouldBeHiddenLock !== lockId) { return; } const commentsArea = elements.comments; const ytdFlexyElm = elements.flexy; if (commentsArea && ytdFlexyElm && !commentsArea.hasAttribute000("hidden")) { const ytdFlexyCnt = insp(ytdFlexyElm); if ("string" == typeof ytdFlexyCnt.videoId) { const commentsVideoId = commentsArea.getAttribute("tyt-comments-video-id"); commentsVideoId && commentsVideoId !== ytdFlexyCnt.videoId && commentsArea.setAttribute111("hidden", ""); } } }, "ytd-comments::defined": cProto => { if (!cProto.attached498 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-comments::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-comments::detached"]).catch(console.warn); return this.detached498(); }; } cProto._createPropertyObserver("data", "_dataChanged498", void 0); cProto._dataChanged498 = function() { Promise.resolve(this.hostElement).then(eventMap["ytd-comments::_dataChanged498"]).catch(console.warn); }; makeInitAttached("ytd-comments"); }, "ytd-comments::_dataChanged498": hostElement => { if (!hostElement.hasAttribute000("tyt-comments-area")) { return; } let commentsDataStatus = 0; const cnt = insp(hostElement); const data = cnt ? cnt.data : null; const contents = data ? data.contents : null; if (data) { contents && 1 === contents.length && contents[0].messageRenderer && (commentsDataStatus = 2); contents && contents.length > 1 && contents[0].commentThreadRenderer && (commentsDataStatus = 1); } commentsDataStatus ? hostElement.setAttribute111("tyt-comments-data-status", commentsDataStatus) : hostElement.removeAttribute000("tyt-comments-data-status"); Promise.resolve(hostElement).then(eventMap.settingCommentsVideoId).catch(console.warn); }, "ytd-comments::attached": async hostElement => { if (invalidFlexyParent(hostElement)) { return; } hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (!(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest("noscript")) { return; } if (!0 !== hostElement.isConnected) { return; } if (!hostElement || "comments" !== hostElement.id) { return; } elements.comments = hostElement; Promise.resolve(hostElement).then(eventMap.settingCommentsVideoId).catch(console.warn); aoComment.observe(hostElement, { attributes: !0 }); hostElement.setAttribute111("tyt-comments-area", ""); const lockId = lockSet.rightTabReadyLock02; await rightTabsProvidedPromise.then(); if (lockGet.rightTabReadyLock02 === lockId && elements.comments === hostElement && !1 !== hostElement.isConnected) { if (hostElement && !hostElement.closest("#right-tabs")) { qs("#tab-comments").assignChildren111(null, hostElement, null); } else { const shouldTabVisible = elements.comments && elements.comments.closest("#tab-comments") && !elements.comments.closest("[hidden]"); document.querySelector('[tyt-tab-content="#tab-comments"]').classList.toggle("tab-btn-hidden", !shouldTabVisible); Promise.resolve(lockSet.removeKeepCommentsScrollerLock).then(removeKeepCommentsScroller).catch(console.warn); } } }, "ytd-comments::detached": hostElement => { if (hostElement instanceof HTMLElement_ && !hostElement.closest("noscript") && !1 === hostElement.isConnected && hostElement.hasAttribute000("tyt-comments-area")) { hostElement.removeAttribute000("tyt-comments-area"); aoComment.disconnect(); aoComment.takeRecords(); elements.comments = null; document.querySelector('[tyt-tab-content="#tab-comments"]').classList.add("tab-btn-hidden"); Promise.resolve(lockSet.removeKeepCommentsScrollerLock).then(removeKeepCommentsScroller).catch(console.warn); } }, "ytd-comments-header-renderer::defined": cProto => { if (!cProto.attached498 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-comments-header-renderer::attached"]).catch(console.warn); Promise.resolve(this.hostElement).then(eventMap["ytd-comments-header-renderer::dataChanged"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-comments-header-renderer::detached"]).catch(console.warn); return this.detached498(); }; } if (!cProto.dataChanged498 && "function" == typeof cProto.dataChanged) { 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; } hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (!(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest("noscript")) { return; } if (!0 !== hostElement.isConnected) { return; } if (!hostElement || !hostElement.classList.contains("ytd-item-section-renderer")) { return; } const targetElement = qs("[tyt-comments-area] ytd-comments-header-renderer"); if (hostElement === targetElement) { hostElement.setAttribute111("tyt-comments-header-field", ""); } else { const parentNode = hostElement.parentNode; parentNode instanceof HTMLElement_ && parentNode.querySelector("[tyt-comments-header-field]") && hostElement.setAttribute111("tyt-comments-header-field", ""); } }, "ytd-comments-header-renderer::detached": hostElement => { if (hostElement instanceof HTMLElement_ && !hostElement.closest("noscript") && !1 === hostElement.isConnected) { if (hostElement.hasAttribute000("field-of-cm-count")) { hostElement.removeAttribute000("field-of-cm-count"); const cmCount = qs("#tyt-cm-count"); cmCount && !qs("#tab-comments ytd-comments-header-renderer[field-of-cm-count]") && (cmCount.textContent = ""); } 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 = !1; const cnt = insp(hostElement); (cnt && hostElement.closest("#tab-comments") && qs("#tab-comments ytd-comments-header-renderer") === hostElement || hostElement instanceof HTMLElement_ && hostElement.parentNode instanceof HTMLElement_ && hostElement.parentNode.querySelector("[tyt-comments-header-field]")) && (b = !0); if (b) { hostElement.setAttribute111("tyt-comments-header-field", ""); ytdFlexyElm && ytdFlexyElm.removeAttribute000("tyt-comment-disabled"); } if (hostElement.hasAttribute000("tyt-comments-header-field") && !0 === hostElement.isConnected) { if (!headerMutationObserver) { headerMutationObserver = new MutationObserver(eventMap["ytd-comments-header-renderer::deferredCounterUpdate"]); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(headerMutationObserver); } headerMutationObserver.observe(hostElement.parentNode, { subtree: !1, childList: !0 }); 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 = qsAll("#tab-comments ytd-comments-header-renderer[class]"); if (1 === nodes.length) { 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; c > max && (max = c); return [ e.text, c ]; }).filter(a => a[1] === max); 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; c > max && (max = c); return [ e.text, c ]; }).filter(a => a[1] === max); z.length >= 1 && (ez = z[0][0]); } const cmCount = qs("#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 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-expander::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-expander::detached"]).catch(console.warn); return this.detached498(); }; } if (!cProto.calculateCanCollapse498 && "function" == typeof cProto.calculateCanCollapse) { cProto.calculateCanCollapse498 = cProto.calculateCanCollapse; cProto.calculateCanCollapse = funcCanCollapse; } if (!cProto.childrenChanged498 && "function" == typeof cProto.childrenChanged) { cProto.childrenChanged498 = cProto.childrenChanged; cProto.childrenChanged = function() { Promise.resolve(this.hostElement).then(eventMap["ytd-expander::childrenChanged"]).catch(console.warn); return this.childrenChanged498(); }; } makeInitAttached("ytd-expander"); }, "ytd-expander::childrenChanged": hostElement => { 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 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-expandable-video-description-body-renderer::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { 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; infoExpanderElementProvidedPromise.resolve(); hostElement.setAttribute111("tyt-main-info", ""); 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 (!1 === hostElement.isConnected) { return; } elements.infoExpander.classList.add("tyt-main-info"); const infoExpander = elements.infoExpander; const inlineExpanderElm = infoExpander.querySelector("ytd-text-inline-expander"); if (inlineExpanderElm) { const mo = new MutationObserver(() => { const p = qs("#tab-info ytd-text-inline-expander"); sessionStorage.__$$tmp_UseAutoExpandInfoDesc$$__ = p && p.hasAttribute("is-expanded") ? "1" : ""; p && fixInlineExpanderContent(); }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(mo); mo.observe(inlineExpanderElm, { attributes: [ "is-expanded", "attr-6v8qu", "hidden" ], subtree: !0 }); inlineExpanderElm.incAttribute111("attr-6v8qu"); const cnt = insp(inlineExpanderElm); cnt && fixInlineExpanderDisplay(cnt); } if (infoExpander && !infoExpander.closest("#right-tabs")) { const tabInfoElm = qs("#tab-info"); tabInfoElm && tabInfoElm.assignChildren111(null, infoExpander, null); } else if (qs('[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); } hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (hostElement instanceof HTMLElement_ && hostElement.classList.length > 0 && !hostElement.closest("noscript") && !0 === hostElement.isConnected) { if (isPageDOM(hostElement, "#tab-info [tyt-main-info]")) {} else if (!hostElement.closest("#tab-info")) { const bodyRenderer = hostElement; let bodyRendererNew = qs("ytd-expandable-video-description-body-renderer[tyt-info-renderer]"); if (!bodyRendererNew) { bodyRendererNew = document.createElement("ytd-expandable-video-description-body-renderer"); bodyRendererNew.setAttribute("tyt-info-renderer", ""); nsTemplateObtain().appendChild(bodyRendererNew); } const cnt = insp(bodyRendererNew); cnt.data = Object.assign({}, insp(bodyRenderer).data); const inlineExpanderElm = bodyRendererNew.querySelector("ytd-text-inline-expander"); const inlineExpanderCnt = insp(inlineExpanderElm); fixInlineExpanderMethods(inlineExpanderCnt); elements.infoExpanderRendererBack = bodyRenderer; elements.infoExpanderRendererFront = bodyRendererNew; bodyRenderer.setAttribute("tyt-info-renderer-back", ""); bodyRendererNew.setAttribute("tyt-info-renderer-front", ""); } } }, "ytd-expandable-video-description-body-renderer::detached": async hostElement => { if (hostElement instanceof HTMLElement_ && !hostElement.closest("noscript") && !1 === hostElement.isConnected && hostElement.hasAttribute000("tyt-main-info")) { elements.infoExpander = null; hostElement.removeAttribute000("tyt-main-info"); } }, "ytd-expander::attached": async hostElement => { if (!invalidFlexyParent(hostElement)) { hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (hostElement instanceof HTMLElement_ && hostElement.classList.length > 0 && !hostElement.closest("noscript") && !0 === hostElement.isConnected && 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); } } }, "ytd-expander::detached": hostElement => { if (hostElement instanceof HTMLElement_ && !hostElement.closest("noscript") && !1 === hostElement.isConnected) { if (hostElement.hasAttribute000("tyt-content-comment-entry")) { ioComment.unobserve(hostElement); hostElement.removeAttribute000("tyt-content-comment-entry"); } else if (hostElement.hasAttribute000("tyt-main-info")) { elements.infoExpander = null; hostElement.removeAttribute000("tyt-main-info"); } } }, "ytd-live-chat-frame::defined": cProto => { if (!cProto.attached498 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-live-chat-frame::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-live-chat-frame::detached"]).catch(console.warn); return this.detached498(); }; } if ("function" == typeof cProto.urlChanged && !cProto.urlChanged66 && !cProto.urlChangedAsync12 && 0 === cProto.urlChanged.length) { cProto.urlChanged66 = cProto.urlChanged; let ath = 0; cProto.urlChangedAsync12 = async function() { await this.__urlChangedAsyncT689__; const t = ath = 1 + (1073741823 & ath); const chatframe = this.chatframe || (this.$ || 0).chatframe || 0; if (chatframe instanceof HTMLIFrameElement) { if (null === chatframe.contentDocument) { await Promise.resolve("#").catch(console.warn); if (t !== ath) { return; } } await new Promise(resolve => setTimeout_(resolve, 1)).catch(console.warn); if (t !== ath) { return; } const isBlankPage = !this.data || this.collapsed; const p1 = new Promise(resolve => setTimeout_(resolve, 706)).catch(console.warn); const p2 = new Promise(resolve => { new IntersectionObserver((entries, observer) => { for (const entry of entries) { const rect = entry.boundingClientRect || 0; if (isBlankPage || rect.width > 0 && rect.height > 0) { observer.disconnect(); resolve("#"); break; } } }).observe(chatframe); }).catch(console.warn); await Promise.race([ p1, p2 ]); if (t !== ath) { return; } } this.urlChanged66(); }; cProto.urlChanged = function() { const t = this.__urlChangedAsyncT688__ = 1 + (1073741823 & this.__urlChangedAsyncT688__); nextBrowserTick(() => { t === this.__urlChangedAsyncT688__ && this.urlChangedAsync12(); }); }; } makeInitAttached("ytd-live-chat-frame"); }, "ytd-live-chat-frame::attached": async hostElement => { if (invalidFlexyParent(hostElement)) { return; } hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (!(hostElement instanceof HTMLElement_) || !(hostElement.classList.length > 0) || hostElement.closest("noscript")) { return; } if (!0 !== hostElement.isConnected) { return; } if (!hostElement || "chat" !== hostElement.id) { return; } const lockId = lockSet.ytdLiveAttachedLock; const chatElem = await getGeneralChatElement(); if (lockGet.ytdLiveAttachedLock === lockId) { if (chatElem === hostElement) { elements.chat = chatElem; aoChat.observe(chatElem, { attributes: !0 }); 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 qsAll("[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__ && !0 === cnt.isAttached && !0 === hostElement.isConnected) { p.resolve(); q === cnt.__urlChangedAsyncT688__ && cnt.urlChanged(); } }, 320); Promise.resolve(lockSet.layoutFixLock).then(layoutFix); } else { console.warn("Issue found in ytd-live-chat-frame::attached", chatElem, hostElement); } } }, "ytd-live-chat-frame::detached": hostElement => { if (hostElement instanceof HTMLElement_ && !hostElement.closest("noscript") && !1 === hostElement.isConnected && 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 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-engagement-panel-section-list-renderer::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { 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", ""); egmPanelsCache.add(hostElement); Promise.resolve(lockSet.updateEgmPanelsLock).then(updateEgmPanels).catch(console.warn); aoEgmPanels.observe(hostElement, { attributes: !0, attributeFilter: [ "visibility", "hidden" ] }); } }, "ytd-engagement-panel-section-list-renderer::attached": hostElement => { if (!invalidFlexyParent(hostElement)) { hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (hostElement instanceof HTMLElement_ && hostElement.classList.length > 0 && !hostElement.closest("noscript") && !0 === hostElement.isConnected && hostElement.matches("#panels.ytd-watch-flexy > ytd-engagement-panel-section-list-renderer")) { 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: !0, attributeFilter: [ "visibility", "target-id" ] }); } } } }, "ytd-engagement-panel-section-list-renderer::detached": hostElement => { if (hostElement instanceof HTMLElement_ && !hostElement.closest("noscript") && !1 === hostElement.isConnected) { 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 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-watch-metadata::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { 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)) { hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); hostElement instanceof HTMLElement_ && hostElement.classList.length > 0 && !hostElement.closest("noscript") && !0 === hostElement.isConnected && plugin.fullChannelNameOnHover.activated && plugin.fullChannelNameOnHover.onNavigateFinish(); } }, "ytd-watch-metadata::detached": hostElement => { hostElement instanceof HTMLElement_ && hostElement.closest("noscript"); }, "ytd-playlist-panel-renderer::defined": cProto => { if (!cProto.attached498 && "function" == typeof cProto.attached) { cProto.attached498 = cProto.attached; cProto.attached = function() { inPageRearrange || Promise.resolve(this.hostElement).then(eventMap["ytd-playlist-panel-renderer::attached"]).catch(console.warn); return this.attached498(); }; } if (!cProto.detached498 && "function" == typeof cProto.detached) { cProto.detached498 = cProto.detached; cProto.detached = function() { 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)) { hostElement instanceof Element && (hostElement[__attachedSymbol__] = !0); if (hostElement instanceof HTMLElement_ && hostElement.classList.length > 0 && !hostElement.closest("noscript") && !0 === hostElement.isConnected) { elements.playlist = hostElement; aoPlayList.observe(hostElement, { attributes: !0, attributeFilter: [ "hidden", "collapsed", "attr-1y6nu" ] }); hostElement.incAttribute111("attr-1y6nu"); } } }, "ytd-playlist-panel-renderer::detached": hostElement => { hostElement instanceof HTMLElement_ && hostElement.closest("noscript"); }, _yt_playerProvided: () => { mLoaded.flag |= 4; document.documentElement.setAttribute111("tabview-loaded", mLoaded.makeString()); }, relatedElementProvided: target => { if (!target.closest("[hidden]")) { elements.related = target; videosElementProvidedPromise.resolve(); } }, onceInfoExpanderElementProvidedPromised: () => { const ytdFlexyElm = elements.flexy; ytdFlexyElm && ytdFlexyElm.setAttribute111("hide-default-text-inline-expander", ""); }, refreshSecondaryInner: lockId => { if (lockGet.refreshSecondaryInnerLock !== lockId) { return; } const ytdFlexyElm = elements.flexy; ytdFlexyElm && ytdFlexyElm.matches("ytd-watch-flexy[theater][full-bleed-player]:not([full-bleed-no-max-width-columns])") && ytdFlexyElm.setAttribute111("full-bleed-no-max-width-columns", ""); const related = elements.related; related && related.isConnected && !related.closest("#right-tabs #tab-videos") && qs("#tab-videos").assignChildren111(null, related, null); const infoExpander = elements.infoExpander; infoExpander && infoExpander.isConnected && !infoExpander.closest("#right-tabs #tab-info") && qs("#tab-info").assignChildren111(null, infoExpander, null); const commentsArea = elements.comments; if (commentsArea) { const isConnected = commentsArea.isConnected; if (isConnected && !commentsArea.closest("#right-tabs #tab-comments")) { const tab = qs("#tab-comments"); tab.assignChildren111(null, commentsArea, null); } } }, "yt-navigate-finish": () => { "function" == typeof shouldActivateMoOverall && (shouldActivateMoOverall() ? activateMoOverall() : deactivateMoOverall()); const ytdAppElm = qs("ytd-page-manager#page-manager.style-scope.ytd-app"); const ytdAppCnt = insp(ytdAppElm); pageType = ytdAppCnt ? (ytdAppCnt.data || 0).page : null; if (!qs("ytd-watch-flexy #player")) { return; } const flexyArr = qsAll("ytd-watch-flexy").filter(e => !e.closest("[hidden]") && e.querySelector("#player")); if (1 === flexyArr.length) { 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(); plugin.minibrowser.toUse && plugin.minibrowser.activate(); plugin.autoExpandInfoDesc.toUse && plugin.autoExpandInfoDesc.activate(); plugin.fullChannelNameOnHover.toUse && plugin.fullChannelNameOnHover.activate(); } const chat = elements.chat; chat instanceof Element && chat.setAttribute111("tyt-active-chat-frame", "CF"); const infoExpander = elements.infoExpander; infoExpander && infoExpander.closest("#right-tabs") && Promise.resolve(lockSet.infoFixLock).then(infoFix).catch(console.warn); Promise.resolve(lockSet.layoutFixLock).then(layoutFix); plugin.fullChannelNameOnHover.activated && plugin.fullChannelNameOnHover.onNavigateFinish(); } }, onceInsertRightTabs: () => { const related = elements.related; let rightTabs = qs("#right-tabs"); if (!qs("#right-tabs") && related) { getLangForPage(); const docTmp = document.createElement("template"); docTmp.innerHTML = createHTML((function getTabsHTML() { const sTabBtnVideos = `${svgElm(16, 16, 90, 90, svgVideos)}<span>${getWord("videos")}</span>`; const sTabBtnInfo = `${svgElm(16, 16, 60, 60, svgInfo)}<span>${getWord("info")}</span>`; const sTabBtnPlayList = `${svgElm(16, 16, 20, 20, svgPlayList)}<span>${getWord("playlist")}</span>`; const str1 = '\n <paper-ripple class="style-scope yt-icon-button">\n <div id="background" class="style-scope paper-ripple" style="opacity:0;"></div>\n <div id="waves" class="style-scope paper-ripple"></div>\n </paper-ripple>\n '; const str_fbtns = '\n <div class="font-size-right">\n <div class="font-size-btn font-size-plus" tyt-di="8rdLQ">\n <svg width="12" height="12" viewbox="0 0 50 50" preserveAspectRatio="xMidYMid meet" \n stroke="currentColor" stroke-width="6" stroke-linecap="round" vector-effect="non-scaling-size">\n <path d="M12 25H38M25 12V38"/>\n </svg>\n </div><div class="font-size-btn font-size-minus" tyt-di="8rdLQ">\n <svg width="12" height="12" viewbox="0 0 50 50" preserveAspectRatio="xMidYMid meet"\n stroke="currentColor" stroke-width="6" stroke-linecap="round" vector-effect="non-scaling-size">\n <path d="M12 25h26"/>\n </svg>\n </div>\n </div>\n '.replace(/[\r\n]+/g, ""); const str_tabs = [ `<a id="tab-btn1" role="tab" aria-selected="false" aria-controls="tab-info" tyt-di="q9Kjc" tyt-tab-content="#tab-info" class="tab-btn${1 & ~hiddenTabsByUserCSS ? "" : " tab-btn-hidden"}">${sTabBtnInfo}${str1}${str_fbtns}</a>`, `<a id="tab-btn3" role="tab" aria-selected="false" aria-controls="tab-comments" tyt-di="q9Kjc" tyt-tab-content="#tab-comments" class="tab-btn${2 & ~hiddenTabsByUserCSS ? "" : " tab-btn-hidden"}">${svgElm(16, 16, 120, 120, svgComments)}<span id="tyt-cm-count"></span>${str1}${str_fbtns}</a>`, `<a id="tab-btn4" role="tab" aria-selected="false" aria-controls="tab-videos" tyt-di="q9Kjc" tyt-tab-content="#tab-videos" class="tab-btn${4 & ~hiddenTabsByUserCSS ? "" : " tab-btn-hidden"}">${sTabBtnVideos}${str1}${str_fbtns}</a>`, `<a id="tab-btn5" role="tab" aria-selected="false" aria-controls="tab-list" tyt-di="q9Kjc" tyt-tab-content="#tab-list" class="tab-btn tab-btn-hidden">${sTabBtnPlayList}${str1}${str_fbtns}</a>` ].join(""); const addHTML = `\n <div id="right-tabs">\n <tabview-view-pos-thead></tabview-view-pos-thead>\n <header>\n <div id="material-tabs" role="tablist">\n ${str_tabs}\n </div>\n </header>\n <div class="tab-content">\n <div id="tab-info" role="tabpanel" aria-labelledby="tab-btn1" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>\n <div id="tab-comments" role="tabpanel" aria-labelledby="tab-btn3" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>\n <div id="tab-videos" role="tabpanel" aria-labelledby="tab-btn4" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>\n <div id="tab-list" role="tabpanel" aria-labelledby="tab-btn5" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>\n </div>\n </div>\n `; return addHTML; })()); const newElm = docTmp.content.firstElementChild; if (null !== newElm) { inPageRearrange = !0; related.parentNode.insertBefore000(newElm, related); inPageRearrange = !1; } rightTabs = newElm; rightTabs.querySelector('[tyt-tab-content="#tab-comments"]').classList.add("tab-btn-hidden"); const secondaryWrapper = document.createElement("secondary-wrapper"); secondaryWrapper.classList.add("tabview-secondary-wrapper"); secondaryWrapper.id = "secondary-inner-wrapper"; const secondaryInner = qs("#secondary-inner.style-scope.ytd-watch-flexy"); if (!secondaryInner) { return; } inPageRearrange = !0; secondaryWrapper.replaceChildren000(...secondaryInner.childNodes); secondaryInner.insertBefore000(secondaryWrapper, secondaryInner.firstChild); inPageRearrange = !1; rightTabs.querySelector("#material-tabs").addEventListener("click", eventMap["tabs-btn-click"], !0); rightTabs.querySelector("#material-tabs").addEventListener("keydown", e => { if ("ArrowLeft" !== e.key && "ArrowRight" !== e.key && "Home" !== e.key && "End" !== e.key) { return; } const tabs = Array.from(rightTabs.querySelectorAll("#material-tabs a.tab-btn[tyt-tab-content]:not(.tab-btn-hidden)")); if (0 === tabs.length) { return; } const idx = tabs.indexOf(document.activeElement); if (idx < 0) { return; } e.preventDefault(); let next; next = "ArrowRight" === e.key ? tabs[(idx + 1) % tabs.length] : "ArrowLeft" === e.key ? tabs[(idx - 1 + tabs.length) % tabs.length] : "Home" === e.key ? tabs[0] : tabs[tabs.length - 1]; next.focus(); next.click(); }); inPageRearrange = !0; rightTabs.closest("secondary-wrapper") || secondaryWrapper.appendChild000(rightTabs); inPageRearrange = !1; } if (rightTabs) { isRightTabsInserted = !0; 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 qsAll(".tab-btn[tyt-tab-content]")) { ioTabBtns.observe(btn); } related.closest("#right-tabs") || qs("#tab-videos").assignChildren111(null, related, null); const infoExpander = elements.infoExpander; infoExpander && !infoExpander.closest("#right-tabs") && qs("#tab-info").assignChildren111(null, infoExpander, null); const commentsArea = elements.comments; commentsArea && !commentsArea.closest("#right-tabs") && qs("#tab-comments").assignChildren111(null, commentsArea, null); rightTabsProvidedPromise.resolve(); roRightTabs.disconnect(); roRightTabs.observe(rightTabs); const ytdFlexyElm = elements.flexy; const aoFlexy = new MutationObserver(eventMap.aoFlexyFn); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(aoFlexy); aoFlexy.observe(ytdFlexyElm, { attributes: !0 }); Promise.resolve(lockSet.fixInitialTabStateLock).then(eventMap.fixInitialTabStateFn).catch(console.warn); ytdFlexyElm.incAttribute111("attr-7qlsy"); } }, 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) { for (const continuation of qsAll("#tab-videos ytd-watch-next-secondary-results-renderer ytd-continuation-item-renderer")) { if (continuation.closest("[hidden]")) { continue; } const cnt = insp(continuation); if ("boolean" == typeof cnt.showButton) { if (!1 === cnt.showButton) { continue; } cnt.showButton = !1; const behavior = cnt.ytRendererBehavior || cnt; "function" == typeof behavior.invalidate && behavior.invalidate(!1); } } } }, tabsStatusCorrection: lockId => { if (lockId !== lockGet.tabsStatusCorrectionLock) { return; } const ytdFlexyElm = elements.flexy; if (!ytdFlexyElm) { return; } const p = tabAStatus; const q = calculationFn(p, 4351); let resetForPanelDisappeared = !1; const wasTheaterBeforeFullscreen = !(1 & ~p); const isEnteringFullscreen = !(64 & p || 64 & ~q); const isExitingFullscreen = !(64 & ~p || 64 & q); if (p !== q) { let actioned = !1; let special = 0; plugin["external.ytlstm"].activated ? 64 & q ? isEnteringFullscreen && wasTheaterBeforeFullscreen && setTimeout(() => { if (isTheater()) {} else { const sizeBtn = qs("ytd-watch-flexy #ytd-player button.ytp-size-button"); sizeBtn && !isTheater() && sizeBtn.click(); } }, 300) : 4121 == (4127 & p) && 4117 == (4127 & q) ? special = 3 : 17 & ~q || !qs("[data-ytlstm-theater-mode]") ? 25 & ~q || !qs('[is-two-columns_][theater][tyt-chat="+"]') || (special = 2) : special = 1 : isExitingFullscreen && wasTheaterBeforeFullscreen && setTimeout(() => { if (!isTheater()) { const sizeBtn = qs("ytd-watch-flexy #ytd-player button.ytp-size-button"); sizeBtn && sizeBtn.click(); } }, 300); if (special) {} else if (128 & p || 128 & ~q) { if (8 & p || 8 & ~q) { if ((4 & ~p || 12 & q) && (8 & ~p || 12 & q) || "chat" !== lastPanel) { if (8 == (12 & p) && 4 == (12 & q) && "chat" === lastPanel) { lastPanel = lastTab || ""; resetForPanelDisappeared = !0; } else if (!(128 & ~p || 128 & q || "playlist" !== lastPanel)) { lastPanel = lastTab || ""; resetForPanelDisappeared = !0; } } else { lastPanel = lastTab || ""; resetForPanelDisappeared = !0; } } else { lastPanel = "chat"; } } else { lastPanel = "playlist"; } tabAStatus = q; if (special) { if (1 === special) { "+" !== ytdFlexyElm.getAttribute("tyt-chat") && ytBtnExpandChat(); ytdFlexyElm.getAttribute("tyt-tab") && switchToTab(null); } else if (2 === special) { ytBtnCollapseChat(); } else if (3 === special) { ytBtnCancelTheater(); lastTab && switchToTab(lastTab); } return; } let bFixForResizedTab = !1; 2 == (2 ^ q) && bFixForResizedTabLater && (bFixForResizedTab = !0); !(16 & ~p) & !(16 & q) && Promise.resolve(lockSet.twoColumnChanged10Lock).then(eventMap.twoColumnChanged10).catch(console.warn); !(2 & ~p) ^ !(2 & ~q) && !(2 & ~q) && (bFixForResizedTab = !0); if (!(2 & p || 2 & ~q || 128 & ~p || 128 & ~q)) { lastPanel = lastTab || ""; ytBtnClosePlaylist(); actioned = !0; } if (128 == (136 & p) && !(136 & ~q) && "chat" === lastPanel) { lastPanel = lastTab || ""; ytBtnClosePlaylist(); actioned = !0; } if (27 == (255 & p) && 26 == (255 & q)) { lastPanel = lastTab || ""; ytBtnCollapseChat(); actioned = !0; } if (2 == (130 & p) && !(130 & ~q) && "playlist" === lastPanel) { switchToTab(null); actioned = !0; } if (8 == (136 & p) && !(136 & ~q) && "playlist" === lastPanel) { lastPanel = lastTab || ""; ytBtnCollapseChat(); actioned = !0; } if (17 == (145 & p) && !(145 & ~q)) { ytBtnCancelTheater(); actioned = !0; } if (144 == (145 & p) && !(145 & ~q)) { lastPanel = lastTab || ""; ytBtnClosePlaylist(); actioned = !0; } if (64 & ~q) { if (64 & ~p || 64 & q) { if (17 == (59 & p) && 25 == (59 & q)) { ytBtnCancelTheater(); actioned = !0; } else if (16 == (49 & p) && 48 == (49 & q) && (10 & q) > 0) { if (2 & q) { switchToTab(null); actioned = !0; } if (8 & q) { ytBtnCollapseChat(); actioned = !0; } } else if (24 != (27 & p) || 16 != (27 & q) || 128 & q) { if (1 & p || 1 & ~q) { if (1 != (3 & p) || 3 & ~q) { if (2 != (10 & p) || 10 & ~q) { if (32 != (40 & p) || 40 & ~q) { if (32 != (34 & p) || 34 & ~q) { if (8 != (10 & p) || 10 & ~q) { if (!(1 & ~p || 33 & q)) { if ("chat" === lastPanel) { ytBtnExpandChat(); actioned = !0; } else if (lastPanel === lastTab && lastTab) { switchToTab(lastTab); actioned = !0; } } } else { ytBtnCollapseChat(); actioned = !0; } } else { ytBtnCloseEngagementPanels(); actioned = !0; } } else { ytBtnCloseEngagementPanels(); actioned = !0; } } else { switchToTab(null); actioned = !0; } } else { ytBtnCancelTheater(); actioned = !0; } } else { 32 & ~q || ytBtnCloseEngagementPanels(); 8 != (9 & p) || 9 & ~q || ytBtnCollapseChat(); switchToTab(null); actioned = !0; } } else if (lastTab) { switchToTab(lastTab); actioned = !0; } } else { 32 & ~q || ytBtnCloseEngagementPanels(); if (!(10 & ~q)) { if ("chat" === lastPanel) { switchToTab(null); actioned = !0; } else if (lastPanel) { ytBtnCollapseChat(); actioned = !0; } } } } else { actioned = !1; } if (!(actioned || 128 & ~q)) { lastPanel = "playlist"; if (!(2 & ~q)) { switchToTab(null); actioned = !0; } } let shouldDoAutoFix = !1; (2 & ~p || 128 != (130 & q)) && (8 & ~p || 128 != (136 & q)) && (actioned || 16 != (17 & p) || 16 != (123 & q) ? 20 == (255 & q) && (shouldDoAutoFix = !0) : shouldDoAutoFix = !0); if (shouldDoAutoFix) { if ("chat" === lastPanel) { ytBtnExpandChat(); actioned = !0; } else if ("playlist" === lastPanel) { !(function ytBtnOpenPlaylist() { const cnt = insp(elements.playlist); cnt && "boolean" == typeof cnt.collapsed && (cnt.collapsed = !1); })(); actioned = !0; } else if (lastTab) { switchToTab(lastTab); actioned = !0; } else if (resetForPanelDisappeared) { Promise.resolve(lockSet.fixInitialTabStateLock).then(eventMap.fixInitialTabStateFn).catch(console.warn); actioned = !0; } } if (bFixForResizedTab) { bFixForResizedTabLater = !1; Promise.resolve(0).then(eventMap.fixForTabDisplay).catch(console.warn); } if (!(16 & ~p) ^ !(16 & ~q)) { 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; bodyRendererNew && bodyRenderer && (insp(bodyRendererNew).data = insp(bodyRenderer).data); Promise.resolve(lockSet.infoFixLock).then(infoFix).catch(console.warn); }, fixInitialTabStateFn: async lockId => { if (lockGet.fixInitialTabStateLock !== lockId) { return; } const delayTime = fixInitialTabStateK > 0 ? 200 : 1; await delayPn(delayTime); if (lockGet.fixInitialTabStateLock !== lockId) { return; } const kTab = qs("[tyt-tab]"); const qTab = kTab && "" !== kTab.getAttribute("tyt-tab") ? null : checkElementExist("ytd-watch-flexy[is-two-columns_]", "[hidden]"); if (checkElementExist("ytd-playlist-panel-renderer#playlist", "[hidden], [collapsed]")) { switchToTab(null); } else if (checkElementExist("ytd-live-chat-frame#chat", "[hidden], [collapsed]")) { switchToTab(null); checkElementExist("ytd-watch-flexy[theater]", "[hidden]") && ytBtnCollapseChat(); } else if (qTab) { const hasTheater = qTab.hasAttribute("theater"); if (hasTheater) { switchToTab(null); } else { const btn0 = qs(".tab-btn-visible"); switchToTab(btn0 || null); } } 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 = "undefined" != typeof customElements && "function" == typeof (customElements || 0).whenDefined; const promiseForCustomYtElementsReady = isCustomElementsProvided ? Promise.resolve(0) : new Promise(callback => { if ("undefined" == typeof customElements) { "__CE_registry" in document || Object.defineProperty(document, "__CE_registry", { get() {}, set(nv) { if ("object" == typeof nv) { delete this.__CE_registry; this.__CE_registry = nv; this.dispatchEvent(new CustomEvent("ytI-ce-registry-created")); } return !0; }, enumerable: !1, configurable: !0 }); let eventHandler = () => { document.removeEventListener("ytI-ce-registry-created", eventHandler, !1); const f = callback; callback = null; eventHandler = null; f(); }; document.addEventListener("ytI-ce-registry-created", eventHandler, !1); } else { callback(); } }); const retrieveCE = async nodeName => { try { isCustomElementsProvided || await promiseForCustomYtElementsReady; await customElements.whenDefined(nodeName); const dummy = qs(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 }; let promiseWaitNext = null; const moOverall = new MutationObserver(() => { if (promiseWaitNext) { promiseWaitNext.resolve(); promiseWaitNext = null; } if ("function" == typeof moOverallRes._yt_playerProvided) { const r = moOverallRes._yt_playerProvided(); if (r) { moOverallRes._yt_playerProvided = r; eventMap._yt_playerProvided(); } } }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(moOverall); let moOverallActive = !1; const shouldActivateMoOverall = () => { try { return !!qs("ytd-watch-flexy #player"); } catch { return !1; } }; const activateMoOverall = () => { if (moOverallActive) { return; } const target = document.head || document.documentElement || document; moOverall.observe(target, { subtree: !0, childList: !0 }); moOverallActive = !0; }; const deactivateMoOverall = () => { if (moOverallActive) { moOverall.disconnect(); moOverallActive = !1; } }; shouldActivateMoOverall() && activateMoOverall(); const moEgmPanelReady = new MutationObserver(mutations => { for (const mutation of mutations) { const target = mutation.target; if (target.hasAttribute000("tyt-egm-panel-jclmd") && (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); } } }); YouTubeUtils?.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(moEgmPanelReady); YouTubeUtils?.ObserverRegistry?.track && YouTubeUtils.ObserverRegistry.track(); const moEgmPanelReadyClearFn = () => { if (null === qs("[tyt-egm-panel-jclmd]")) { moEgmPanelReady.takeRecords(); moEgmPanelReady.disconnect(); } }; YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", eventMap["yt-navigate-finish"], !1) : document.addEventListener("yt-navigate-finish", eventMap["yt-navigate-finish"], !1); const _animStartHandler = evt => { const f = eventMap[evt.animationName]; "function" == typeof f && f(evt.target); }; YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "animationstart", _animStartHandler, capturePassive) : document.addEventListener("animationstart", _animStartHandler, capturePassive); mLoaded.flag |= 1; document.documentElement.setAttribute111("tabview-loaded", mLoaded.makeString()); promiseForCustomYtElementsReady.then(eventMap.ceHack).catch(console.warn); } catch (e) { console.error("error 0xF491", e); } }; const styles = { main: '\n @keyframes relatedElementProvided{0%{background-position-x:3px;}100%{background-position-x:4px;}}\n html[tabview-loaded="icp"] #related.ytd-watch-flexy{animation:relatedElementProvided 1ms linear 0s 1 normal forwards;}\n 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;}\n #secondary.ytd-watch-flexy{position:relative;}\n #secondary-inner.style-scope.ytd-watch-flexy{height:100%;}\n #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;}\n #right-tabs{position:relative;display:flex;padding:0;margin:0;flex-grow:1;flex-direction:column;}\n [tyt-tab=""] #right-tabs{flex-grow:0;}\n [tyt-tab=""] #right-tabs .tab-content{border:0;}\n #right-tabs .tab-content{flex-grow:1;}\n ytd-watch-flexy[hide-default-text-inline-expander] #primary.style-scope.ytd-watch-flexy ytd-text-inline-expander{display:none;}\n ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden{--comment-pre-load-sizing:90px;visibility:collapse;z-index:-1;position:absolute!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;}\n 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;}\n 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;}\n ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents{--comment-pre-load-display:none;}\n 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;}\n ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents>*{display:var(--comment-pre-load-display)!important;}\n ytd-watch-flexy #tab-comments:not(.tab-content-hidden){pointer-events:auto!important;}\n ytd-watch-flexy #tab-comments:not(.tab-content-hidden) *{pointer-events:auto!important;}\n ytd-watch-flexy #tab-comments:not(.tab-content-hidden) button,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) yt-button-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) a,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) tp-yt-paper-button,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) [role="button"],ytd-watch-flexy #tab-comments:not(.tab-content-hidden) yt-button-shape{pointer-events:auto!important;}\n ytd-watch-flexy #tab-comments tp-yt-paper-button{white-space:normal;word-break:break-word;max-width:100%;overflow-wrap:break-word;}\n ytd-watch-flexy #tab-comments:not(.tab-content-hidden) ytd-comment-action-buttons-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) ytd-button-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) #action-buttons,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) ytd-menu-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) yt-dropdown-menu{pointer-events:auto!important;}\n #right-tabs #material-tabs{position:relative;display:flex;padding:0;border:1px solid var(--ytd-searchbox-legacy-border-color);overflow:hidden;}\n [tyt-tab] #right-tabs #material-tabs{border-radius:12px;}\n [tyt-tab^="#"] #right-tabs #material-tabs{border-radius:12px 12px 0 0;}\n ytd-watch-flexy:not([is-two-columns_]) #right-tabs #material-tabs{outline:0;}\n #right-tabs #material-tabs a.tab-btn[tyt-tab-content]>*{pointer-events:none;}\n #right-tabs #material-tabs a.tab-btn[tyt-tab-content]>.font-size-right{pointer-events:initial;display:none;}\n ytd-watch-flexy #right-tabs .tab-content{padding:0;box-sizing:border-box;display:block;border:1px solid var(--ytd-searchbox-legacy-border-color);border-top:0;position:relative;top:0;display:flex;flex-direction:row;overflow:hidden;border-radius:0 0 12px 12px;}\n ytd-watch-flexy:not([is-two-columns_]) #right-tabs .tab-content{height:100%;}\n 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;will-change:scroll-position;}\n .tab-content-cld,#right-tabs,.tab-content{transition:none;animation:none;}\n ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar{width:8px;height:8px;}\n ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar-track{background:transparent;}\n ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar-thumb{background:rgba(144,144,144,.5);border-radius:4px;}\n ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar-thumb:hover{background:rgba(170,170,170,.7);}\n #right-tabs #emojis.ytd-commentbox{inset:auto 0 auto 0;width:auto;}\n ytd-watch-flexy[is-two-columns_] #right-tabs .tab-content-cld{height:100%;width:100%;contain:size layout paint style;position:absolute;}\n ytd-watch-flexy #right-tabs .tab-content-cld.tab-content-hidden{display:none;width:100%;contain:size layout paint style;}\n @supports (color:var(--tabview-tab-btn-define)){\n ytd-watch-flexy #right-tabs .tab-btn{background:var(--yt-spec-general-background-a);}\n 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));}\n 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;}\n 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;}\n ytd-watch-flexy #right-tabs .tab-btn{--tabview-btn-txt-ml:8px;}\n ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"]{--tabview-btn-txt-ml:0;}\n ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]>svg+span{margin-left:var(--tabview-btn-txt-ml);}\n 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;}\n ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].active svg{opacity:.9;}\n 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);}\n ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]:not(.active):hover svg{opacity:.9;}\n ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].tab-btn-hidden{display:none;}\n 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);}\n ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"] span#tyt-cm-count:empty{display:none;}\n 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);}}\n @supports (color:var(--tyt-cm-count-define)){\n ytd-watch-flexy{--tyt-x-loading-content-letter-spacing:2px;}\n html{--tabview-text-loading:"Loading";--tabview-text-fetching:"Fetching";--tabview-panel-loading:var(--tabview-text-loading);}\n 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);}}\n @supports (color:var(--tabview-font-size-btn-define)){\n .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;}\n html body ytd-watch-flexy.style-scope .font-size-btn{user-select:none!important;}\n .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;}\n .font-size-btn:hover{background-color:var(--yt-spec-text-primary);color:var(--yt-spec-general-background-a);}\n @supports (zoom:.5){\n .tab-btn .font-size-btn{--tyt-font-size-btn-display:none;}\n .tab-btn.active:hover .font-size-btn{--tyt-font-size-btn-display:inline-block;}\n body ytd-watch-flexy:not([is-two-columns_]) #columns.ytd-watch-flexy{flex-direction:column;}\n body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy{display:block;width:100%;box-sizing:border-box;}\n body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy secondary-wrapper{padding-left:var(--ytd-margin-6x);contain:content;height:initial;}\n body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy secondary-wrapper #right-tabs{overflow:auto;}\n [tyt-chat="+"] { --tyt-chat-grow: 1;}\n [tyt-chat="+"] secondary-wrapper>[tyt-chat-container]{flex-grow:var(--tyt-chat-grow);flex-shrink:0;display:flex;flex-direction:column;}\n [tyt-chat="+"] secondary-wrapper>[tyt-chat-container]>#chat{flex-grow:var(--tyt-chat-grow);}\n ytd-watch-flexy[is-two-columns_]:not([theater]):not([full-bleed-player]) #columns.style-scope.ytd-watch-flexy{min-height:calc(100vh - var(--ytd-toolbar-height,56px));}\n ytd-watch-flexy[is-two-columns_]:not([full-bleed-player]) ytd-live-chat-frame#chat{min-height:initial!important;height:initial!important;}\n ytd-watch-flexy[tyt-tab^="#"]:not([is-two-columns_]):not([tyt-chat="+"]) #right-tabs{min-height:var(--ytd-watch-flexy-chat-max-height);}\n body ytd-watch-flexy:not([is-two-columns_]) #chat.ytd-watch-flexy{margin-top:0;}\n body ytd-watch-flexy:not([is-two-columns_]) ytd-watch-metadata.ytd-watch-flexy{margin-bottom:0;}\n ytd-watch-metadata.ytd-watch-flexy ytd-metadata-row-container-renderer{display:none;}\n #tab-info [show-expand-button] #expand-sizer.ytd-text-inline-expander{visibility:initial;}\n #tab-info #collapse.button.ytd-text-inline-expander {display: none;}\n #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;}\n #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;}\n #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;}\n #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;}\n 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;}\n 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;}\n 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;}\n secondary-wrapper #content.ytd-engagement-panel-section-list-renderer{position:relative;}\n secondary-wrapper #content.ytd-engagement-panel-section-list-renderer>[panel-target-id]:only-child{contain:style size;}\n secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-list-renderer.ytd-transcript-search-panel-renderer{flex-grow:1;contain:strict;}\n secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-renderer.style-scope.ytd-transcript-segment-list-renderer{contain:layout paint style;}\n 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;}\n body ytd-watch-flexy[theater] #secondary.ytd-watch-flexy{margin-top:var(--ytd-margin-3x);padding-top:0;}\n body ytd-watch-flexy[theater] secondary-wrapper{margin-top:0;padding-top:0;}\n body ytd-watch-flexy[theater] #chat.ytd-watch-flexy{margin-bottom:var(--ytd-margin-2x);}\n ytd-watch-flexy[theater] #right-tabs .tab-btn[tyt-tab-content]{padding:8px 4px 6px;border-bottom:0 solid transparent;}\n ytd-watch-flexy[theater] #playlist.ytd-watch-flexy{margin-bottom:var(--ytd-margin-2x);}\n ytd-watch-flexy[theater] ytd-playlist-panel-renderer[collapsible][collapsed] .header.ytd-playlist-panel-renderer{padding:6px 8px;}\n #tab-comments ytd-comments#comments [field-of-cm-count]{margin-top:0;}\n #tab-info>ytd-expandable-video-description-body-renderer{margin-bottom:var(--ytd-margin-3x);}\n #tab-info [class]:last-child{margin-bottom:0;padding-bottom:0;}\n #tab-info ytd-rich-metadata-row-renderer ytd-rich-metadata-renderer{max-width:initial;}\n ytd-watch-flexy[is-two-columns_] secondary-wrapper #chat.ytd-watch-flexy{margin-bottom:var(--ytd-margin-3x);}\n ytd-watch-flexy[tyt-tab] tp-yt-paper-tooltip{white-space:nowrap;contain:content;}\n ytd-watch-info-text tp-yt-paper-tooltip.style-scope.ytd-watch-info-text{margin-bottom:-300px;margin-top:-96px;}\n [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata{font-size:1.2rem;line-height:1.8rem;}\n [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata yt-animated-rolling-number{font-size:inherit;}\n [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata #info-container.style-scope.ytd-watch-info-text{align-items:center;}\n ytd-watch-flexy[hide-default-text-inline-expander]{--tyt-bottom-watch-metadata-margin:6px;}\n [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata>#description-inner.ytd-watch-metadata{margin:6px 12px;}\n [hide-default-text-inline-expander] ytd-watch-metadata[title-headline-xs] h1.ytd-watch-metadata{font-size:1.8rem;}\n 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;}\n 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;}\n #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;}\n #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;}\n #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;}\n #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model--image-large .yt-video-attribute-view-model__hero-section{max-width:128px;}\n #tab-info ytd-reel-shelf-renderer #items.yt-horizontal-list-renderer ytd-reel-item-renderer.yt-horizontal-list-renderer{max-width:142px;}\n 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;}\n ytd-watch-info-text:not([detailed]) #info.ytd-watch-info-text a.yt-simple-endpoint.yt-formatted-string{pointer-events:none;}\n body ytd-app>ytd-popup-container>tp-yt-iron-dropdown>#contentWrapper>[slot="dropdown-content"]{backdrop-filter:none;}\n #tab-info [tyt-clone-refresh-count]{overflow:visible!important;}\n #tab-info #items.ytd-horizontal-card-list-renderer yt-video-attribute-view-model.ytd-horizontal-card-list-renderer{contain:layout;}\n #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;}\n 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);}\n [tyt-no-less-btn] #less{display:none;}\n .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;}\n .tyt-metadata-hover #upload-info{max-width:max-content;min-width:max-content;flex-basis:100vw;flex-shrink:0;}\n .tyt-info-invisible{display:none;}\n [tyt-playlist-expanded] secondary-wrapper>ytd-playlist-panel-renderer#playlist{overflow:auto;flex-shrink:1;flex-grow:1;max-height:unset!important;}\n [tyt-playlist-expanded] secondary-wrapper>ytd-playlist-panel-renderer#playlist>#container{max-height:unset!important;}\n secondary-wrapper ytd-playlist-panel-renderer{--ytd-margin-6x:var(--ytd-margin-3x);}\n #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #playlist-thumbnail.style-scope.ytd-structured-description-playlist-lockup-renderer{max-width:100%;}\n #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #lockup-container.ytd-structured-description-playlist-lockup-renderer{padding:1px;}\n #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #thumbnail.ytd-structured-description-playlist-lockup-renderer{outline:1px solid rgba(127,127,127,.5);}\n 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;}\n ytd-watch-flexy{--tyt-bottom-watch-metadata-margin:12px;}\n ytd-watch-flexy[rounded-info-panel],ytd-watch-flexy[rounded-player-large]{--tyt-rounded-a1:12px;}\n #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);}\n #cinematics{contain:layout style size;}\n ytd-watch-flexy[is-two-columns_]{contain:layout style;}\n .yt-spec-touch-feedback-shape--touch-response .yt-spec-touch-feedback-shape__fill{background-color:transparent;}\n /* plugin: external.ytlstm */\n body[data-ytlstm-theater-mode] #secondary-inner[class] > secondary-wrapper[class]:not(#chat-container):not(#chat) {display: flex !important;} \n body[data-ytlstm-theater-mode] secondary-wrapper {all: unset;height: 100vh;}\n body[data-ytlstm-theater-mode] #right-tabs {display: none;}\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] [tyt-chat="+"] {--tyt-chat-grow: unset;}\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #columns.style-scope.ytd-watch-flexy,\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #secondary.style-scope.ytd-watch-flexy,\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #secondary-inner.style-scope.ytd-watch-flexy,\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] secondary-wrapper,\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #chat-container.style-scope,\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] [tyt-chat-container].style-scope {pointer-events: none;}\n body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #chat[class] {pointer-events: auto;}\n .playlist-items.ytd-playlist-panel-renderer {background-color: transparent !important;}\n @supports (color: var(--tyt-fix-20251124)) { #below ytd-watch-metadata .ytTextCarouselItemViewModelImageType { height: 16px; width: 16px;}\n #below ytd-watch-metadata yt-text-carousel-item-view-model { column-gap: 6px;}\n #below ytd-watch-metadata ytd-watch-info-text#ytd-watch-info-text { font-size: inherit; line-height: inherit;}\n /* Fix: video tab thumbnails (yt-lockup-view-model) too large in side panel */\n #tab-videos yt-lockup-view-model{max-width:100%;contain:layout paint;}\n #tab-videos yt-lockup-view-model .yt-lockup-view-model__content-image,#tab-videos yt-lockup-view-model .yt-lockup-view-model__content-image img,#tab-videos yt-lockup-view-model .yt-lockup-view-model__content-image yt-image{max-width:175px;max-height:94px;width:175px;height:auto;object-fit:cover;border-radius:8px;flex-shrink:0;}\n #tab-videos yt-lockup-view-model .yt-lockup-view-model--horizontal{display:flex;gap:8px;align-items:flex-start;}\n #tab-videos yt-lockup-view-model .yt-lockup-view-model--horizontal .yt-lockup-view-model__content-image{flex-shrink:0;width:175px;}\n #tab-videos yt-lockup-view-model .yt-lockup-view-model--horizontal .yt-lockup-view-model__metadata{flex:1;min-width:0;overflow:hidden;}\n #tab-videos ytd-video-renderer[use-search-ui] #thumbnail.ytd-video-renderer,#tab-videos ytd-compact-video-renderer #thumbnail{max-width:175px;width:175px;flex-shrink:0;}\n /* ── LCP Performance: safe content-visibility hints (no contain:layout to preserve sticky) ── */\n ytd-browse[page-subtype="home"] #contents.ytd-rich-grid-renderer>ytd-rich-item-renderer:nth-child(n+9){content-visibility:auto;contain-intrinsic-size:auto 360px;}\n ytd-playlist-video-list-renderer #contents>ytd-playlist-video-renderer:nth-child(n+10){content-visibility:auto;contain-intrinsic-size:auto 90px;}\n ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer:nth-child(n+5){content-visibility:auto;contain-intrinsic-size:auto 94px;}\n /* ── CLS Fix: reserve thumbnail space to prevent layout shifts (home, playlist, search) ── */\n ytd-thumbnail,ytd-thumbnail a.ytd-thumbnail{display:block;aspect-ratio:16/9;contain:layout paint;}\n ytd-browse[page-subtype="home"] ytd-thumbnail,ytd-browse[page-subtype="home"] ytd-thumbnail a.ytd-thumbnail{aspect-ratio:16/9;}\n ytd-playlist-video-renderer ytd-thumbnail,ytd-playlist-video-renderer ytd-thumbnail a.ytd-thumbnail{aspect-ratio:16/9;}\n ytd-compact-video-renderer ytd-thumbnail,ytd-compact-video-renderer ytd-thumbnail a.ytd-thumbnail{aspect-ratio:16/9;}\n ytd-rich-item-renderer ytd-thumbnail yt-image{aspect-ratio:16/9;width:100%;height:auto;display:block;}\n /* ── CLS Fix: stabilise rich-grid rows so columns don\'t shift as items load ── */\n ytd-browse[page-subtype="home"] ytd-rich-grid-row{contain:layout size;}\n ytd-browse[page-subtype="home"] ytd-rich-item-renderer{contain:layout paint;min-height:280px;}\n /* ── Playlist page: reserve row height to prevent CLS on item load ── */\n ytd-playlist-video-renderer{contain:layout paint;min-height:90px;}\n ytd-playlist-panel-video-renderer{contain:layout paint;min-height:72px;}\n /* ── Video page sidebar: stabilise compact-video rows ── */\n ytd-compact-video-renderer{contain:layout paint;min-height:92px;}\n /* ── Performance: GPU-composite the fixed/sticky nav to avoid repaint ── */\n #masthead-container{will-change:transform;}\n ' }; (async () => { var nextBrowserTick = void 0 !== nextBrowserTick && nextBrowserTick.version >= 2 ? nextBrowserTick : (() => { "use strict"; const e = "undefined" != typeof globalThis ? globalThis : "undefined" != typeof window ? window : this; let t = !0; if (!(function n(s) { return s ? t = !1 : e.postMessage && !e.importScripts && e.addEventListener ? (e.addEventListener("message", n, !1), e.postMessage("$$$", "*"), e.removeEventListener("message", n, !1), t) : void 0; })()) { return void console.warn("Your browser environment cannot use nextBrowserTick"); } const n = (async () => {}).constructor; let s = null; const o = new Map, {floor: r, random: i} = Math; let l; do { l = `$$nextBrowserTick$$${(i() + 8).toString().slice(2)}$$`; } while (l in e); const a = l, c = a.length + 9; e[a] = 1; e.addEventListener("message", e => { if (0 !== o.size) { const t = (e || 0).data; if ("string" == typeof t && t.length === c && e.source === (e.target || 1)) { const e = o.get(t); e && ("p" === t[0] && (s = null), o.delete(t), e()); } } }, !1); const d = (t = o) => { if (t === o) { if (s) { return s; } let t; do { t = `p${a}${r(314159265359 * i() + 314159265359).toString(36)}`; } while (o.has(t)); return s = new n(e => { o.set(t, e); }), e.postMessage(t, "*"), t = null, s; } { let n; do { n = `f${a}${r(314159265359 * i() + 314159265359).toString(36)}`; } while (o.has(n)); o.set(n, t), e.postMessage(n, "*"); } }; return d.version = 2, d; })(); const communicationKey = `ck-${Date.now()}-${Math.floor(314159265359 * Math.random() + 314159265359).toString(36)}`; const Promise = (async () => {})().constructor; if (!document.documentElement) { await Promise.resolve(0); for (;!document.documentElement; ) { await new Promise(resolve => nextBrowserTick(resolve)).then().catch(console.warn); } } const textContent = `(${executionScript})("${communicationKey}");\n\n//# sourceURL=debug://tabview-youtube/tabview.execution.js\n`; let executionInjected = !1; try { const script = document.createElement("script"); const nonceSource = document.querySelector("script[nonce]") || document.querySelector("[nonce]") || document.documentElement; const nonce = nonceSource?.nonce || nonceSource?.getAttribute?.("nonce") || document.documentElement?.getAttribute?.("nonce") || ""; nonce && script.setAttribute("nonce", nonce); script.textContent = textContent; (document.head || document.documentElement).appendChild(script); script.remove(); executionInjected = !0; } catch (e) { console.warn("[YouTube+] Script nonce injection failed, falling back:", e); } if (!executionInjected) { try { const blob = new globalThis.Blob([ textContent ], { type: "text/javascript" }); const blobUrl = globalThis.URL.createObjectURL(blob); const script = document.createElement("script"); script.src = blobUrl; script.onload = script.onerror = () => { try { globalThis.URL.revokeObjectURL(blobUrl); } catch {} try { script.remove(); } catch {} }; (document.head || document.documentElement).appendChild(script); executionInjected = !0; } catch (e) { console.error("[YouTube+] Failed to inject execution script", e); throw e; } } const style = document.createElement("style"); const cssContent = `${styles.main.trim()}\n\n/*# sourceURL=debug://tabview-youtube/tabview.main.css */\n`; const gmAddStyle = "undefined" != typeof window && window.GM_addStyle || null; if ("function" == typeof gmAddStyle) { gmAddStyle(cssContent); } else { style.textContent = cssContent; document.documentElement.appendChild(style); } const scheduleTabviewI18nTabs = () => { let attempts = 0; const tryApply = () => { if (!(() => { const container = document.querySelector("#right-tabs"); if (!container) { return !1; } const i18n = "undefined" != typeof window ? window.YouTubePlusI18n : null; const translate = (key, fallback) => { if (i18n && "function" == typeof i18n.t) { const value = i18n.t(key); if (value && value !== key) { return value; } } return fallback; }; const labels = [ { selector: "#tab-btn1 span", key: "info", fallback: "Info" }, { selector: "#tab-btn4 span", key: "videos", fallback: "Videos" }, { selector: "#tab-btn5 span", key: "playlist", fallback: "Playlist" } ]; for (const {selector, key, fallback} of labels) { const label = container.querySelector(selector); label && (label.textContent = translate(key, fallback)); } return !0; })() && attempts < 20) { attempts += 1; setTimeout(tryApply, 250); } }; tryApply(); }; const refreshTabviewI18n = () => { (() => { const root = document.documentElement; if (!root) { return; } const i18n = "undefined" != typeof window ? window.YouTubePlusI18n : null; const translate = (key, fallback) => { if (i18n && "function" == typeof i18n.t) { const value = i18n.t(key); if (value && value !== key) { return value; } } return fallback; }; const toCssString = value => { const text = String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"'); return `"${text}"`; }; root.style.setProperty("--tabview-text-loading", toCssString(translate("loading", "Loading"))); root.style.setProperty("--tabview-text-fetching", toCssString(translate("fetching", "Fetching"))); })(); scheduleTabviewI18nTabs(); }; let tabviewI18nListenerBound = !1; if ("undefined" != typeof window) { if (YouTubeUtils?.cleanupManager?.registerListener) { YouTubeUtils.cleanupManager.registerListener(window, "youtube-plus-i18n-ready", refreshTabviewI18n, { passive: !0 }); YouTubeUtils.cleanupManager.registerListener(window, "youtube-plus-language-changed", refreshTabviewI18n, { passive: !0 }); } else { window.addEventListener("youtube-plus-i18n-ready", refreshTabviewI18n, { passive: !0 }); window.addEventListener("youtube-plus-language-changed", refreshTabviewI18n, { passive: !0 }); } } (() => { let attempts = 0; const tryBind = () => { const i18n = "undefined" != typeof window ? window.YouTubePlusI18n : null; if (i18n && "function" == typeof i18n.t) { refreshTabviewI18n(); if (!tabviewI18nListenerBound && "function" == typeof i18n.onLanguageChange) { i18n.onLanguageChange(refreshTabviewI18n); tabviewI18nListenerBound = !0; } } else if (attempts < 120) { attempts += 1; setTimeout(tryBind, 500); } }; tryBind(); })(); scheduleTabviewI18nTabs(); if ("undefined" != typeof window && window.YouTubePlusLazyLoader) { const {loadOnIdle} = window.YouTubePlusLazyLoader; loadOnIdle(2e3); } !(function injectPerformanceHints() { try { const preconnectOrigins = [ "https://i.ytimg.com", "https://yt3.ggpht.com", "https://yt3.googleusercontent.com" ]; preconnectOrigins.forEach(origin => { if (!document.querySelector(`link[rel="preconnect"][href="${origin}"]`)) { const link = document.createElement("link"); link.rel = "preconnect"; link.href = origin; link.crossOrigin = "anonymous"; document.head.appendChild(link); } }); const promoteLCPImage = () => { try { const firstThumb = document.querySelector("ytd-rich-item-renderer ytd-thumbnail img, ytd-playlist-video-renderer ytd-thumbnail img, ytd-video-renderer ytd-thumbnail img"); if (firstThumb && !firstThumb._ytpLCPPromoted) { firstThumb.fetchPriority = "high"; firstThumb.loading = "eager"; firstThumb.decoding = "sync"; firstThumb._ytpLCPPromoted = !0; } const allThumbs = document.querySelectorAll("ytd-rich-item-renderer ytd-thumbnail img, ytd-playlist-video-renderer ytd-thumbnail img, ytd-compact-video-renderer ytd-thumbnail img"); for (let i = 1; i < allThumbs.length; i++) { const img = allThumbs[i]; if (!img._ytpDecoding) { img.decoding = "async"; img._ytpDecoding = !0; } } } catch {} }; window.addEventListener("yt-navigate-finish", promoteLCPImage, { passive: !0 }); "loading" !== document.readyState ? promoteLCPImage() : document.addEventListener("DOMContentLoaded", promoteLCPImage, { once: !0 }); } catch {} })(); })(); /** * YouTube+ Internationalization (i18n) System - v3.2 * Unified i18n system with integrated loader * Supports all major YouTube interface languages * @module i18n * @version 3.2 */ !(function() { "use strict"; const GITHUB_CONFIG_owner = "diorhc", GITHUB_CONFIG_repo = "YTP", GITHUB_CONFIG_branch = "main", GITHUB_CONFIG_basePath = "locales"; const CDN_URLS = { github: `https://raw.githubusercontent.com/${GITHUB_CONFIG_owner}/${GITHUB_CONFIG_repo}/${GITHUB_CONFIG_branch}/${GITHUB_CONFIG_basePath}`, jsdelivr: `https://cdn.jsdelivr.net/gh/${GITHUB_CONFIG_owner}/${GITHUB_CONFIG_repo}@${GITHUB_CONFIG_branch}/${GITHUB_CONFIG_basePath}` }; const AVAILABLE_LANGUAGES = [ "en", "ru", "kr", "fr", "du", "cn", "tw", "jp", "tr", "es", "pt", "de", "it", "pl", "uk", "ar", "hi", "id", "vi", "uz", "kk", "ky", "be", "bg", "az" ]; const LANGUAGE_NAMES = { en: "English", ru: "???????", kr: "???", fr: "Fran�ais", du: "Nederlands", cn: "????", tw: "????", jp: "???", tr: "T�rk�e", es: "Espa�ol", pt: "Portugu�s", de: "Deutsch", it: "Italiano", pl: "Polski", uk: "??????????", sv: "Svenska", no: "Norsk", da: "Dansk", fi: "Suomi", cs: "Ce�tina", sk: "Slovencina", hu: "Magyar", ro: "Rom�na", bg: "?????????", hr: "Hrvatski", sr: "??????", sl: "Sloven�cina", el: "????????", lt: "Lietuviu", lv: "Latvie�u", et: "Eesti", mk: "??????????", sq: "Shqip", bs: "Bosanski", is: "�slenska", ca: "Catal�", eu: "Euskara", gl: "Galego", ar: "???????", he: "?????", fa: "?????", sw: "Kiswahili", zu: "isiZulu", af: "Afrikaans", am: "????", hi: "??????", th: "???", vi: "Ti?ng Vi?t", id: "Bahasa Indonesia", ms: "Bahasa Melayu", bn: "?????", ta: "?????", te: "??????", mr: "?????", gu: "???????", kn: "?????", ml: "??????", pa: "??????", fil: "Filipino", km: "?????????", lo: "???", my: "??????", ne: "??????", si: "?????", az: "Az?rbaycanca", be: "??????????", hy: "???????", ka: "???????", kk: "?????", ky: "????????", mn: "??????", tg: "??????", uz: "O?zbekcha" }; const LANGUAGE_FALLBACKS = { es: "es", "es-es": "es", "es-mx": "es", "es-419": "es", pt: "pt", "pt-br": "pt", "pt-pt": "pt", de: "de", "de-de": "de", "de-at": "de", "de-ch": "de", it: "it", pl: "pl", uk: "uk", "uk-ua": "uk", ar: "ar", "ar-sa": "ar", "ar-ae": "ar", "ar-eg": "ar", hi: "hi", "hi-in": "hi", th: "en", "th-th": "en", vi: "vi", "vi-vn": "vi", id: "id", "id-id": "id", ms: "en", "ms-my": "en", sv: "en", "sv-se": "en", no: "en", "nb-no": "en", "nn-no": "en", da: "en", "da-dk": "en", fi: "en", "fi-fi": "en", cs: "en", "cs-cz": "en", sk: "en", "sk-sk": "en", hu: "en", "hu-hu": "en", ro: "en", "ro-ro": "en", bg: "bg", "bg-bg": "bg", hr: "en", "hr-hr": "en", sr: "ru", "sr-rs": "ru", sl: "en", "sl-si": "en", el: "en", "el-gr": "en", he: "en", "he-il": "en", iw: "en", fa: "en", "fa-ir": "en", bn: "en", "bn-in": "en", ta: "en", "ta-in": "en", te: "en", "te-in": "en", mr: "en", "mr-in": "en", gu: "en", "gu-in": "en", kn: "en", "kn-in": "en", ml: "en", "ml-in": "en", pa: "en", "pa-in": "en", fil: "en", "fil-ph": "en", tl: "en", km: "en", lo: "en", my: "en", ne: "en", si: "en", sw: "en", "sw-ke": "en", zu: "en", af: "en", am: "en", az: "az", "az-az": "az", be: "be", "be-by": "be", hy: "ru", ka: "en", kk: "kk", "kk-kz": "kk", ky: "ky", mn: "ru", tg: "ru", uz: "uz", "uz-uz": "uz", lt: "en", "lt-lt": "en", lv: "en", "lv-lv": "en", et: "en", "et-ee": "en", mk: "ru", sq: "en", bs: "en", is: "en", ca: "es", eu: "es", gl: "es" }; const translationsCache = new Map; const loadingPromises = new Map; function loadTranslationsFromLoader(lang) { const languageCode = AVAILABLE_LANGUAGES.includes(lang) ? lang : "en"; if (translationsCache.has(languageCode)) { return translationsCache.get(languageCode); } if (loadingPromises.has(languageCode)) { return loadingPromises.get(languageCode); } const loadPromise = (async () => { try { const translations = await (async function fetchTranslation(lang) { try { if ("undefined" != typeof window && window.YouTubePlusEmbeddedTranslations) { const embedded = window.YouTubePlusEmbeddedTranslations[lang]; if (embedded) { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+][i18n]", `Using embedded translations for ${lang}`); return embedded; } } } catch (e) { console.warn("[YouTube+][i18n]", "Error reading embedded translations", e); } try { const rawUrl = `${CDN_URLS.github}/${lang}.json`; const response = await fetch(rawUrl, { cache: "default", headers: { Accept: "application/json" } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (firstErr) { try { const cdnUrl = `${CDN_URLS.jsdelivr}/${lang}.json`; console.warn("[YouTube+][i18n]", `Raw GitHub fetch failed, trying jsDelivr: ${cdnUrl}`); const response = await fetch(cdnUrl, { cache: "default", headers: { Accept: "application/json" } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (err) { console.error("[YouTube+][i18n]", `Failed to fetch translations for ${lang}:`, err, firstErr); throw err; } } })(languageCode); try { const missing = []; [ "loading", "fetching" ].forEach(k => { Object.prototype.hasOwnProperty.call(translations, k) || missing.push(k); }); missing.length > 0 && console.warn("[YouTube+][i18n]", `Translations for ${languageCode} missing keys: ${missing.join(", ")} (source may be stale)`); } catch {} translationsCache.set(languageCode, translations); loadingPromises.delete(languageCode); return translations; } catch (error) { loadingPromises.delete(languageCode); if ("en" !== languageCode) { return loadTranslationsFromLoader("en"); } throw error; } })(); loadingPromises.set(languageCode, loadPromise); return loadPromise; } let currentLanguage = "en"; let translations = {}; let fallbackTranslationsEn = {}; const translationCache = new Map; const languageChangeListeners = new Set; let loadingPromise = null; function emitI18nEvent(name, detail = {}) { try { if ("undefined" == typeof window) { return; } window.dispatchEvent(new CustomEvent(name, { detail })); } catch { try { if ("undefined" == typeof window) { return; } window.dispatchEvent(new Event(name)); } catch {} } } const languageMap = { ko: "kr", "ko-kr": "kr", fr: "fr", "fr-fr": "fr", "fr-ca": "fr", "fr-be": "fr", "fr-ch": "fr", nl: "du", "nl-nl": "du", "nl-be": "du", zh: "cn", "zh-cn": "cn", "zh-hans": "cn", "zh-sg": "cn", "zh-tw": "tw", "zh-hk": "tw", "zh-hant": "tw", ja: "jp", "ja-jp": "jp", tr: "tr", "tr-tr": "tr", ru: "ru", "ru-ru": "ru", en: "en", "en-us": "en", "en-gb": "en", "en-au": "en", "en-ca": "en", "en-in": "en", ...Object.fromEntries(Object.entries(LANGUAGE_FALLBACKS).map(([key, fallback]) => [ key, fallback ])) }; function mapToSupportedLanguage(langCode) { const lower = langCode.toLowerCase(); if (languageMap[lower]) { return languageMap[lower]; } if (AVAILABLE_LANGUAGES.includes(lower)) { return lower; } const shortCode = lower.substr(0, 2); return languageMap[shortCode] ? languageMap[shortCode] : AVAILABLE_LANGUAGES.includes(shortCode) ? shortCode : LANGUAGE_FALLBACKS[lower] ? LANGUAGE_FALLBACKS[lower] : LANGUAGE_FALLBACKS[shortCode] ? LANGUAGE_FALLBACKS[shortCode] : "en"; } function detectLanguage() { try { const ytLang = document.documentElement.lang || document.querySelector("html")?.getAttribute("lang"); if (ytLang) { const mapped = mapToSupportedLanguage(ytLang); return mapped; } try { const urlParams = new URLSearchParams(window.location.search); const hlParam = urlParams.get("hl"); if (hlParam) { const mapped = mapToSupportedLanguage(hlParam); return mapped; } } catch {} try { const ytConfig = window.ytcfg || window.yt?.config_; if (ytConfig && "function" == typeof ytConfig.get) { const hl = ytConfig.get("HL") || ytConfig.get("GAPI_LOCALE"); if (hl) { const mapped = mapToSupportedLanguage(hl); return mapped; } } } catch {} const browserLang = navigator.language || navigator.userLanguage || "en"; const mapped = mapToSupportedLanguage(browserLang); return mapped; } catch (error) { console.error("[YouTube+][i18n]", "Error detecting language:", error); return "en"; } } async function loadTranslations() { if (loadingPromise) { await loadingPromise; return !0; } loadingPromise = (async () => { try { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+][i18n]", `Loading translations for ${currentLanguage}...`); translations = await loadTranslationsFromLoader(currentLanguage); if (!fallbackTranslationsEn || 0 === Object.keys(fallbackTranslationsEn).length) { try { const embeddedEn = "undefined" != typeof window && window.YouTubePlusEmbeddedTranslations && window.YouTubePlusEmbeddedTranslations.en; fallbackTranslationsEn = embeddedEn && "object" == typeof embeddedEn ? embeddedEn : await loadTranslationsFromLoader("en"); } catch { fallbackTranslationsEn = {}; } } translationCache.clear(); window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+][i18n]", `? Loaded ${Object.keys(translations).length} translations for ${currentLanguage}`); return !0; } catch (error) { console.error("[YouTube+][i18n]", "Failed to load translations:", error); if ("en" !== currentLanguage) { currentLanguage = "en"; return loadTranslations(); } return !1; } finally { loadingPromise = null; } })(); return loadingPromise; } function translate(key, params = {}) { const cacheKey = `${key}:${JSON.stringify(params)}`; if (translationCache.has(cacheKey)) { return translationCache.get(cacheKey); } let text = translations[key]; if (!text) { const enText = fallbackTranslationsEn ? fallbackTranslationsEn[key] : void 0; if (enText) { text = enText; } else { Object.keys(translations).length > 0 && console.warn("[YouTube+][i18n]", `Missing translation for key: ${key}`); text = key; } } if (Object.keys(params).length > 0) { const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); Object.keys(params).forEach(param => { text = text.replace(new RegExp(`\\{${escapeRegex(param)}\\}`, "g"), params[param]); }); } translationCache.set(cacheKey, text); return text; } function getLanguage() { return currentLanguage; } function getAvailableLanguages() { return AVAILABLE_LANGUAGES; } async function initialize() { try { currentLanguage = detectLanguage(); window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+][i18n]", `Detected language: ${currentLanguage} (${LANGUAGE_NAMES[currentLanguage] || currentLanguage})`); await loadTranslations(); emitI18nEvent("youtube-plus-i18n-ready", { language: currentLanguage }); } catch (error) { console.error("[YouTube+][i18n]", "Initialization error:", error); currentLanguage = "en"; } } const i18nAPI = { t: translate, translate, getLanguage, setLanguage: async function setLanguage(lang) { if (lang === currentLanguage) { return !0; } const oldLang = currentLanguage; currentLanguage = lang; try { const success = await loadTranslations(); if (success) { languageChangeListeners.forEach(listener => { try { listener(currentLanguage, oldLang); } catch (error) { console.error("[YouTube+][i18n]", "Error in language change listener:", error); } }); emitI18nEvent("youtube-plus-language-changed", { language: currentLanguage, previousLanguage: oldLang }); } return success; } catch (error) { console.error("[YouTube+][i18n]", "Failed to change language:", error); currentLanguage = oldLang; return !1; } }, detectLanguage, getAllTranslations: function getAllTranslations() { return { ...translations }; }, getAvailableLanguages, hasTranslation: function hasTranslation(key) { return void 0 !== translations[key]; }, addTranslation: function addTranslation(key, value) { translations[key] = value; translationCache.clear(); }, addTranslations: function addTranslations(newTranslations) { Object.assign(translations, newTranslations); translationCache.clear(); }, onLanguageChange: function onLanguageChange(callback) { languageChangeListeners.add(callback); return () => languageChangeListeners.delete(callback); }, formatNumber: function formatNumber(num, options = {}) { try { const lang = getLanguage(); const localeMap = { ru: "ru-RU", kr: "ko-KR", fr: "fr-FR", du: "nl-NL", cn: "zh-CN", tw: "zh-TW", jp: "ja-JP", tr: "tr-TR" }; const locale = localeMap[lang] || "en-US"; return new Intl.NumberFormat(locale, options).format(num); } catch (error) { console.error("[YouTube+][i18n]", "Error formatting number:", error); return String(num); } }, formatDate: function formatDate(date, options = {}) { try { const lang = getLanguage(); const localeMap = { ru: "ru-RU", kr: "ko-KR", fr: "fr-FR", du: "nl-NL", cn: "zh-CN", tw: "zh-TW", jp: "ja-JP", tr: "tr-TR" }; const locale = localeMap[lang] || "en-US"; const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(locale, options).format(dateObj); } catch (error) { console.error("[YouTube+][i18n]", "Error formatting date:", error); return String(date); } }, pluralize: function pluralize(count, singular, plural, few = null) { const lang = getLanguage(); if ("ru" === lang && few) { const mod10 = count % 10; const mod100 = count % 100; return 1 === mod10 && 11 !== mod100 ? singular : mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20) ? few : plural; } return 1 === count ? singular : plural; }, clearCache: function clearCache() { translationCache.clear(); }, getCacheStats: function getCacheStats() { return { size: translationCache.size, currentLanguage, availableLanguages: getAvailableLanguages(), translationsLoaded: Object.keys(translations).length }; }, loadTranslations, initialize }; if ("undefined" != typeof window) { window.YouTubePlusI18n = i18nAPI; window.YouTubePlusI18nLoader = { loadTranslations: loadTranslationsFromLoader, AVAILABLE_LANGUAGES, LANGUAGE_NAMES, CDN_URLS }; if (window.YouTubeUtils) { window.YouTubeUtils.i18n = i18nAPI; window.YouTubeUtils.t = translate; window.YouTubeUtils.getLanguage = getLanguage; } } "undefined" != typeof module && module.exports && (module.exports = i18nAPI); initialize().then(() => { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+][i18n]", "i18n system initialized successfully"); }); })(); !(function() { "use strict"; function createNavItem(section, label, icon, active = !1) { const activeClass = active ? " active" : ""; return `\n <div class="ytp-plus-settings-nav-item${activeClass}" data-section="${section}">\n ${icon}\n ${label}\n </div>\n `; } function createSettingsItem(label, description, setting, checked) { const inputId = `ytp-plus-setting-${setting}`; return `\n <div class="ytp-plus-settings-item">\n <div>\n <label class="ytp-plus-settings-item-label" for="${inputId}">${label}</label>\n <div class="ytp-plus-settings-item-description">${description}</div>\n </div>\n <input type="checkbox" id="${inputId}" class="ytp-plus-settings-checkbox" data-setting="${setting}" ${checked ? "checked" : ""}>\n </div>\n `; } function createDownloadSiteOption(site) { const {key, name, description, checked, hasControls, controls} = site; const inputId = `download-site-${key}`; return `\n <div class="download-site-option">\n <div class="download-site-header">\n <label for="${inputId}" class="download-site-label">\n <div class="download-site-name">${name}</div>\n <div class="download-site-desc">${description}</div>\n </label>\n <input type="checkbox" id="${inputId}" class="ytp-plus-settings-checkbox" data-setting="downloadSite_${key}" ${checked ? "checked" : ""}>\n </div>\n ${hasControls ? `<div class="download-site-controls" style="display:${checked ? "block" : "none"};">${controls}</div>` : ""}\n </div>\n `; } function createExternalDownloaderControls(customization, t) { const name = customization?.name || "SSYouTube"; const url = customization?.url || "https://ssyoutube.com/watch?v={videoId}"; return `\n <input type="text" placeholder="${t("siteName")}" value="${name}" \n data-site="externalDownloader" data-field="name" class="download-site-input">\n <input type="text" placeholder="${t("urlTemplate")}" value="${url}" \n data-site="externalDownloader" data-field="url" class="download-site-input small">\n <div class="download-site-cta">\n <button class="glass-button" id="download-externalDownloader-save">${t("saveButton")}</button>\n <button class="glass-button danger" id="download-externalDownloader-reset">${t("resetButton")}</button>\n </div>\n `; } function tr(t, key, fallback) { try { const v = t(key); if ("string" == typeof v && v && v !== key) { return v; } } catch {} return fallback; } function createBasicSettingsSection(settings, t) { const downloadEnabled = !!settings.enableDownload; const styleEnabled = !1 !== settings.enableZenStyles; const speedEnabled = !!settings.enableSpeedControl; return `\n <div class="ytp-plus-settings-section" data-section="basic">\n <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">\n <div>\n <label class="ytp-plus-settings-item-label" for="ytp-plus-setting-enableZenStyles">${tr(t, "zenStylesTitle", "Zen styles")}</label>\n <div class="ytp-plus-settings-item-description">${tr(t, "zenStylesDesc", "Optional UI tweaks and cosmetic improvements")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <button\n type="button"\n class="ytp-plus-submenu-toggle"\n data-submenu="style"\n aria-label="Toggle styles submenu"\n aria-expanded="${styleEnabled ? "true" : "false"}"\n ${styleEnabled ? "" : "disabled"}\n style="display:${styleEnabled ? "inline-flex" : "none"};"\n >\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="6 9 12 15 18 9"></polyline>\n </svg>\n </button>\n <input type="checkbox" id="ytp-plus-setting-enableZenStyles" class="ytp-plus-settings-checkbox" data-setting="enableZenStyles" ${styleEnabled ? "checked" : ""}>\n </div>\n </div>\n ${(function createStyleSubmenu(settings, t) { const display = settings.enableZenStyles ? "block" : "none"; const rows = [ { label: tr(t, "zenStyleThumbnailHoverLabel", "Thumbnail hover preview"), desc: tr(t, "zenStyleThumbnailHoverDesc", "Enlarge inline preview player on hover"), key: "zenStyles.thumbnailHover", value: settings.zenStyles?.thumbnailHover }, { label: tr(t, "zenStyleImmersiveSearchLabel", "Immersive search"), desc: tr(t, "zenStyleImmersiveSearchDesc", "Centered searchbox experience when focused"), key: "zenStyles.immersiveSearch", value: settings.zenStyles?.immersiveSearch }, { label: tr(t, "zenStyleHideVoiceSearchLabel", "Hide Voice Search"), desc: tr(t, "zenStyleHideVoiceSearchDesc", "Remove microphone button from the header"), key: "zenStyles.hideVoiceSearch", value: settings.zenStyles?.hideVoiceSearch }, { label: tr(t, "zenStyleTransparentHeaderLabel", "Transparent Header"), desc: tr(t, "zenStyleTransparentHeaderDesc", "Make the top header transparent"), key: "zenStyles.transparentHeader", value: settings.zenStyles?.transparentHeader }, { label: tr(t, "zenStyleHideSideGuideLabel", "Hide Side Guide"), desc: tr(t, "zenStyleHideSideGuideDesc", "Completely hide the sidebar guide"), key: "zenStyles.hideSideGuide", value: settings.zenStyles?.hideSideGuide }, { label: tr(t, "zenStyleCleanSideGuideLabel", "Clean Side Guide"), desc: tr(t, "zenStyleCleanSideGuideDesc", "Remove Premium/Sports/Settings from sidebar"), key: "zenStyles.cleanSideGuide", value: settings.zenStyles?.cleanSideGuide }, { label: tr(t, "zenStyleFixFeedLayoutLabel", "Fix Feed Layout"), desc: tr(t, "zenStyleFixFeedLayoutDesc", "Improve video grid layout on home page"), key: "zenStyles.fixFeedLayout", value: settings.zenStyles?.fixFeedLayout }, { label: tr(t, "zenStyleBetterCaptionsLabel", "Better Captions"), desc: tr(t, "zenStyleBetterCaptionsDesc", "Enhanced subtitle styling with blur backdrop"), key: "zenStyles.betterCaptions", value: settings.zenStyles?.betterCaptions }, { label: tr(t, "zenStylePlayerBlurLabel", "Player Controls Blur"), desc: tr(t, "zenStylePlayerBlurDesc", "Add blur effect to player controls"), key: "zenStyles.playerBlur", value: settings.zenStyles?.playerBlur }, { label: tr(t, "zenStyleTheaterEnhancementsLabel", "Theater Enhancements"), desc: tr(t, "zenStyleTheaterEnhancementsDesc", "Floating comments panel and improved theater mode"), key: "zenStyles.theaterEnhancements", value: settings.zenStyles?.theaterEnhancements }, { label: tr(t, "zenStyleMiscLabel", "Misc Enhancements"), desc: tr(t, "zenStyleMiscDesc", "Compact feed, hover menus, and other minor improvements"), key: "zenStyles.misc", value: settings.zenStyles?.misc } ]; return `\n <div class="style-submenu" data-submenu="style" style="display:${display};">\n <div class="glass-card style-submenu-container">\n ${rows.map(r => createSettingsItem(r.label, r.desc, r.key, r.value)).join("")}\n </div>\n </div>\n `; })(settings, t)}\n <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">\n <div>\n <label class="ytp-plus-settings-item-label" for="ytp-plus-setting-enableSpeedControl">${t("speedControl")}</label>\n <div class="ytp-plus-settings-item-description">${t("speedControlDesc")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <button\n type="button"\n class="ytp-plus-submenu-toggle"\n data-submenu="speed"\n aria-label="Toggle speed submenu"\n aria-expanded="${speedEnabled ? "true" : "false"}"\n ${speedEnabled ? "" : "disabled"}\n style="display:${speedEnabled ? "inline-flex" : "none"};"\n >\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="6 9 12 15 18 9"></polyline>\n </svg>\n </button>\n <input type="checkbox" id="ytp-plus-setting-enableSpeedControl" class="ytp-plus-settings-checkbox" data-setting="enableSpeedControl" ${speedEnabled ? "checked" : ""}>\n </div>\n </div>\n ${(function createSpeedControlSubmenu(settings, t) { const display = settings.enableSpeedControl ? "block" : "none"; const decrease = (settings.speedControlHotkeys?.decrease || "g").slice(0, 1).toLowerCase(); const increase = (settings.speedControlHotkeys?.increase || "h").slice(0, 1).toLowerCase(); const reset = (settings.speedControlHotkeys?.reset || "b").slice(0, 1).toLowerCase(); return `\n <div class="speed-submenu" data-submenu="speed" style="display:${display};">\n <div class="glass-card speed-submenu-container">\n <div class="ytp-plus-settings-item speed-hotkeys-row">\n <div class="speed-hotkeys-info">\n <div class="ytp-plus-settings-item-label">${tr(t, "speedHotkeysTitle", "Keyboard hotkeys")}</div>\n <div class="ytp-plus-settings-item-description">${tr(t, "speedHotkeysDesc", "Use single-letter shortcuts to decrease/increase/reset playback speed")}</div>\n <div class="speed-hotkeys-fields">\n <label class="speed-hotkey-field"> \n <input\n type="text"\n class="speed-hotkey-input"\n data-speed-hotkey="decrease"\n value="${decrease}"\n maxlength="1"\n autocomplete="off"\n spellcheck="false"\n >\n <span>${tr(t, "decreaseSpeedHotkey", "Decrease")}</span>\n </label>\n <label class="speed-hotkey-field"> \n <input\n type="text"\n class="speed-hotkey-input"\n data-speed-hotkey="increase"\n value="${increase}"\n maxlength="1"\n autocomplete="off"\n spellcheck="false"\n >\n <span>${tr(t, "increaseSpeedHotkey", "Increase")}</span>\n </label>\n <label class="speed-hotkey-field"> \n <input\n type="text"\n class="speed-hotkey-input"\n data-speed-hotkey="reset"\n value="${reset}"\n maxlength="1"\n autocomplete="off"\n spellcheck="false"\n >\n <span>${tr(t, "resetButton", "Reset")}</span>\n </label>\n </div>\n </div>\n </div>\n </div>\n </div>\n `; })(settings, t)}\n ${createSettingsItem(t("screenshotButton"), t("screenshotButtonDesc"), "enableScreenshot", settings.enableScreenshot)}\n <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">\n <div>\n <label class="ytp-plus-settings-item-label" for="ytp-plus-setting-enableDownload">${t("downloadButton")}</label>\n <div class="ytp-plus-settings-item-description">${t("downloadButtonDesc")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <button\n type="button"\n class="ytp-plus-submenu-toggle"\n data-submenu="download"\n aria-label="Toggle download submenu"\n aria-expanded="${downloadEnabled ? "true" : "false"}"\n ${downloadEnabled ? "" : "disabled"}\n style="display:${downloadEnabled ? "inline-flex" : "none"};"\n >\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="6 9 12 15 18 9"></polyline>\n </svg>\n </button>\n <input type="checkbox" id="ytp-plus-setting-enableDownload" class="ytp-plus-settings-checkbox" data-setting="enableDownload" ${settings.enableDownload ? "checked" : ""}>\n </div>\n </div>\n ${(function createDownloadSubmenu(settings, t) { const display = settings.enableDownload ? "block" : "none"; const sites = [ { key: "externalDownloader", name: settings.downloadSiteCustomization?.externalDownloader?.name || "SSYouTube", description: t("customDownloader"), checked: settings.downloadSites?.externalDownloader, hasControls: !0, controls: createExternalDownloaderControls(settings.downloadSiteCustomization?.externalDownloader, t) }, { key: "ytdl", name: t("byYTDL"), description: t("customDownload"), checked: settings.downloadSites?.ytdl, hasControls: !0, controls: '\n <div class="download-site-cta one-btn">\n <button class="glass-button" id="open-ytdl-github">\n <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>\n <polyline points="15,3 21,3 21,9"/>\n <line x1="10" y1="14" x2="21" y2="3"/>\n </svg>\n GitHub\n </button>\n </div>\n ' }, { key: "direct", name: t("directDownload"), description: t("directDownloadDesc"), checked: settings.downloadSites?.direct, hasControls: !1 } ]; return `\n <div class="download-submenu" data-submenu="download" style="display:${display};">\n <div class="glass-card download-submenu-container">\n ${sites.map(site => createDownloadSiteOption(site)).join("")}\n </div>\n </div>\n `; })(settings, t)}\n </div>\n `; } function getMusicSettings() { const defaults = { enableMusic: !0, immersiveSearchStyles: !0, hoverStyles: !0, playerSidebarStyles: !0, centeredPlayerStyles: !0, playerBarStyles: !0, centeredPlayerBarStyles: !0, miniPlayerStyles: !0, scrollToTopStyles: !0 }; try { if ("undefined" != typeof GM_getValue) { const stored = GM_getValue("youtube-plus-music-settings", null); if ("string" == typeof stored && stored) { const parsed = JSON.parse(stored); if (parsed && "object" == typeof parsed) { const merged = { ...defaults }; "boolean" == typeof parsed.enableMusic && (merged.enableMusic = parsed.enableMusic); for (const key of Object.keys(defaults)) { "enableMusic" !== key && "boolean" == typeof parsed[key] && (merged[key] = parsed[key]); } "boolean" == typeof parsed.enableImmersiveSearch && (merged.immersiveSearchStyles = parsed.enableImmersiveSearch); "boolean" == typeof parsed.enableSidebarHover && (merged.hoverStyles = parsed.enableSidebarHover); "boolean" == typeof parsed.enableCenteredPlayer && (merged.centeredPlayerStyles = parsed.enableCenteredPlayer); "boolean" == typeof parsed.enableScrollToTop && (merged.scrollToTopStyles = parsed.enableScrollToTop); return merged; } } } } catch {} try { const stored = localStorage.getItem("youtube-plus-music-settings"); if (stored) { const parsed = JSON.parse(stored); if (parsed && "object" == typeof parsed) { const merged = { ...defaults }; "boolean" == typeof parsed.enableMusic && (merged.enableMusic = parsed.enableMusic); for (const key of Object.keys(defaults)) { "enableMusic" !== key && "boolean" == typeof parsed[key] && (merged[key] = parsed[key]); } "boolean" == typeof parsed.enableImmersiveSearch && (merged.immersiveSearchStyles = parsed.enableImmersiveSearch); "boolean" == typeof parsed.enableSidebarHover && (merged.hoverStyles = parsed.enableSidebarHover); "boolean" == typeof parsed.enableCenteredPlayer && (merged.centeredPlayerStyles = parsed.enableCenteredPlayer); "boolean" == typeof parsed.enableScrollToTop && (merged.scrollToTopStyles = parsed.enableScrollToTop); const legacyEnabled = !!(parsed.enableMusicStyles || parsed.enableMusicEnhancements || parsed.enableImmersiveSearch || parsed.enableSidebarHover || parsed.enableCenteredPlayer || parsed.enableScrollToTop); legacyEnabled && "boolean" != typeof parsed.enableMusic && (merged.enableMusic = !0); return merged; } } } catch (e) { console.warn("[YouTube+] Failed to load music settings:", e); } return defaults; } function createAdvancedSettingsSection(settings, t) { const musicSettings = getMusicSettings(); const musicEnabled = !!musicSettings.enableMusic; const enhancedEnabled = !1 !== settings.enableEnhanced; const enhancedSettings = { enablePlayAll: !1 !== settings.enablePlayAll, enableResumeTime: !1 !== settings.enableResumeTime, enableZoom: !1 !== settings.enableZoom, enableThumbnail: !1 !== settings.enableThumbnail, enablePlaylistSearch: !1 !== settings.enablePlaylistSearch, enableScrollToTopButton: !1 !== settings.enableScrollToTopButton }; return `\n <div class="ytp-plus-settings-section hidden" data-section="advanced">\n <div class="ytp-plus-settings-group">\n <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">\n <div>\n <label class="ytp-plus-settings-item-label">${tr(t, "enhancedFeaturesTitle", "Enhanced Features")}</label>\n <div class="ytp-plus-settings-item-description">${tr(t, "enhancedFeaturesDesc", "Additional productivity features and UI enhancements")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <button\n type="button"\n class="ytp-plus-submenu-toggle"\n data-submenu="enhanced"\n aria-label="Toggle enhanced features submenu"\n aria-expanded="${enhancedEnabled ? "true" : "false"}"\n ${enhancedEnabled ? "" : "disabled"}\n style="display:${enhancedEnabled ? "inline-flex" : "none"};"\n >\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="6 9 12 15 18 9"></polyline>\n </svg>\n </button>\n <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enableEnhanced" ${enhancedEnabled ? "checked" : ""}>\n </div>\n </div>\n\n <div class="enhanced-submenu" data-submenu="enhanced" style="display:${enhancedEnabled ? "block" : "none"};margin-left:12px;margin-bottom:12px;">\n <div class="glass-card" style="display:flex;flex-direction:column;gap:8px;">\n <div class="endscreen-settings-slot"></div>\n ${createSettingsItem(tr(t, "enablePlayAllLabel", "Play All Button"), tr(t, "enablePlayAllDesc", "Add Play All button to playlists and channel pages"), "enablePlayAll", enhancedSettings.enablePlayAll)}\n ${createSettingsItem(tr(t, "enableResumeTimeLabel", "Resume Playback"), tr(t, "enableResumeTimeDesc", "Remember video position and offer to resume"), "enableResumeTime", enhancedSettings.enableResumeTime)}\n ${createSettingsItem(tr(t, "enableZoomLabel", "Video Zoom"), tr(t, "enableZoomDesc", "Enable zoom and pan controls for video player"), "enableZoom", enhancedSettings.enableZoom)}\n ${createSettingsItem(tr(t, "thumbnailPreview", "Thumbnail Preview"), tr(t, "thumbnailPreviewDesc", "Add a button to thumbnails/avatars/banners to open the original image"), "enableThumbnail", enhancedSettings.enableThumbnail)}\n ${createSettingsItem(tr(t, "enablePlaylistSearchLabel", "Playlist Search"), tr(t, "enablePlaylistSearchDesc", "Add search functionality to playlist panels"), "enablePlaylistSearch", enhancedSettings.enablePlaylistSearch)}\n ${createSettingsItem(tr(t, "scrollToTopButtonLabel", "Scroll to Top"), tr(t, "scrollToTopButtonDesc", "Show scroll-to-top button on pages"), "enableScrollToTopButton", enhancedSettings.enableScrollToTopButton)}\n <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu" style="margin-top:4px;">\n <div>\n <label class="ytp-plus-settings-item-label">${tr(t, "enableLoopLabel", "Loop")}</label>\n <div class="ytp-plus-settings-item-description">${tr(t, "enableLoopDesc", "Enable looping of videos and custom segments (A → B)")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enableLoop" ${settings.enableLoop ? "checked" : ""}>\n </div>\n </div>\n ${(function createLoopSubmenu(settings, t) { const display = settings.enableLoop ? "block" : "none"; const setPointA = (settings.loopHotkeys?.setPointA || "k").slice(0, 1).toLowerCase(); const setPointB = (settings.loopHotkeys?.setPointB || "l").slice(0, 1).toLowerCase(); const resetPoints = (settings.loopHotkeys?.resetPoints || "o").slice(0, 1).toLowerCase(); return `\n <div class="loop-submenu" data-submenu="loop" style="display:${display};margin:0 0 4px 0;">\n <div class="ytp-plus-settings-item loop-hotkeys-row" style="margin-bottom:0;">\n <div class="loop-hotkeys-info">\n <div class="ytp-plus-settings-item-label">${tr(t, "loopSegmentTitle", "Loop A → B")}</div>\n <div class="ytp-plus-settings-item-description">${tr(t, "loopSegmentDesc", "Repeat a custom segment of the video (A → B)")}</div>\n <div class="loop-hotkeys-fields" style="margin-top:12px;">\n <label class="loop-hotkey-field"> \n <input\n type="text"\n class="loop-hotkey-input"\n data-loop-hotkey="setPointA"\n value="${setPointA}"\n maxlength="1"\n autocomplete="off"\n spellcheck="false"\n >\n <span>${tr(t, "setPointAHotkey", "Set Point A")}</span>\n </label>\n <label class="loop-hotkey-field"> \n <input\n type="text"\n class="loop-hotkey-input"\n data-loop-hotkey="setPointB"\n value="${setPointB}"\n maxlength="1"\n autocomplete="off"\n spellcheck="false"\n >\n <span>${tr(t, "setPointBHotkey", "Set Point B")}</span>\n </label>\n <label class="loop-hotkey-field"> \n <input\n type="text"\n class="loop-hotkey-input"\n data-loop-hotkey="resetPoints"\n value="${resetPoints}"\n maxlength="1"\n autocomplete="off"\n spellcheck="false"\n >\n <span>${tr(t, "resetButton", "Reset")}</span>\n </label>\n </div>\n </div>\n </div>\n </div>\n `; })(settings, t)}\n </div>\n </div>\n\n <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">\n <div>\n <label class="ytp-plus-settings-item-label">${t("youtubeMusicTitle")}</label>\n <div class="ytp-plus-settings-item-description">${t("youtubeMusicDesc")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <button\n type="button"\n class="ytp-plus-submenu-toggle"\n data-submenu="music"\n aria-label="Toggle YouTube Music submenu"\n aria-expanded="${musicEnabled ? "true" : "false"}"\n ${musicEnabled ? "" : "disabled"}\n style="display:${musicEnabled ? "inline-flex" : "none"};"\n >\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="6 9 12 15 18 9"></polyline>\n </svg>\n </button>\n <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enableMusic" ${musicSettings.enableMusic ? "checked" : ""}>\n </div>\n </div>\n\n <div class="music-submenu" data-submenu="music" style="display:${musicEnabled ? "block" : "none"};margin-left:12px;margin-bottom:12px;">\n <div class="glass-card" style="display:flex;flex-direction:column;gap:8px;">\n ${createSettingsItem(t("immersiveSearchLabel"), t("immersiveSearchDesc"), "immersiveSearchStyles", musicSettings.immersiveSearchStyles)}\n ${createSettingsItem(t("sidebarHoverLabel"), t("sidebarHoverDesc"), "hoverStyles", musicSettings.hoverStyles)}\n ${createSettingsItem(t("playerSidebarStylesLabel"), t("playerSidebarStylesDesc"), "playerSidebarStyles", musicSettings.playerSidebarStyles)}\n ${createSettingsItem(t("centeredPlayerLabel"), t("centeredPlayerDesc"), "centeredPlayerStyles", musicSettings.centeredPlayerStyles)}\n ${createSettingsItem(t("playerBarStylesLabel"), t("playerBarStylesDesc"), "playerBarStyles", musicSettings.playerBarStyles)}\n ${createSettingsItem(t("centeredPlayerBarStylesLabel"), t("centeredPlayerBarStylesDesc"), "centeredPlayerBarStyles", musicSettings.centeredPlayerBarStyles)}\n ${createSettingsItem(t("miniPlayerStylesLabel"), t("miniPlayerStylesDesc"), "miniPlayerStyles", musicSettings.miniPlayerStyles)}\n </div>\n </div>\n </div>\n </div>\n `; } function createExperimentalSettingsSection() { return '\n <div class="ytp-plus-settings-section hidden" data-section="experimental"></div>\n '; } function createVotingSection(_settings, t) { return `\n <div class="ytp-plus-settings-section hidden" data-section="voting">\n <div class="ytp-plus-settings-voting-header">\n <h3>${tr(t, "votingTitle", "Feature Requests")}</h3>\n <p class="ytp-plus-settings-voting-desc">${tr(t, "votingDesc", "Vote for features you want to see in YouTube+")}</p>\n </div>\n\n <div class="ytp-plus-voting-preview">\n <div class="ytp-plus-ba-container">\n <div class="ytp-plus-ba-before">\n <img src="https://i.imgur.com/FVW4tdH.jpeg" alt="Before" draggable="false" />\n <span class="ytp-plus-ba-label ytp-plus-ba-label-before">Before</span>\n </div>\n <div class="ytp-plus-ba-after">\n <img src="https://i.imgur.com/ljq1KeL.jpeg" alt="After" draggable="false" />\n <span class="ytp-plus-ba-label ytp-plus-ba-label-after">After</span>\n </div>\n <div class="ytp-plus-ba-divider" role="separator" tabindex="0" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50"></div>\n </div>\n\n <div class="ytp-plus-vote-bar-section" id="ytp-plus-vote-bar-section">\n <div class="ytp-plus-vote-bar-buttons">\n <div class="ytp-plus-vote-bar-track" id="ytp-plus-vote-bar-fill"></div>\n <button class="ytp-plus-vote-bar-btn" id="ytp-plus-vote-bar-up" type="button" aria-label="${tr(t, "like", "Like")}" data-vote="1">\n <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>\n </button>\n <button class="ytp-plus-vote-bar-btn" id="ytp-plus-vote-bar-down" type="button" aria-label="${tr(t, "dislike", "Dislike")}" data-vote="-1">\n <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>\n </button>\n </div>\n <div class="ytp-plus-vote-bar-count" id="ytp-plus-vote-bar-count">0</div>\n </div>\n </div>\n\n <div id="ytp-plus-voting-container"></div>\n </div>\n `; } "undefined" != typeof window && (window.YouTubePlusSettingsHelpers = { createSettingsSidebar: function createSettingsSidebar(t) { return `\n <div class="ytp-plus-settings-sidebar">\n <div class="ytp-plus-settings-sidebar-header">\n <h2 class="ytp-plus-settings-title">${t("settingsTitle")}</h2> \n </div>\n <div class="ytp-plus-settings-nav">\n ${createNavItem("basic", t("basicTab"), '\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>\n <circle cx="9" cy="9" r="2"/>\n <path d="m21 15-3.086-3.086a2 2 0 0 0-1.414-.586H13l-2-2v3h6l3 3"/>\n </svg>\n ', !0)}\n ${createNavItem("advanced", t("advancedTab"), '\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <circle cx="12" cy="12" r="3"/>\n <path d="m12 1 0 6m0 6 0 6"/>\n <path d="m17.5 6.5-4.5 4.5m0 0-4.5 4.5m9-9L12 12l5.5 5.5"/>\n </svg>\n ')}\n ${createNavItem("experimental", t("experimentalTab"), '\n <svg width="64px" height="64px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M18.019 4V15.0386L6.27437 39.3014C5.48686 40.9283 6.16731 42.8855 7.79421 43.673C8.23876 43.8882 8.72624 44 9.22013 44H38.7874C40.5949 44 42.0602 42.5347 42.0602 40.7273C42.0602 40.2348 41.949 39.7488 41.7351 39.3052L30.0282 15.0386V4H18.019Z" stroke="currentColor" stroke-width="4" stroke-linejoin="round"></path> \n <path d="M10.9604 29.9998C13.1241 31.3401 15.2893 32.0103 17.4559 32.0103C19.6226 32.0103 21.7908 31.3401 23.9605 29.9998C26.1088 28.6735 28.2664 28.0103 30.433 28.0103C32.5997 28.0103 34.7755 28.6735 36.9604 29.9998" stroke="currentColor" stroke-width="4" stroke-linecap="round"></path>\n </svg>\n ')}\n ${createNavItem("voting", tr(t, "votingTab", "Voting"), '\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M7 10v12"/>\n <path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z"/>\n </svg>\n ')}\n ${createNavItem("report", t("reportTab"), '\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>\n <polyline points="14 2 14 8 20 8"></polyline>\n <line x1="12" y1="18" x2="12" y2="12"></line>\n <line x1="12" y1="9" x2="12.01" y2="9"></line>\n </svg>\n ')}\n ${createNavItem("about", t("aboutTab"), '\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <circle cx="12" cy="12" r="10"/>\n <path d="m9 12 2 2 4-4"/>\n </svg>\n ')}\n </div>\n </div>\n `; }, createMainContent: function createMainContent(settings, t) { return `\n <div class="ytp-plus-settings-main">\n <div class="ytp-plus-settings-sidebar-close">\n <button class="ytp-plus-settings-close" aria-label="${t("closeButton")}">\n <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\n </svg>\n </button>\n </div> \n <div class="ytp-plus-settings-content"> \n ${createBasicSettingsSection(settings, t)}\n ${createAdvancedSettingsSection(settings, t)}\n \n <div class="ytp-plus-settings-section hidden" data-section="experimental"></div>\n \n ${createVotingSection(0, t)}\n <div class="ytp-plus-settings-section hidden" data-section="report"></div>\n \n <div class="ytp-plus-settings-section hidden" data-section="about">\n <svg class="app-icon" width="90" height="90" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1">\n <path d="m23.24,4.62c-0.85,0.45 -2.19,2.12 -4.12,5.13c-1.54,2.41 -2.71,4.49 -3.81,6.8c-0.55,1.14 -1.05,2.2 -1.13,2.35c-0.08,0.16 -0.78,0.7 -1.66,1.28c-1.38,0.91 -1.8,1.29 -1.4,1.28c0.08,0 0.67,-0.35 1.31,-0.77c0.64,-0.42 1.19,-0.76 1.2,-0.74c0.02,0.02 -0.1,0.31 -0.25,0.66c-1.03,2.25 -1.84,5.05 -1.84,6.37c0.01,1.89 0.84,2.67 2.86,2.67c1.08,0 1.94,-0.31 3.66,-1.29c1.84,-1.06 3.03,-1.93 4.18,-3.09c1.69,-1.7 2.91,-3.4 3.28,-4.59c0.59,-1.9 -0.1,-3.08 -2.02,-3.44c-0.87,-0.16 -2.85,-0.14 -3.75,0.06c-1.78,0.38 -2.74,0.76 -2.5,1c0.03,0.03 0.5,-0.1 1.05,-0.28c1.49,-0.48 2.34,-0.59 3.88,-0.53c1.64,0.07 2.09,0.19 2.69,0.75l0.46,0.43l0,0.87c0,0.74 -0.05,0.98 -0.35,1.6c-0.69,1.45 -2.69,3.81 -4.37,5.14c-0.93,0.74 -2.88,1.94 -4.07,2.5c-1.64,0.77 -3.56,0.72 -4.21,-0.11c-0.39,-0.5 -0.5,-1.02 -0.44,-2.11c0.05,-0.85 0.16,-1.32 0.67,-2.86c0.34,-1.01 0.86,-2.38 1.15,-3.04c0.52,-1.18 0.55,-1.22 1.6,-2.14c4.19,-3.65 8.42,-9.4 9.02,-12.26c0.2,-0.94 0.13,-1.46 -0.21,-1.7c-0.31,-0.22 -0.38,-0.21 -0.89,0.06m0.19,0.26c-0.92,0.41 -3.15,3.44 -5.59,7.6c-1.05,1.79 -3.12,5.85 -3.02,5.95c0.07,0.07 1.63,-1.33 2.58,-2.34c1.57,-1.65 3.73,-4.39 4.88,-6.17c1.31,-2.03 2.06,-4.11 1.77,-4.89c-0.13,-0.34 -0.16,-0.35 -0.62,-0.15m11.69,13.32c-0.3,0.6 -1.19,2.54 -1.98,4.32c-1.6,3.62 -1.67,3.71 -2.99,4.34c-1.13,0.54 -2.31,0.85 -3.54,0.92c-0.99,0.06 -1.08,0.04 -1.38,-0.19c-0.28,-0.22 -0.31,-0.31 -0.26,-0.7c0.03,-0.25 0.64,-1.63 1.35,-3.08c1.16,-2.36 2.52,-5.61 2.52,-6.01c0,-0.49 -0.36,0.19 -1.17,2.22c-0.51,1.26 -1.37,3.16 -1.93,4.24c-0.55,1.08 -1.04,2.17 -1.09,2.43c-0.1,0.59 0.07,1.03 0.49,1.28c0.78,0.46 3.3,0.06 5.13,-0.81l0.93,-0.45l-0.66,1.25c-0.7,1.33 -3.36,6.07 -4.31,7.67c-2.02,3.41 -3.96,5.32 -6.33,6.21c-2.57,0.96 -4.92,0.74 -6.14,-0.58c-0.81,-0.88 -0.82,-1.71 -0.04,-3.22c1.22,-2.36 6.52,-6.15 10.48,-7.49c0.52,-0.18 0.95,-0.39 0.95,-0.46c0,-0.21 -0.19,-0.18 -1.24,0.2c-1.19,0.43 -3.12,1.37 -4.34,2.11c-2.61,1.59 -5.44,4.09 -6.13,5.43c-1.15,2.2 -0.73,3.61 1.4,4.6c0.59,0.28 0.75,0.3 2.04,0.3c1.67,0 2.42,-0.18 3.88,-0.89c1.87,-0.92 3.17,-2.13 4.72,-4.41c0.98,-1.44 4.66,-7.88 5.91,-10.33c0.25,-0.49 0.68,-1.19 0.96,-1.56c0.28,-0.37 0.76,-1.15 1.06,-1.73c0.82,-1.59 2.58,-6.10 2.58,-6.6c0,-0.06 -0.07,-0.1 -0.17,-0.1c-0.10,0 -0.39,0.44 -0.71,1.09m-1.34,3.7c-0.93,2.08 -1.09,2.48 -0.87,2.2c0.19,-0.24 1.66,-3.65 1.6,-3.71c-0.02,-0.02 -0.35,0.66 -0.73,1.51" fill="none" fill-rule="evenodd" stroke="currentColor" />\n </svg>\n <h1>YouTube +</h1><br><br>\n </div>\n \n </div>\n <div class="ytp-plus-footer">\n <button class="ytp-plus-button ytp-plus-button-primary" id="ytp-plus-save-settings">${t("saveChanges")}</button>\n </div>\n </div>\n `; }, createSettingsItem, createDownloadSiteOption, createBasicSettingsSection, createAdvancedSettingsSection, createExperimentalSettingsSection, createVotingSection, getMusicSettings }); })(); const qs = sel => window.YouTubeUtils?.$(sel) || document.querySelector(sel); const setSettingByPath = (settings, path, value) => { if (!settings || "object" != typeof settings) { return; } if (!path || "string" != typeof path) { return; } if (!path.includes(".")) { settings[path] = value; return; } const keys = path.split(".").filter(Boolean); if (!keys.length) { return; } const lastKey = keys.pop(); if (!lastKey) { return; } let cur = settings; for (const k of keys) { Object.prototype.hasOwnProperty.call(cur, k) && "object" == typeof cur[k] && cur[k] || (cur[k] = {}); cur = cur[k]; } cur[lastKey] = value; }; const initializeDownloadSites = settings => { settings.downloadSites || (settings.downloadSites = { externalDownloader: !0, ytdl: !0, direct: !0 }); if (settings.downloadSites && Object.prototype.hasOwnProperty.call(settings.downloadSites, "y2mate")) { Object.prototype.hasOwnProperty.call(settings.downloadSites, "externalDownloader") || (settings.downloadSites.externalDownloader = settings.downloadSites.y2mate); delete settings.downloadSites.y2mate; } }; const toggleDownloadSiteControls = checkbox => { try { const container = checkbox.closest(".download-site-option"); if (container) { const controls = container.querySelector(".download-site-controls"); controls && (controls.style.display = checkbox.checked ? "block" : "none"); } } catch (err) { console.warn("[YouTube+] toggle download-site-controls failed:", err); } }; const safelySaveSettings = saveSettings => { try { saveSettings(); } catch (err) { console.warn("[YouTube+] autosave downloadSite toggle failed:", err); } }; const handleDownloadSiteToggle = (target, key, settings, markDirty, saveSettings) => { initializeDownloadSites(settings); const checkbox = target; settings.downloadSites[key] = checkbox.checked; try { markDirty(); } catch {} toggleDownloadSiteControls(checkbox); rebuildDownloadDropdown(settings); safelySaveSettings(saveSettings); }; const handleDownloadButtonToggle = context => { const {settings, getElement, addDownloadButton} = context; const controls = getElement(".ytp-right-controls"); const existing = getElement(".ytp-download-button", !1); if (settings.enableDownload) { controls && !existing && addDownloadButton(controls); } else { existing && existing.remove(); const dropdown = qs(".download-options"); dropdown && dropdown.remove(); } }; const handleSpeedControlToggle = context => { const {settings, getElement, addSpeedControlButton} = context; const controls = getElement(".ytp-right-controls"); const existing = getElement(".speed-control-btn", !1); if (settings.enableSpeedControl) { controls && !existing && addSpeedControlButton(controls); } else { existing && existing.remove(); const speedOptions = qs(".speed-options"); speedOptions && speedOptions.remove(); } }; const updateGlobalSettings = settings => { "undefined" != typeof window && window.youtubePlus && (window.youtubePlus.settings = window.youtubePlus.settings || settings); }; const applySettingLive = (setting, context) => { const {settings, refreshDownloadButton} = context; try { context.updatePageBasedOnSettings && context.updatePageBasedOnSettings(); "enableDownload" === setting ? handleDownloadButtonToggle(context) : "enableSpeedControl" === setting && handleSpeedControlToggle(context); refreshDownloadButton && refreshDownloadButton(); } catch (innerErr) { console.warn("[YouTube+] live apply specific toggle failed:", innerErr); } updateGlobalSettings(settings); }; const handleSimpleSettingToggle = (target, setting, settings, context, markDirty, saveSettings, modal) => { const checked = target.checked; setSettingByPath(settings, setting, checked); try { markDirty(); } catch {} try { applySettingLive(setting, context); } catch (err) { console.warn("[YouTube+] apply settings live failed:", err); } try { saveSettings(); } catch (err) { console.warn("[YouTube+] autosave simple setting failed:", err); } if ("enableDownload" === setting) { const submenu = modal.querySelector(".download-submenu"); submenu && (submenu.style.display = checked ? "block" : "none"); const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="download"]'); if (toggleBtn instanceof HTMLElement) { if (checked) { toggleBtn.removeAttribute("disabled"); toggleBtn.setAttribute("aria-expanded", "true"); toggleBtn.style.display = "inline-flex"; } else { toggleBtn.setAttribute("disabled", ""); toggleBtn.setAttribute("aria-expanded", "false"); toggleBtn.style.display = "none"; } } } if ("enableZenStyles" === setting) { const submenu = modal.querySelector(".style-submenu"); submenu && (submenu.style.display = checked ? "block" : "none"); const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="style"]'); if (toggleBtn instanceof HTMLElement) { if (checked) { toggleBtn.removeAttribute("disabled"); toggleBtn.setAttribute("aria-expanded", "true"); toggleBtn.style.display = "inline-flex"; } else { toggleBtn.setAttribute("disabled", ""); toggleBtn.setAttribute("aria-expanded", "false"); toggleBtn.style.display = "none"; } } } if ("enableSpeedControl" === setting) { const submenu = modal.querySelector(".speed-submenu"); submenu && (submenu.style.display = checked ? "block" : "none"); const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="speed"]'); if (toggleBtn instanceof HTMLElement) { if (checked) { toggleBtn.removeAttribute("disabled"); toggleBtn.setAttribute("aria-expanded", "true"); toggleBtn.style.display = "inline-flex"; } else { toggleBtn.setAttribute("disabled", ""); toggleBtn.setAttribute("aria-expanded", "false"); toggleBtn.style.display = "none"; } } } if ("enableEnhanced" === setting) { const submenu = modal.querySelector(".enhanced-submenu"); submenu && (submenu.style.display = checked ? "block" : "none"); const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="enhanced"]'); if (toggleBtn instanceof HTMLElement) { if (checked) { toggleBtn.removeAttribute("disabled"); toggleBtn.setAttribute("aria-expanded", "true"); toggleBtn.style.display = "inline-flex"; } else { toggleBtn.setAttribute("disabled", ""); toggleBtn.setAttribute("aria-expanded", "false"); toggleBtn.style.display = "none"; } } } if ("enableLoop" === setting) { const submenu = modal.querySelector(".loop-submenu"); submenu && (submenu.style.display = checked ? "block" : "none"); const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="loop"]'); if (toggleBtn instanceof HTMLElement) { if (checked) { toggleBtn.removeAttribute("disabled"); toggleBtn.setAttribute("aria-expanded", "true"); toggleBtn.style.display = "inline-flex"; } else { toggleBtn.setAttribute("disabled", ""); toggleBtn.setAttribute("aria-expanded", "false"); toggleBtn.style.display = "none"; } } } }; const initializeDownloadCustomization = settings => { settings.downloadSiteCustomization || (settings.downloadSiteCustomization = { externalDownloader: { name: "SSYouTube", url: "https://ssyoutube.com/watch?v={videoId}" } }); if (settings.downloadSiteCustomization && Object.prototype.hasOwnProperty.call(settings.downloadSiteCustomization, "y2mate")) { Object.prototype.hasOwnProperty.call(settings.downloadSiteCustomization, "externalDownloader") || (settings.downloadSiteCustomization.externalDownloader = settings.downloadSiteCustomization.y2mate); delete settings.downloadSiteCustomization.y2mate; } }; const initializeDownloadSite = (settings, site) => { settings.downloadSiteCustomization[site] || (settings.downloadSiteCustomization[site] = { name: "", url: "" }); }; const getDownloadSiteFallbackName = (site, t) => "externalDownloader" === site ? "SSYouTube" : t("ytdl" === site ? "byYTDL" : "directDownload"); const updateDownloadSiteName = (target, site, t) => { const nameDisplay = target.closest(".download-site-option")?.querySelector(".download-site-name"); if (nameDisplay) { const inputValue = target.value; const fallbackName = getDownloadSiteFallbackName(site, t); nameDisplay.textContent = inputValue || fallbackName; } }; const rebuildDownloadDropdown = settings => { try { if ("undefined" != typeof window && window.youtubePlus && "function" == typeof window.youtubePlus.rebuildDownloadDropdown) { window.youtubePlus.settings = window.youtubePlus.settings || settings; window.youtubePlus.rebuildDownloadDropdown(); } } catch (err) { console.warn("[YouTube+] rebuildDownloadDropdown call failed:", err); } }; const handleDownloadSiteInput = (target, site, field, settings, markDirty, t) => { initializeDownloadCustomization(settings); initializeDownloadSite(settings, site); settings.downloadSiteCustomization[site][field] = target.value; try { markDirty(); } catch {} "name" === field && updateDownloadSiteName(target, site, t); rebuildDownloadDropdown(settings); }; const ensureExternalDownloaderStructure = settings => { settings.downloadSiteCustomization || (settings.downloadSiteCustomization = { externalDownloader: { name: "SSYouTube", url: "https://ssyoutube.com/watch?v={videoId}" } }); settings.downloadSiteCustomization.externalDownloader || (settings.downloadSiteCustomization.externalDownloader = { name: "", url: "" }); }; const readExternalDownloaderInputs = (container, settings) => { const nameInput = container.querySelector('input.download-site-input[data-site="externalDownloader"][data-field="name"]'); const urlInput = container.querySelector('input.download-site-input[data-site="externalDownloader"][data-field="url"]'); nameInput && (settings.downloadSiteCustomization.externalDownloader.name = nameInput.value); urlInput && (settings.downloadSiteCustomization.externalDownloader.url = urlInput.value); }; const triggerRebuildDropdown = () => { try { "undefined" != typeof window && window.youtubePlus && "function" == typeof window.youtubePlus.rebuildDownloadDropdown && window.youtubePlus.rebuildDownloadDropdown(); } catch (err) { console.warn("[YouTube+] rebuildDownloadDropdown call failed:", err); } }; const handleExternalDownloaderSave = (target, settings, saveSettings, showNotification, t) => { ensureExternalDownloaderStructure(settings); const container = target.closest(".download-site-option"); container && readExternalDownloaderInputs(container, settings); saveSettings(); window.youtubePlus && (window.youtubePlus.settings = window.youtubePlus.settings || settings); triggerRebuildDropdown(); try { const msg = t && "function" == typeof t && t("externalDownloaderSettingsSaved") || t("y2mateSettingsSaved"); showNotification(msg); } catch { showNotification("Settings saved"); } }; const resetExternalDownloaderToDefaults = settings => { ensureExternalDownloaderStructure(settings); settings.downloadSiteCustomization.externalDownloader = { name: "SSYouTube", url: "https://ssyoutube.com/watch?v={videoId}" }; }; const updateExternalDownloaderModalInputs = (container, settings) => { const nameInput = container.querySelector('input.download-site-input[data-site="externalDownloader"][data-field="name"]'); const urlInput = container.querySelector('input.download-site-input[data-site="externalDownloader"][data-field="url"]'); const nameDisplay = container.querySelector(".download-site-name"); const edSettings = settings.downloadSiteCustomization.externalDownloader; nameInput && (nameInput.value = edSettings.name); urlInput && (urlInput.value = edSettings.url); nameDisplay && (nameDisplay.textContent = edSettings.name); }; const handleExternalDownloaderReset = (modal, settings, saveSettings, showNotification, t) => { resetExternalDownloaderToDefaults(settings); const container = modal.querySelector(".download-site-option"); container && updateExternalDownloaderModalInputs(container, settings); saveSettings(); window.youtubePlus && (window.youtubePlus.settings = window.youtubePlus.settings || settings); triggerRebuildDropdown(); try { const msg = t && "function" == typeof t && t("externalDownloaderReset") || t("y2mateReset"); showNotification(msg); } catch { showNotification("Settings reset"); } }; const handleSidebarNavigation = (navItem, modal) => { const {dataset} = navItem; const {section} = dataset; modal.querySelectorAll(".ytp-plus-settings-nav-item").forEach(item => item.classList.remove("active")); modal.querySelectorAll(".ytp-plus-settings-section").forEach(s => s.classList.add("hidden")); navItem.classList.add("active"); const targetSection = modal.querySelector(`.ytp-plus-settings-section[data-section="${section}"]`); targetSection && targetSection.classList.remove("hidden"); "voting" === section && window.YouTubePlus?.Voting?.initSlider && requestAnimationFrame(() => window.YouTubePlus.Voting.initSlider()); try { localStorage.setItem("ytp-plus-active-nav-section", section); } catch {} }; const handleMusicSettingToggle = (target, setting, showNotification, t) => { try { const defaults = { enableMusic: !0, immersiveSearchStyles: !0, hoverStyles: !0, playerSidebarStyles: !0, centeredPlayerStyles: !0, playerBarStyles: !0, centeredPlayerBarStyles: !0, miniPlayerStyles: !0, scrollToTopStyles: !0 }; const allowedKeys = new Set(Object.keys(defaults)); if (!allowedKeys.has(setting)) { return; } let musicSettings = { ...defaults }; try { if ("undefined" != typeof GM_getValue) { const stored = GM_getValue("youtube-plus-music-settings", null); if ("string" == typeof stored && stored) { const parsed = JSON.parse(stored); parsed && "object" == typeof parsed && (musicSettings = { ...musicSettings, ...parsed }); } } } catch {} try { const stored = localStorage.getItem("youtube-plus-music-settings"); if (stored) { const parsed = JSON.parse(stored); parsed && "object" == typeof parsed && (musicSettings = { ...musicSettings, ...parsed }); } } catch {} musicSettings[setting] = target.checked; try { if ("enableMusic" === setting) { const enabled = !!musicSettings.enableMusic; const root = target.closest(".ytp-plus-settings-section") || target.closest(".ytp-plus-settings-panel"); if (root) { const submenu = root.querySelector('.music-submenu[data-submenu="music"]'); submenu instanceof HTMLElement && (submenu.style.display = enabled ? "block" : "none"); const toggleBtn = root.querySelector('.ytp-plus-submenu-toggle[data-submenu="music"]'); if (toggleBtn instanceof HTMLElement) { if (enabled) { toggleBtn.removeAttribute("disabled"); toggleBtn.style.display = "inline-flex"; } else { toggleBtn.setAttribute("disabled", ""); toggleBtn.style.display = "none"; } toggleBtn.setAttribute("aria-expanded", enabled ? "true" : "false"); } } } } catch {} localStorage.setItem("youtube-plus-music-settings", JSON.stringify(musicSettings)); try { "undefined" != typeof GM_setValue && GM_setValue("youtube-plus-music-settings", JSON.stringify(musicSettings)); } catch {} if ("undefined" != typeof window && window.YouTubeMusic) { window.YouTubeMusic.saveSettings && window.YouTubeMusic.saveSettings(musicSettings); window.YouTubeMusic.applySettingsChanges && window.YouTubeMusic.applySettingsChanges(); } showNotification && t && showNotification(t("musicSettingsSaved")); } catch { console.warn("[YouTube+] handleMusicSettingToggle failed"); } }; const isMusicSetting = setting => "enableMusic" === setting || "immersiveSearchStyles" === setting || "hoverStyles" === setting || "playerSidebarStyles" === setting || "centeredPlayerStyles" === setting || "playerBarStyles" === setting || "centeredPlayerBarStyles" === setting || "miniPlayerStyles" === setting || "scrollToTopStyles" === setting; if ("undefined" != typeof window) { const createFocusTrap = container => { const handler = e => { if ("Tab" !== e.key) { return; } const focusable = Array.from(container.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter(el => null !== el.offsetParent); if (0 === focusable.length) { return; } const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else if (document.activeElement === last) { e.preventDefault(); first.focus(); } }; container.addEventListener("keydown", handler); return () => container.removeEventListener("keydown", handler); }; window.YouTubePlusModalHandlers = { handleDownloadSiteToggle, handleSimpleSettingToggle, handleDownloadSiteInput, handleExternalDownloaderSave, handleExternalDownloaderReset, handleSidebarNavigation, applySettingLive, handleMusicSettingToggle, isMusicSetting, createFocusTrap }; } /** * YouTube+ Download Module * Unified download system with button UI and download functionality * @version 3.0 */ !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const $ = sel => window.YouTubeUtils?.$(sel) || document.querySelector(sel); if (void 0 === YouTubeUtils) { console.error("[YouTube+ Download] YouTubeUtils not found!"); return; } const {NotificationManager} = YouTubeUtils; const t = (key, params = {}) => { if (window.YouTubeUtils?.t) { return window.YouTubeUtils.t(key, params); } const str = String(key || ""); if (!params || 0 === Object.keys(params).length) { return str; } let result = str; for (const [k, v] of Object.entries(params)) { result = result.split(`{${k}}`).join(String(v)); } return result; }; const logger = "undefined" != typeof YouTubePlusLogger && YouTubePlusLogger ? YouTubePlusLogger.createLogger("Download") : { debug: () => {}, info: () => {}, warn: console.warn.bind(console), error: console.error.bind(console) }; const DownloadConfig = { API: { KEY_URL: "https://cnv.cx/v2/sanity/key", CONVERT_URL: "https://cnv.cx/v2/converter" }, HEADERS: { "Content-Type": "application/json", Origin: "https://mp3yt.is", Accept: "*/*", "User-Agent": "undefined" != typeof navigator ? navigator.userAgent : "" }, VIDEO_QUALITIES: [ "144", "240", "360", "480", "720", "1080", "1440", "2160" ], AUDIO_BITRATES: [ "64", "128", "192", "256", "320" ], DEFAULTS: { format: "video", videoQuality: "1080", audioBitrate: "320", embedThumbnail: !0 } }; function getVideoId() { const params = new URLSearchParams(window.location.search); return params.get("v") || null; } function getVideoUrl() { const videoId = getVideoId(); return videoId ? `https://www.youtube.com/watch?v=${videoId}` : window.location.href; } function getVideoTitle() { try { const titleElement = $("h1.ytd-video-primary-info-renderer yt-formatted-string") || $("h1.title yt-formatted-string") || $("ytd-watch-metadata h1"); return titleElement ? titleElement.textContent.trim() : "video"; } catch { return "video"; } } function sanitizeFilename(filename) { return filename.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").trim().substring(0, 200); } function formatBytes(bytes) { if (0 === bytes) { return "0 B"; } const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${[ "B", "KB", "MB", "GB" ][i]}`; } function gmXmlHttpRequest(options) { return new Promise((resolve, reject) => { "undefined" == typeof GM_xmlhttpRequest ? (async () => { try { const responseLike = await (async function executeFetchFallback(options) { const fetchOpts = { method: options.method || "GET", headers: options.headers || {}, body: options.data || options.body || void 0 }; const resp = await fetch(options.url, fetchOpts); const responseLike = (function buildResponseObject(resp) { return { status: resp.status, statusText: resp.statusText, finalUrl: resp.url, headers: {}, responseText: null, response: null }; })(resp); await (async function extractResponseText(resp, responseLike) { try { responseLike.responseText = await resp.text(); } catch { responseLike.responseText = null; } })(resp, responseLike); await (async function extractResponseBlob(resp, responseLike, responseType) { if ("blob" === responseType) { try { responseLike.response = await resp.blob(); } catch { responseLike.response = null; } } })(resp, responseLike, options.responseType); options.onload && options.onload(responseLike); return responseLike; })(options); resolve(responseLike); } catch (err) { options.onerror && options.onerror(err); reject(err); } })() : GM_xmlhttpRequest((function createGmRequestOptions(options, resolve, reject) { return { ...options, onload: response => { options.onload && options.onload(response); resolve(response); }, onerror: error => { options.onerror && options.onerror(error); reject(error); }, ontimeout: () => { options.ontimeout && options.ontimeout(); reject(new Error("Request timeout")); } }; })(options, resolve, reject)); }); } function buildSubtitleUrl(baseUrl) { return baseUrl.includes("fmt=") ? baseUrl : `${baseUrl}&fmt=srv1`; } function parseCaptionTracks(captionTracks) { return captionTracks.map(track => ({ name: track.name?.simpleText || track.languageCode, languageCode: track.languageCode, url: buildSubtitleUrl(track.baseUrl), isAutoGenerated: "asr" === track.kind })); } function parseTranslationLanguages(translationLanguages, baseUrl) { return translationLanguages.map(lang => ({ name: lang.languageName?.simpleText || lang.languageCode, languageCode: lang.languageCode, baseUrl: baseUrl || "", isAutoGenerated: !0 })); } async function getSubtitles(videoId) { try { const data = await (async function fetchPlayerData(videoId) { const response = await gmXmlHttpRequest({ method: "POST", url: "https://www.youtube.com/youtubei/v1/player", headers: { "Content-Type": "application/json", "User-Agent": DownloadConfig.HEADERS["User-Agent"] }, data: JSON.stringify({ context: { client: { clientName: "WEB", clientVersion: "2.20240304.00.00" } }, videoId }) }); if (200 !== response.status) { throw new Error(`Failed to get player data: ${response.status}`); } return JSON.parse(response.responseText); })(videoId); const videoTitle = data.videoDetails?.title || "video"; const captions = data.captions?.playerCaptionsTracklistRenderer; if (!captions) { return (function createEmptySubtitleResult(videoId, videoTitle) { return { videoId, videoTitle, subtitles: [], autoTransSubtitles: [] }; })(videoId, videoTitle); } const captionTracks = captions.captionTracks || []; const translationLanguages = captions.translationLanguages || []; const baseUrl = captionTracks[0]?.baseUrl || ""; return { videoId, videoTitle, subtitles: parseCaptionTracks(captionTracks), autoTransSubtitles: parseTranslationLanguages(translationLanguages, baseUrl) }; } catch (error) { logger.error("Error getting subtitles:", error); return null; } } function decodeHTMLEntities(text) { const entities = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'", "'": "'", " ": " " }; let decoded = text; for (const [entity, char] of Object.entries(entities)) { decoded = decoded.replaceAll(entity, char); } decoded = decoded.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10))); decoded = decoded.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))); return decoded; } function formatSRTTime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor(seconds % 3600 / 60); const secs = Math.floor(seconds % 60); const milliseconds = Math.floor(seconds % 1 * 1e3); return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")},${String(milliseconds).padStart(3, "0")}`; } async function downloadSubtitle(options = {}) { const {videoId, url: baseUrl, languageCode, languageName, format = "srt", translateTo = null} = options; if (!videoId || !baseUrl) { throw new Error("Video ID and URL are required"); } const title = getVideoTitle(); let subtitleUrl = baseUrl; subtitleUrl.includes("fmt=") || (subtitleUrl += "&fmt=srv1"); translateTo && (subtitleUrl += `&tlang=${translateTo}`); NotificationManager.show(t("subtitleDownloading"), { duration: 2e3, type: "info" }); try { const response = await gmXmlHttpRequest({ method: "GET", url: subtitleUrl, headers: { "User-Agent": DownloadConfig.HEADERS["User-Agent"], Referer: "https://www.youtube.com/" } }); if (200 !== response.status) { throw new Error(`Failed to download subtitle: ${response.status}`); } const xmlText = response.responseText; if (!xmlText || 0 === xmlText.length) { throw new Error("Empty subtitle response"); } let content; let extension; if ("xml" === format) { content = xmlText; extension = "xml"; } else { const cues = (function parseSubtitleXML(xml) { const cues = []; const textTagRegex = /<text\s+start="([^"]+)"\s+dur="([^"]+)"[^>]*>([\s\S]*?)<\/text>/gi; let match; for (;null !== (match = textTagRegex.exec(xml)); ) { const start = parseFloat(match[1] || "0"); const duration = parseFloat(match[2] || "0"); let text = match[3] || ""; text = text.replace(/<!\[CDATA\[(.*?)\]\]>/g, "$1"); text = decodeHTMLEntities(text.trim()); cues.push({ start, duration, text }); } return cues; })(xmlText); if (0 === cues.length) { throw new Error("No subtitle cues found"); } if ("srt" === format) { content = (function convertToSRT(cues) { let srt = ""; cues.forEach((cue, index) => { const startTime = formatSRTTime(cue.start); const endTime = formatSRTTime(cue.start + cue.duration); const text = cue.text.replace(/\n/g, " ").trim(); srt += `${index + 1}\n`; srt += `${startTime} --\x3e ${endTime}\n`; srt += `${text}\n\n`; }); return srt; })(cues); extension = "srt"; } else if ("txt" === format) { content = (function convertToTXT(cues) { return cues.map(cue => cue.text.trim()).join("\n"); })(cues); extension = "txt"; } else { content = xmlText; extension = "xml"; } } const langSuffix = translateTo ? `${languageCode}-${translateTo}` : languageCode; const filename = sanitizeFilename(`${title} - ${languageName} (${langSuffix}).${extension}`); const blob = new Blob([ content ], { type: "text/plain;charset=utf-8" }); const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); NotificationManager.show(t("subtitleDownloaded"), { duration: 3e3, type: "success" }); logger.debug("Subtitle downloaded:", filename); } catch (error) { logger.error("Error downloading subtitle:", error); NotificationManager.show(`${t("subtitleDownloadFailed")} ${error.message}`, { duration: 5e3, type: "error" }); throw error; } } async function downloadVideo(options = {}) { const {format = DownloadConfig.DEFAULTS.format, quality = DownloadConfig.DEFAULTS.videoQuality, audioBitrate = DownloadConfig.DEFAULTS.audioBitrate, embedThumbnail = DownloadConfig.DEFAULTS.embedThumbnail, onProgress = null} = options; const videoId = getVideoId(); if (!videoId) { throw new Error("Video ID not found"); } const videoUrl = getVideoUrl(); const title = getVideoTitle(); NotificationManager.show(t("startingDownload"), { duration: 2e3, type: "info" }); try { logger.debug("Fetching API key..."); const keyResponse = await gmXmlHttpRequest({ method: "GET", url: DownloadConfig.API.KEY_URL, headers: DownloadConfig.HEADERS }); if (200 !== keyResponse.status) { throw new Error(`Failed to get API key: ${keyResponse.status}`); } const keyData = JSON.parse(keyResponse.responseText); if (!keyData || !keyData.key) { throw new Error("API key not found in response"); } const {key} = keyData; logger.debug("API key obtained"); let payload; if ("video" === format) { const codec = parseInt(quality, 10) > 1080 ? "vp9" : "h264"; payload = { link: videoUrl, format: "mp4", audioBitrate: "128", videoQuality: quality, filenameStyle: "pretty", vCodec: codec }; } else { payload = { link: videoUrl, format: "mp3", audioBitrate, filenameStyle: "pretty" }; } logger.debug("Requesting conversion...", payload); const customHeaders = { ...DownloadConfig.HEADERS, key }; const downloadResponse = await gmXmlHttpRequest({ method: "POST", url: DownloadConfig.API.CONVERT_URL, headers: customHeaders, data: JSON.stringify(payload) }); if (200 !== downloadResponse.status) { throw new Error(`Conversion failed: ${downloadResponse.status}`); } const apiDownloadInfo = JSON.parse(downloadResponse.responseText); logger.debug("Conversion response:", apiDownloadInfo); if (!apiDownloadInfo.url) { throw new Error("No download URL received from API"); } logger.debug("Downloading file from:", apiDownloadInfo.url); return new Promise((resolve, reject) => { if ("undefined" != typeof GM_xmlhttpRequest) { GM_xmlhttpRequest({ method: "GET", url: apiDownloadInfo.url, responseType: "blob", headers: { "User-Agent": DownloadConfig.HEADERS["User-Agent"], Referer: "https://mp3yt.is/", Accept: "*/*" }, onprogress: progress => { onProgress && onProgress({ loaded: progress.loaded, total: progress.total, percent: progress.total ? Math.round(progress.loaded / progress.total * 100) : 0 }); }, onload: async response => { if (200 === response.status && response.response) { let blob = response.response; if (0 === blob.size) { reject(new Error(t("zeroBytesError"))); return; } window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug(`[Download] File downloaded: ${formatBytes(blob.size)}`); if ("audio" === format && embedThumbnail) { try { logger.debug("Embedding album art..."); const thumbnailUrl = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`; const albumArt = await (function createSquareAlbumArt(thumbnailUrl) { return new Promise((resolve, reject) => { const img = document.createElement("img"); img.crossOrigin = "anonymous"; img.onload = () => { const canvas = document.createElement("canvas"); const size = Math.min(img.width, img.height); canvas.width = size; canvas.height = size; const ctx = canvas.getContext("2d"); if (!ctx) { canvas.width = 0; canvas.height = 0; reject(new Error("Failed to get canvas context")); return; } const sx = (img.width - size) / 2; const sy = (img.height - size) / 2; ctx.drawImage(img, sx, sy, size, size, 0, 0, size, size); canvas.toBlob(blob => { canvas.width = 0; canvas.height = 0; blob ? resolve(blob) : reject(new Error("Failed to create blob")); }, "image/jpeg", .95); }; img.onerror = () => reject(new Error("Failed to load thumbnail")); img.src = thumbnailUrl; }); })(thumbnailUrl); blob = await (async function embedAlbumArtToMP3(mp3Blob, albumArtBlob, metadata) { try { if (void 0 === window.ID3Writer) { logger.warn("ID3Writer not available, skipping album art embedding"); return mp3Blob; } const arrayBuffer = await mp3Blob.arrayBuffer(); const writer = new window.ID3Writer(arrayBuffer); metadata.title && writer.setFrame("TIT2", metadata.title); metadata.artist && writer.setFrame("TPE1", [ metadata.artist ]); metadata.album && writer.setFrame("TALB", metadata.album); if (albumArtBlob) { const coverArrayBuffer = await albumArtBlob.arrayBuffer(); writer.setFrame("APIC", { type: 3, data: coverArrayBuffer, description: "Cover" }); } writer.addTag(); return new Blob([ writer.arrayBuffer ], { type: "audio/mpeg" }); } catch (error) { logger.error("Error embedding album art:", error); return mp3Blob; } })(blob, albumArt, { title }); logger.debug("Album art embedded successfully"); } catch (error) { logger.error("Failed to embed album art:", error); } } const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; const filename = apiDownloadInfo.filename || `${title}.${"video" === format ? "mp4" : "mp3"}`; a.download = sanitizeFilename(filename); document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(blobUrl), 100); NotificationManager.show(t("downloadCompleted"), { duration: 3e3, type: "success" }); logger.debug("Download completed:", filename); resolve(); } else { reject(new Error(`Download failed: ${response.status}`)); } }, onerror: () => reject(new Error("Download failed - network error")), ontimeout: () => reject(new Error("Download timeout")) }); } else { logger.warn("GM_xmlhttpRequest not available, opening in new tab"); window.open(apiDownloadInfo.url, "_blank"); resolve(); } }); } catch (error) { logger.error("Error:", error); NotificationManager.show(`${t("downloadFailed")} ${error.message}`, { duration: 5e3, type: "error" }); throw error; } } let _modalElements = null; function buildModalForm() { const qualitySelect = document.createElement("div"); qualitySelect.role = "radiogroup"; qualitySelect.value = DownloadConfig.DEFAULTS.videoQuality; Object.assign(qualitySelect.style, { display: "flex", flexWrap: "wrap", gap: "10px", padding: "12px 6px", borderRadius: "10px", width: "100%", alignItems: "center", justifyContent: "center", background: "transparent" }); const embedCheckbox = document.createElement("input"); embedCheckbox.type = "checkbox"; embedCheckbox.checked = DownloadConfig.DEFAULTS.embedThumbnail; const embedLabel = document.createElement("label"); embedLabel.style.fontSize = "13px"; embedLabel.style.display = "flex"; embedLabel.style.alignItems = "center"; embedLabel.style.gap = "6px"; embedLabel.style.color = "#fff"; embedLabel.style.display = "none"; embedLabel.appendChild(embedCheckbox); embedLabel.appendChild(document.createTextNode(t("embedThumbnail"))); const subtitleWrapper = document.createElement("div"); subtitleWrapper.style.display = "none"; const subtitleSelect = (function createSubtitleSelect() { const subtitleSelect = document.createElement("div"); subtitleSelect.setAttribute("role", "listbox"); subtitleSelect.setAttribute("aria-expanded", "false"); subtitleSelect.setAttribute("aria-label", t("subtitleLanguage") || "Subtitle language"); subtitleSelect.setAttribute("tabindex", "0"); Object.assign(subtitleSelect.style, { position: "relative", width: "100%", marginBottom: "8px", fontSize: "14px", color: "#fff", cursor: "pointer" }); const _ssDisplay = document.createElement("div"); Object.assign(_ssDisplay.style, { padding: "10px 12px", borderRadius: "10px", background: "linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))", border: "1px solid rgba(255,255,255,0.06)", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", backdropFilter: "blur(6px)", boxShadow: "0 4px 18px rgba(0,0,0,0.35) inset" }); const _ssLabel = document.createElement("div"); _ssLabel.style.flex = "1"; _ssLabel.style.overflow = "hidden"; _ssLabel.style.textOverflow = "ellipsis"; _ssLabel.style.whiteSpace = "nowrap"; _ssLabel.textContent = t("loading"); const _ssChevron = document.createElement("div"); _ssChevron.textContent = "▾"; _ssChevron.style.opacity = "0.8"; _ssDisplay.appendChild(_ssLabel); _ssDisplay.appendChild(_ssChevron); const _ssList = document.createElement("div"); Object.assign(_ssList.style, { position: "absolute", top: "calc(100% + 8px)", left: "0", right: "0", maxHeight: "220px", overflowY: "auto", borderRadius: "10px", background: "linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.02))", border: "1px solid rgba(255,255,255,0.06)", boxShadow: "0 8px 30px rgba(0,0,0,0.6)", backdropFilter: "blur(8px)", zIndex: "9999", display: "none" }); subtitleSelect.appendChild(_ssDisplay); subtitleSelect.appendChild(_ssList); _ssList.addEventListener("click", e => { const item = e.target?.closest?.("[data-value]"); if (item && _ssList.contains(item)) { subtitleSelect.value = item.dataset.value; _ssList.style.display = "none"; } }); _ssList.addEventListener("mouseover", e => { const item = e.target?.closest?.("[data-value]"); item && _ssList.contains(item) && (item.style.background = "rgba(255,255,255,0.02)"); }); _ssList.addEventListener("mouseout", e => { const item = e.target?.closest?.("[data-value]"); if (!item || !_ssList.contains(item)) { return; } const related = e.relatedTarget; related && item.contains(related) || (item.style.background = "transparent"); }); subtitleSelect._options = []; subtitleSelect._value = ""; subtitleSelect._disabled = !1; subtitleSelect.setPlaceholder = text => { _ssLabel.textContent = text || ""; subtitleSelect._options = []; _ssList.replaceChildren(); subtitleSelect._value = ""; }; subtitleSelect.setOptions = options => { subtitleSelect._options = options || []; _ssList.replaceChildren(); subtitleSelect._options.forEach(opt => { const item = document.createElement("div"); item.textContent = opt.text; item.dataset.value = String(opt.value); Object.assign(item.style, { padding: "10px 12px", cursor: "pointer", borderBottom: "1px solid rgba(255,255,255,0.02)", color: "#fff" }); _ssList.appendChild(item); }); if (subtitleSelect._options.length > 0) { subtitleSelect.value = String(subtitleSelect._options[0].value); } else { subtitleSelect._value = ""; _ssLabel.textContent = t("noSubtitles"); } }; Object.defineProperty(subtitleSelect, "value", { get: () => subtitleSelect._value, set(v) { subtitleSelect._value = String(v); const found = subtitleSelect._options.find(o => String(o.value) === subtitleSelect._value); _ssLabel.textContent = found ? found.text : ""; } }); Object.defineProperty(subtitleSelect, "disabled", { get: () => subtitleSelect._disabled, set(v) { subtitleSelect._disabled = !!v; _ssDisplay.style.opacity = subtitleSelect._disabled ? "0.5" : "1"; subtitleSelect.style.pointerEvents = subtitleSelect._disabled ? "none" : "auto"; } }); _ssDisplay.addEventListener("click", () => { if (subtitleSelect._disabled) { return; } const isOpen = "none" !== _ssList.style.display; _ssList.style.display = isOpen ? "none" : ""; subtitleSelect.setAttribute("aria-expanded", isOpen ? "false" : "true"); }); subtitleSelect.addEventListener("keydown", e => { if (subtitleSelect._disabled) { return; } const isOpen = "none" !== _ssList.style.display; if ("Enter" === e.key || " " === e.key) { e.preventDefault(); _ssList.style.display = isOpen ? "none" : ""; subtitleSelect.setAttribute("aria-expanded", isOpen ? "false" : "true"); } else if ("Escape" === e.key && isOpen) { e.preventDefault(); _ssList.style.display = "none"; subtitleSelect.setAttribute("aria-expanded", "false"); } else if ("ArrowDown" === e.key || "ArrowUp" === e.key) { e.preventDefault(); if (!isOpen) { _ssList.style.display = ""; subtitleSelect.setAttribute("aria-expanded", "true"); } const opts = subtitleSelect._options; if (0 === opts.length) { return; } const currentIdx = opts.findIndex(o => String(o.value) === subtitleSelect._value); const nextIdx = "ArrowDown" === e.key ? Math.min(currentIdx + 1, opts.length - 1) : Math.max(currentIdx - 1, 0); subtitleSelect.value = String(opts[nextIdx].value); } }); const _ac = new AbortController; document.addEventListener("click", e => { if (!subtitleSelect.contains(e.target)) { _ssList.style.display = "none"; subtitleSelect.setAttribute("aria-expanded", "false"); } }, { signal: _ac.signal }); subtitleSelect.destroy = () => _ac.abort(); return subtitleSelect; })(); const formatSelect = document.createElement("div"); formatSelect.role = "radiogroup"; formatSelect.value = "srt"; Object.assign(formatSelect.style, { display: "flex", gap: "8px", padding: "6px 0", borderRadius: "6px", width: "100%", alignItems: "center", justifyContent: "center", background: "transparent" }); [ "srt", "txt", "xml" ].forEach(fmt => { const btn = document.createElement("button"); btn.type = "button"; btn.setAttribute("role", "radio"); btn.setAttribute("aria-checked", "false"); btn.dataset.value = fmt; btn.textContent = fmt.toUpperCase(); Object.assign(btn.style, { padding: "6px 12px", borderRadius: "999px", border: "1px solid rgba(255,255,255,0.08)", background: "rgba(255,255,255,0.02)", color: "#fff", cursor: "pointer", fontSize: "13px", fontWeight: "600" }); btn.addEventListener("click", () => { Array.from(formatSelect.children).forEach(c => { c.style.background = "transparent"; c.style.color = "#fff"; c.style.border = "1px solid rgba(255,255,255,0.08)"; c.setAttribute && c.setAttribute("aria-checked", "false"); }); btn.style.background = "#111"; btn.style.color = "#10c56a"; btn.style.border = "1px solid rgba(16,197,106,0.15)"; btn.setAttribute("aria-checked", "true"); formatSelect.value = fmt; }); formatSelect.appendChild(btn); }); const _defaultFmtBtn = Array.from(formatSelect.children).find(c => c.dataset.value === formatSelect.value); _defaultFmtBtn && _defaultFmtBtn.click(); subtitleWrapper.appendChild(subtitleSelect); subtitleWrapper.appendChild(formatSelect); const cancelBtn = document.createElement("button"); cancelBtn.type = "button"; cancelBtn.textContent = t("cancel"); Object.assign(cancelBtn.style, { padding: "8px 16px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.12)", background: "transparent", cursor: "pointer", fontSize: "14px", color: "#fff" }); const downloadBtn = document.createElement("button"); downloadBtn.type = "button"; downloadBtn.textContent = t("download"); Object.assign(downloadBtn.style, { padding: "8px 20px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.12)", background: "transparent", color: "#fff", cursor: "pointer", fontSize: "14px", fontWeight: "600" }); const progressWrapper = document.createElement("div"); progressWrapper.style.display = "none"; progressWrapper.style.marginTop = "12px"; const progressBar = document.createElement("div"); Object.assign(progressBar.style, { width: "100%", height: "3px", background: "#e0e0e0", borderRadius: "5px", overflow: "hidden", marginBottom: "6px" }); const progressFill = document.createElement("div"); Object.assign(progressFill.style, { width: "0%", height: "100%", background: "#1a73e8", transition: "width 200ms linear" }); progressBar.appendChild(progressFill); const progressText = document.createElement("div"); progressText.style.fontSize = "12px"; progressText.style.color = "#666"; progressWrapper.appendChild(progressBar); progressWrapper.appendChild(progressText); return { qualitySelect, embedLabel, subtitleWrapper, subtitleSelect, formatSelect, cancelBtn, downloadBtn, progressWrapper, progressFill, progressText }; } function enableFormControls(formParts) { try { formParts.qualitySelect && (formParts.qualitySelect.disabled = !1); formParts.downloadBtn && (formParts.downloadBtn.disabled = !1); formParts.cancelBtn && (formParts.cancelBtn.disabled = !1); if (formParts.downloadBtn) { formParts.downloadBtn.style.opacity = "1"; formParts.downloadBtn.style.cursor = "pointer"; formParts.downloadBtn.style.pointerEvents = "auto"; } } catch (e) { console.error("Error enabling form controls:", e); } } function wireModalEvents(formParts, activeFormatGetter, getSubtitlesData) { formParts.cancelBtn.addEventListener("click", () => closeModal()); formParts.downloadBtn.addEventListener("click", async () => { if (formParts.downloadBtn.disabled) { return; } !(function disableFormControls(formParts) { try { formParts.qualitySelect && (formParts.qualitySelect.disabled = !0); if (formParts.downloadBtn) { formParts.downloadBtn.disabled = !0; formParts.downloadBtn.style.opacity = "0.5"; formParts.downloadBtn.style.cursor = "not-allowed"; } formParts.cancelBtn && (formParts.cancelBtn.disabled = !0); } catch (e) { console.error("Error disabling form controls:", e); } })(formParts); !(function initializeProgress(formParts) { formParts.progressWrapper.style.display = ""; formParts.progressFill.style.width = "0%"; formParts.progressText.textContent = t("starting"); })(formParts); const format = activeFormatGetter(); try { "subtitle" === format ? await (async function handleSubtitleDownload(formParts, getSubtitlesData) { const subtitlesData = getSubtitlesData(); const selectedIndex = parseInt(formParts.subtitleSelect.value, 10); const subtitle = subtitlesData.all[selectedIndex]; const subtitleFormat = formParts.formatSelect.value; if (!subtitle) { throw new Error(t("noSubtitleSelected")); } const videoId = getVideoId(); await downloadSubtitle({ videoId, url: subtitle.url, languageCode: subtitle.languageCode, languageName: subtitle.name, format: subtitleFormat, translateTo: subtitle.translateTo || null }); })(formParts, getSubtitlesData) : await (async function handleMediaDownload(formParts, format) { const opts = { format, quality: formParts.qualitySelect.value, audioBitrate: formParts.qualitySelect.value, embedThumbnail: "audio" === format, onProgress: p => { formParts.progressFill.style.width = `${p.percent || 0}%`; formParts.progressText.textContent = `${p.percent || 0}% • ${formatBytes(p.loaded || 0)} / ${p.total ? formatBytes(p.total) : "—"}`; } }; await downloadVideo(opts); })(formParts, format); !(function completeDownload(formParts) { formParts.progressText.textContent = t("completed"); setTimeout(() => closeModal(), 800); })(formParts); } catch (err) { console.error("[Download Error]:", err); !(function handleDownloadError(formParts, err) { const errorMsg = err?.message || "Unknown error"; formParts.progressText.textContent = `${t("downloadFailed")} ${errorMsg}`; formParts.progressText.style.color = "#ff5555"; enableFormControls(formParts); setTimeout(() => { try { enableFormControls(formParts); } catch (e) { console.error("Failed to re-enable controls:", e); } }, 500); setTimeout(() => { formParts.progressText.style.color = "#fff"; }, 3e3); })(formParts, err); } finally { setTimeout(() => { formParts.downloadBtn && !formParts.downloadBtn.disabled || enableFormControls(formParts); }, 1e3); } }); } function updateQualityOptionsForForm(formParts, activeFormat, subtitlesData) { if ("subtitle" === activeFormat) { formParts.qualitySelect.style.display = "none"; formParts.embedLabel.style.display = "none"; formParts.subtitleWrapper.style.display = "block"; !(async function loadSubtitlesForForm(formParts, subtitlesData) { const videoId = getVideoId(); if (videoId) { formParts.subtitleSelect.setPlaceholder(t("loading")); formParts.subtitleSelect.disabled = !0; try { const data = await getSubtitles(videoId); if (!data) { formParts.subtitleSelect.setPlaceholder(t("noSubtitles")); return; } subtitlesData.original = data.subtitles; subtitlesData.translated = data.autoTransSubtitles.map(autot => ({ ...autot, url: data.subtitles[0]?.url || "", translateTo: autot.languageCode })); subtitlesData.all = [ ...subtitlesData.original, ...subtitlesData.translated ]; if (0 === subtitlesData.all.length) { formParts.subtitleSelect.setPlaceholder(t("noSubtitles")); return; } const opts = subtitlesData.all.map((sub, idx) => ({ value: idx, text: sub.name + (sub.translateTo ? t("autoTranslateSuffix") : "") })); formParts.subtitleSelect.setOptions(opts); formParts.subtitleSelect.disabled = !1; } catch (err) { logger.error("Failed to load subtitles:", err); formParts.subtitleSelect.setPlaceholder(t("subtitleLoadError")); } } })(formParts, subtitlesData); return; } if ("video" === activeFormat) { formParts.qualitySelect.style.display = "flex"; formParts.embedLabel.style.display = "none"; formParts.subtitleWrapper.style.display = "none"; formParts.qualitySelect.replaceChildren(); const lowQuals = DownloadConfig.VIDEO_QUALITIES.filter(q => parseInt(q, 10) <= 1080); const highQuals = DownloadConfig.VIDEO_QUALITIES.filter(q => parseInt(q, 10) > 1080); function makeQualityButton(q) { const btn = document.createElement("button"); btn.type = "button"; btn.setAttribute("role", "radio"); btn.setAttribute("aria-checked", "false"); btn.dataset.value = q; btn.textContent = `${q}p`; Object.assign(btn.style, { display: "inline-flex", alignItems: "center", gap: "8px", padding: "8px 12px", borderRadius: "999px", border: "1px solid rgba(255,255,255,0.08)", background: "rgba(255,255,255,0.02)", color: "#fff", cursor: "pointer", fontSize: "13px", fontWeight: "600" }); btn.addEventListener("click", () => { Array.from(formParts.qualitySelect.children).forEach(c => { if (c.dataset && c.dataset.value) { c.style.background = "transparent"; c.style.color = "#fff"; c.style.border = "1px solid rgba(255,255,255,0.08)"; c.setAttribute && c.setAttribute("aria-checked", "false"); } }); btn.style.background = "#111"; btn.style.color = "#10c56a"; btn.style.border = "1px solid rgba(16,197,106,0.15)"; btn.setAttribute("aria-checked", "true"); formParts.qualitySelect.value = q; }); return btn; } lowQuals.forEach(q => formParts.qualitySelect.appendChild(makeQualityButton(q))); if (highQuals.length > 0) { const labelWrap = document.createElement("div"); Object.assign(labelWrap.style, { display: "flex", alignItems: "center", gap: "12px", width: "100%", margin: "8px 0" }); const lineLeft = document.createElement("div"); lineLeft.style.flex = "1"; lineLeft.style.borderTop = "1px solid rgba(255,255,255,0.06)"; const label = document.createElement("div"); label.textContent = t("vp9Label"); Object.assign(label.style, { fontSize: "12px", color: "rgba(255,255,255,0.7)", padding: "0 8px" }); const lineRight = document.createElement("div"); lineRight.style.flex = "1"; lineRight.style.borderTop = "1px solid rgba(255,255,255,0.06)"; labelWrap.appendChild(lineLeft); labelWrap.appendChild(label); labelWrap.appendChild(lineRight); formParts.qualitySelect.appendChild(labelWrap); highQuals.forEach(q => formParts.qualitySelect.appendChild(makeQualityButton(q))); } formParts.qualitySelect.value = DownloadConfig.DEFAULTS.videoQuality; const defaultBtn = Array.from(formParts.qualitySelect.children).find(c => c.dataset && c.dataset.value === formParts.qualitySelect.value); defaultBtn && defaultBtn.click(); return; } formParts.qualitySelect.style.display = "flex"; formParts.embedLabel.style.display = "flex"; formParts.subtitleWrapper.style.display = "none"; formParts.qualitySelect.replaceChildren(); DownloadConfig.AUDIO_BITRATES.forEach(b => { const btn = document.createElement("button"); btn.type = "button"; btn.setAttribute("role", "radio"); btn.setAttribute("aria-checked", "false"); btn.dataset.value = b; btn.textContent = `${b} kbps`; Object.assign(btn.style, { display: "inline-flex", alignItems: "center", gap: "8px", padding: "8px 12px", borderRadius: "999px", border: "1px solid rgba(255,255,255,0.08)", background: "rgba(255,255,255,0.02)", color: "#fff", cursor: "pointer", fontSize: "13px", fontWeight: "600" }); btn.addEventListener("click", () => { Array.from(formParts.qualitySelect.children).forEach(c => { c.style.background = "transparent"; c.style.color = "#fff"; c.style.border = "1px solid rgba(255,255,255,0.08)"; c.setAttribute && c.setAttribute("aria-checked", "false"); }); btn.style.background = "#111"; btn.style.color = "#10c56a"; btn.style.border = "1px solid rgba(16,197,106,0.15)"; btn.setAttribute("aria-checked", "true"); formParts.qualitySelect.value = b; }); formParts.qualitySelect.appendChild(btn); }); formParts.qualitySelect.value = DownloadConfig.DEFAULTS.audioBitrate; const defaultAudioBtn = Array.from(formParts.qualitySelect.children).find(c => c.dataset.value === formParts.qualitySelect.value); defaultAudioBtn && defaultAudioBtn.click(); formParts.embedLabel.style.display = "none"; } function createModalUI() { if (_modalElements) { return _modalElements; } let activeFormat = "video"; const subtitlesData = { all: [], original: [], translated: [] }; const overlay = document.createElement("div"); Object.assign(overlay.style, { position: "fixed", inset: "0", background: "rgba(0,0,0,0.6)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: "999999" }); const box = document.createElement("div"); Object.assign(box.style, { width: "420px", maxWidth: "94%", background: "rgba(20,20,20,0.64)", color: "#fff", borderRadius: "12px", boxShadow: "0 8px 40px rgba(0,0,0,0.6)", fontFamily: "Arial, sans-serif", border: "1px solid rgba(255,255,255,0.06)", backdropFilter: "blur(8px)" }); const formParts = buildModalForm(); const tabContainer = (function createTabButtons(onTabChange) { const tabContainer = document.createElement("div"); tabContainer.setAttribute("role", "tablist"); Object.assign(tabContainer.style, { display: "flex", gap: "8px", padding: "12px", justifyContent: "center", alignItems: "center", background: "transparent" }); const videoTab = document.createElement("button"); videoTab.textContent = t("videoTab"); videoTab.dataset.format = "video"; const audioTab = document.createElement("button"); audioTab.textContent = t("audioTab"); audioTab.dataset.format = "audio"; const subTab = document.createElement("button"); subTab.textContent = t("subtitleTab"); subTab.dataset.format = "subtitle"; [ videoTab, audioTab, subTab ].forEach(btn => { Object.assign(btn.style, { flex: "initial", padding: "8px 18px", border: "1px solid rgba(255,255,255,0.06)", background: "transparent", cursor: "pointer", fontSize: "13px", fontWeight: "600", transition: "all 0.18s ease", color: "#666", borderRadius: "999px" }); btn.type = "button"; btn.setAttribute("role", "tab"); btn.setAttribute("aria-selected", "false"); btn.style.outline = "none"; btn.style.userSelect = "none"; }); function setActive(btn) { [ videoTab, audioTab, subTab ].forEach(b => { b.style.background = "transparent"; b.style.color = "#666"; b.style.border = "1px solid rgba(255,255,255,0.06)"; b.style.boxShadow = "none"; b.setAttribute("aria-selected", "false"); }); Object.assign(btn.style, { background: "#10c56a", color: "#fff", border: "1px solid rgba(0,0,0,0.06)", boxShadow: "0 1px 0 rgba(0,0,0,0.04) inset" }); btn.setAttribute("aria-selected", "true"); try { onTabChange(btn.dataset.format); } catch {} } [ videoTab, audioTab, subTab ].forEach(btn => { btn.addEventListener("click", () => { setActive(btn); try { btn.blur(); } catch {} }); }); tabContainer.appendChild(videoTab); tabContainer.appendChild(audioTab); tabContainer.appendChild(subTab); tabContainer.addEventListener("keydown", e => { if ("ArrowLeft" !== e.key && "ArrowRight" !== e.key) { return; } const tabs = [ videoTab, audioTab, subTab ]; const idx = tabs.indexOf(document.activeElement); if (idx < 0) { return; } e.preventDefault(); const next = "ArrowRight" === e.key ? tabs[(idx + 1) % tabs.length] : tabs[(idx - 1 + tabs.length) % tabs.length]; next.focus(); next.click(); }); setTimeout(() => setActive(videoTab), 0); return tabContainer; })(format => { activeFormat = format; updateQualityOptionsForForm(formParts, activeFormat, subtitlesData); }); const content = document.createElement("div"); content.style.padding = "16px"; content.appendChild(formParts.qualitySelect); content.appendChild(formParts.embedLabel); content.appendChild(formParts.subtitleWrapper); content.appendChild(formParts.progressWrapper); const btnRow = document.createElement("div"); Object.assign(btnRow.style, { display: "flex", gap: "8px", padding: "16px", justifyContent: "center" }); btnRow.appendChild(formParts.cancelBtn); btnRow.appendChild(formParts.downloadBtn); box.appendChild(tabContainer); box.appendChild(content); box.appendChild(btnRow); overlay.appendChild(box); updateQualityOptionsForForm(formParts, activeFormat, subtitlesData); wireModalEvents(formParts, () => activeFormat, () => subtitlesData); _modalElements = { overlay, box, ...formParts }; return _modalElements; } function openModal() { const els = createModalUI(); if (els) { try { document.body.contains(els.overlay) || document.body.appendChild(els.overlay); } catch {} } } function closeModal() { if (_modalElements) { try { const ss = _modalElements.overlay?.querySelector('[role="listbox"]'); ss && "function" == typeof ss.destroy && ss.destroy(); _modalElements.overlay && _modalElements.overlay.parentNode && _modalElements.overlay.parentNode.removeChild(_modalElements.overlay); } catch {} _modalElements = null; } } const buildUrl = (template, videoId, videoUrl) => (template || "").replace("{videoId}", videoId || "").replace("{videoUrl}", encodeURIComponent(videoUrl || "")); const positionDropdown = (() => { let rafId = null; let pendingButton = null; let pendingDropdown = null; const applyPosition = () => { if (!pendingButton || !pendingDropdown) { return; } const rect = pendingButton.getBoundingClientRect(); const left = Math.max(8, rect.left + rect.width / 2 - 75); const bottom = Math.max(8, window.innerHeight - rect.top + 12); pendingDropdown.style.left = `${left}px`; pendingDropdown.style.bottom = `${bottom}px`; rafId = null; pendingButton = null; pendingDropdown = null; }; return (button, dropdown) => { pendingButton = button; pendingDropdown = dropdown; null === rafId && (rafId = requestAnimationFrame(applyPosition)); }; })(); const createDownloadActions = (tFn, ytUtils) => { const handleDirectDownload = async () => { const api = await (timeout => new Promise(resolve => { let waited = 0; if (void 0 !== window.YouTubePlusDownload) { return resolve(window.YouTubePlusDownload); } const id = setInterval(() => { waited += 200; if (void 0 !== window.YouTubePlusDownload) { clearInterval(id); return resolve(window.YouTubePlusDownload); } if (waited >= timeout) { clearInterval(id); return resolve(void 0); } }, 200); try { window.YouTubeUtils?.cleanupManager?.registerInterval && window.YouTubeUtils.cleanupManager.registerInterval(id); } catch {} }))(2e3); if (api) { try { if ("function" == typeof api.openModal) { api.openModal(); return; } if ("function" == typeof api.downloadVideo) { await api.downloadVideo({ format: "video", quality: "1080" }); return; } } catch (err) { console.error("[YouTube+] Direct download invocation failed:", err); } ytUtils.NotificationManager.show(tFn("directDownloadModuleNotAvailable"), { duration: 3e3, type: "error" }); } else { console.error("[YouTube+] Direct download module not loaded"); ytUtils.NotificationManager.show(tFn("directDownloadModuleNotAvailable"), { duration: 3e3, type: "error" }); } }; const handleYTDLDownload = url => { const videoId = new URLSearchParams(location.search).get("v"); const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href; navigator.clipboard.writeText(videoUrl).then(() => { ytUtils.NotificationManager.show(tFn("copiedToClipboard"), { duration: 2e3, type: "success" }); }).catch(() => { (async (text, tFn, notificationMgr) => { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); } else { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.left = "-9999px"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, text.length); document.execCommand("copy"); document.body.removeChild(ta); } notificationMgr.show(tFn("copiedToClipboard"), { duration: 2e3, type: "success" }); } catch { notificationMgr.show(tFn("copyFailed") || "Copy failed", { duration: 2e3, type: "error" }); } })(videoUrl, tFn, ytUtils.NotificationManager); }); window.open(url, "_blank"); }; return { handleDirectDownload, handleYTDLDownload, openDownloadSite: (url, isYTDL, isDirect, dropdown, button) => { dropdown.classList.remove("visible"); button.setAttribute("aria-expanded", "false"); isDirect ? handleDirectDownload() : isYTDL ? handleYTDLDownload(url) : window.open(url, "_blank"); } }; }; const setupDropdownHoverBehavior = (() => { let initialized = !1; const dropdownTimers = new WeakMap; const setTimer = (element, timerId) => dropdownTimers.set(element, timerId); const clearTimer = element => { const timerId = (element => dropdownTimers.get(element))(element); if (void 0 !== timerId) { clearTimeout(timerId); dropdownTimers.delete(element); } }; const showDropdown = (button, dropdown) => { clearTimer(button); clearTimer(dropdown); positionDropdown(button, dropdown); dropdown.classList.add("visible"); button.setAttribute("aria-expanded", "true"); }; const hideDropdown = (button, dropdown) => { clearTimer(button); clearTimer(dropdown); const timerId = setTimeout(() => { dropdown.classList.remove("visible"); button.setAttribute("aria-expanded", "false"); }, 180); setTimer(button, timerId); }; return () => { (() => { if (!initialized) { initialized = !0; document.addEventListener("mouseenter", e => { const button = e.target?.closest?.(".ytp-download-button"); if (button) { const dropdown = $(".download-options"); if (dropdown) { clearTimer(button); clearTimer(dropdown); showDropdown(button, dropdown); } return; } const dropdown = e.target?.closest?.(".download-options"); if (dropdown) { const button = $(".ytp-download-button"); if (button) { clearTimer(button); clearTimer(dropdown); showDropdown(button, dropdown); } } }, !0); document.addEventListener("mouseleave", e => { const button = e.target?.closest?.(".ytp-download-button"); if (button) { const dropdown = $(".download-options"); if (dropdown) { clearTimer(button); clearTimer(dropdown); const timerId = setTimeout(() => hideDropdown(button, dropdown), 180); setTimer(button, timerId); } return; } const dropdown = e.target?.closest?.(".download-options"); if (dropdown) { const button = $(".ytp-download-button"); if (button) { clearTimer(button); clearTimer(dropdown); const timerId = setTimeout(() => hideDropdown(button, dropdown), 180); setTimer(dropdown, timerId); } } }, !0); document.addEventListener("keydown", e => { const button = e.target?.closest?.(".ytp-download-button"); if (button && ("Enter" === e.key || " " === e.key)) { const dropdown = $(".download-options"); if (!dropdown) { return; } dropdown.classList.contains("visible") ? hideDropdown(button, dropdown) : showDropdown(button, dropdown); } }); } })(); }; })(); const createDownloadButtonManager = config => { const {settings, t: tFn, getElement, YouTubeUtils: ytUtils} = config; const actions = createDownloadActions(tFn, ytUtils); const buildDownloadSites = (tFn => (customization, enabledSites, videoId, videoUrl) => { const baseSites = [ { key: "externalDownloader", name: customization?.externalDownloader?.name || "SSYouTube", url: buildUrl(customization?.externalDownloader?.url || "https://ssyoutube.com/watch?v={videoId}", videoId, videoUrl), isYTDL: !1, isDirect: !1 }, { key: "ytdl", name: "by YTDL", url: "http://localhost:5005", isYTDL: !0, isDirect: !1 }, { key: "direct", name: tFn("directDownload"), url: "#", isYTDL: !1, isDirect: !0 } ]; const downloadSites = baseSites.filter(s => !1 !== enabledSites[s.key]); return { baseSites, downloadSites }; })(tFn); const addDownloadButton = controls => { if (!settings.enableDownload) { return; } try { const existingBtn = controls.querySelector(".ytp-download-button"); existingBtn && existingBtn.remove(); } catch {} const videoId = new URLSearchParams(location.search).get("v"); const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href; const customization = settings.downloadSiteCustomization || { externalDownloader: { name: "SSYouTube", url: "https://ssyoutube.com/watch?v={videoId}" } }; const enabledSites = settings.downloadSites || { externalDownloader: !0, ytdl: !0, direct: !0 }; const {downloadSites} = buildDownloadSites(customization, enabledSites, videoId, videoUrl); const button = (tFn => { const button = document.createElement("div"); button.className = "ytp-button ytp-download-button"; button.setAttribute("title", tFn("downloadOptions")); button.setAttribute("tabindex", "0"); button.setAttribute("role", "button"); button.setAttribute("aria-haspopup", "true"); button.setAttribute("aria-expanded", "false"); button.innerHTML = _createHTML('\n <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" style="display:block;margin:auto;vertical-align:middle;">\n <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>\n <polyline points="7 10 12 15 17 10"></polyline>\n <line x1="12" y1="15" x2="12" y2="3"></line>\n </svg>\n '); return button; })(tFn); if (1 === downloadSites.length) { const singleSite = downloadSites[0]; button.style.cursor = "pointer"; const tempDropdown = document.createElement("div"); button.addEventListener("click", () => actions.openDownloadSite(singleSite.url, singleSite.isYTDL, singleSite.isDirect, tempDropdown, button)); controls.insertBefore(button, controls.firstChild); return; } const dropdown = ((downloadSites, button, openDownloadSiteFn) => { const options = document.createElement("div"); options.className = "download-options"; options.setAttribute("role", "menu"); const list = document.createElement("div"); list.className = "download-options-list"; downloadSites.forEach(site => { const opt = document.createElement("div"); opt.className = "download-option-item"; opt.textContent = site.name; opt.setAttribute("role", "menuitem"); opt.setAttribute("tabindex", "0"); opt.dataset.url = site.url; opt.dataset.isYtdl = site.isYTDL ? "true" : "false"; opt.dataset.isDirect = site.isDirect ? "true" : "false"; list.appendChild(opt); }); const handleOptionActivate = item => { item && openDownloadSiteFn(item.dataset.url, "true" === item.dataset.isYtdl, "true" === item.dataset.isDirect, options, button); }; list.addEventListener("click", e => { const item = e.target?.closest?.(".download-option-item"); item && list.contains(item) && handleOptionActivate(item); }); list.addEventListener("keydown", e => { const item = e.target?.closest?.(".download-option-item"); item && list.contains(item) && ("Enter" !== e.key && " " !== e.key || handleOptionActivate(item)); }); options.appendChild(list); return options; })(downloadSites, button, actions.openDownloadSite); const existingDownload = $(".download-options"); existingDownload && existingDownload.remove(); try { document.body.appendChild(dropdown); } catch { button.appendChild(dropdown); } setupDropdownHoverBehavior(button, dropdown); try { if ("undefined" != typeof window) { window.youtubePlus = window.youtubePlus || {}; window.youtubePlus.downloadButtonManager = window.youtubePlus.downloadButtonManager || {}; window.youtubePlus.downloadButtonManager.addDownloadButton = controlsArg => addDownloadButton(controlsArg); window.youtubePlus.downloadButtonManager.refreshDownloadButton = () => { try { const btn = $(".ytp-download-button"); const dd = $(".download-options"); if (settings.enableDownload && (!btn || !dd)) { try { const controlsEl = $(".ytp-right-controls"); controlsEl && addDownloadButton(controlsEl); } catch {} } if (settings.enableDownload) { btn && (btn.style.display = ""); dd && (dd.style.display = ""); } else { btn && (btn.style.display = "none"); dd && (dd.style.display = "none"); } } catch {} }; window.youtubePlus.rebuildDownloadDropdown = () => { try { const controlsEl = $(".ytp-right-controls"); if (!controlsEl) { return; } window.youtubePlus.downloadButtonManager.addDownloadButton(controlsEl); window.youtubePlus.settings = window.youtubePlus.settings || settings; } catch (e) { console.warn("[YouTube+] rebuildDownloadDropdown failed:", e); } }; } } catch (e) { console.warn("[YouTube+] expose rebuildDownloadDropdown failed:", e); } controls.insertBefore(button, controls.firstChild); }; return { addDownloadButton, refreshDownloadButton: () => { const button = getElement(".ytp-download-button"); let dropdown = $(".download-options"); if (settings.enableDownload && (!button || !dropdown)) { try { const controlsEl = $(".ytp-right-controls"); if (controlsEl) { addDownloadButton(controlsEl); dropdown = $(".download-options"); } } catch (e) { logger && logger.warn && logger.warn("[YouTube+] recreate download button failed:", e); } } if (settings.enableDownload) { button && (button.style.display = ""); dropdown && (dropdown.style.display = ""); } else { button && (button.style.display = "none"); dropdown && (dropdown.style.display = "none"); } } }; }; let initialized = !1; function init() { if (!initialized) { initialized = !0; try { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+ Download] Unified module loaded"); window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+ Download] Use window.YouTubePlusDownload.downloadVideo() to download"); window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+ Download] Button manager available"); } catch {} } } if ("undefined" != typeof window) { window.YouTubePlusDownload = { downloadVideo, getSubtitles, downloadSubtitle, getVideoId, getVideoUrl, getVideoTitle, sanitizeFilename, formatBytes, DownloadConfig, openModal, init }; window.YouTubePlusDownloadButton = { createDownloadButtonManager }; } "undefined" != typeof window && (window.YouTubeDownload = { init, openModal, getVideoId, getVideoTitle, version: "3.0" }); const ensureInit = () => { (() => { try { const path = location.pathname || ""; return "/watch" === path || path.startsWith("/shorts"); } catch { return !1; } })() && ("function" == typeof requestIdleCallback ? requestIdleCallback(init, { timeout: 1500 }) : setTimeout(init, 0)); }; window.YouTubePlusLazyLoader ? window.YouTubePlusLazyLoader.register("download", ensureInit, { priority: 2 }) : (cb => { "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", cb, { once: !0 }) : cb(); })(ensureInit); window.YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", ensureInit, { passive: !0 }) : document.addEventListener("yt-navigate-finish", ensureInit, { passive: !0 }); })(); const {$, $$, byId} = window.YouTubeUtils || {}; const onDomReady = (() => { let ready = "loading" !== document.readyState; const queue = []; const run = () => { ready = !0; for (;queue.length; ) { const cb = queue.shift(); try { cb(); } catch (e) { console.warn("[YouTube+] DOMReady callback error:", e); } } }; ready || document.addEventListener("DOMContentLoaded", run, { once: !0 }); return cb => { ready ? cb() : queue.push(cb); }; })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const t = window.YouTubeUtils?.t || (key => key || ""); const config = { enabled: window.YouTubeUtils?.loadFeatureEnabled?.("enableScrollToTopButton") ?? !0, storageKey: "youtube_top_button_settings" }; let universalScrollHandler = null; let universalScrollContainer = null; const universalExtraScrollTargets = new Set; let universalAttachTimeoutIds = []; let _musicContainersCache = null; let _musicContainersCacheTime = 0; const resolveMusicContainers = () => { const now = Date.now(); if (_musicContainersCache && now - _musicContainersCacheTime < 5e3) { return _musicContainersCache; } _musicContainersCache = [ document.querySelector("ytmusic-app-layout #layout"), document.querySelector("ytmusic-app-layout"), document.querySelector("ytmusic-browse-response #contents"), document.querySelector("ytmusic-section-list-renderer") ].filter(Boolean); _musicContainersCacheTime = now; return _musicContainersCache; }; const getUniversalScrollContainer = () => { try { const host = window.location.hostname; const candidates = []; if ("music.youtube.com" === host) { const musicContainers = resolveMusicContainers(); candidates.push(...musicContainers); candidates.push(document.querySelector("ytmusic-tabbed-page #content"), document.querySelector("ytmusic-app-layout #content"), document.querySelector("#content"), document.querySelector("ytmusic-app")); } else { "studio.youtube.com" === host && candidates.push($("ytcp-entity-page #scrollable-content"), $("ytcp-app #content"), $("#main-content"), $("#content"), $("#main"), $("ytcp-app")); } candidates.push(document.scrollingElement, document.documentElement, document.body); for (const el of candidates) { if (el && el.scrollHeight > el.clientHeight + 50) { return el; } } if ("music.youtube.com" === host || "studio.youtube.com" === host) { return document.scrollingElement || document.documentElement; } } catch (e) { console.warn("[YouTube+] Error detecting scroll container:", e); } return document.scrollingElement || document.documentElement; }; let universalWindowScrollHandler = null; let musicSideScrollHandler = null; let musicSideScrollContainer = null; const getMusicSidePanelContainer = () => { if ("music.youtube.com" !== window.location.hostname) { return null; } const directSelectors = [ "ytmusic-player-queue #contents", "ytmusic-player-queue", "#side-panel #contents", "#side-panel", 'ytmusic-tab-renderer[page-type="MUSIC_PAGE_TYPE_QUEUE"] #contents', "ytmusic-queue #automix-contents", "ytmusic-queue #contents" ]; for (const sel of directSelectors) { try { const el = document.querySelector(sel); if (el && el.scrollHeight > el.clientHeight + 30) { return el; } } catch {} } const roots = [ document.querySelector("ytmusic-player-page"), document.querySelector("ytmusic-app-layout"), document.querySelector("ytmusic-app") ]; const selectors = [ "#side-panel", "#right-content", "ytmusic-player-queue", "ytmusic-queue", "ytmusic-tab-renderer[selected] #contents", ".side-panel" ]; for (const root of roots) { if (root) { for (const sel of selectors) { try { const el = root.querySelector(sel); if (el && el.scrollHeight > el.clientHeight + 30) { return el; } } catch {} } } } return null; }; const cleanupTopButtons = () => { try { const rightButton = byId("right-tabs-top-button"); rightButton && rightButton.remove(); } catch {} try { const playlistButton = byId("playlist-panel-top-button"); playlistButton && playlistButton.remove(); } catch {} (() => { try { const btn = byId("music-side-top-button"); btn && btn.remove(); } catch {} try { musicSideScrollHandler && musicSideScrollContainer && musicSideScrollContainer.removeEventListener("scroll", musicSideScrollHandler); } catch {} musicSideScrollHandler = null; musicSideScrollContainer = null; })(); (() => { try { const btn = byId("universal-top-button"); btn && btn.remove(); } catch {} try { universalScrollHandler && universalScrollContainer && universalScrollContainer.removeEventListener("scroll", universalScrollHandler); } catch {} try { universalWindowScrollHandler && window.removeEventListener("scroll", universalWindowScrollHandler); } catch {} try { if (universalWindowScrollHandler && universalExtraScrollTargets.size) { for (const target of universalExtraScrollTargets) { try { target.removeEventListener("scroll", universalWindowScrollHandler); target._ytpScrollAttached && (target._ytpScrollAttached = !1); } catch {} } } } catch {} try { universalAttachTimeoutIds.length && universalAttachTimeoutIds.forEach(id => clearTimeout(id)); } catch {} universalScrollHandler = null; universalScrollContainer = null; universalWindowScrollHandler = null; universalExtraScrollTargets.clear(); universalAttachTimeoutIds = []; })(); try { $$("#right-tabs .tab-content-cld").forEach(tab => { if (tab && tab._topButtonScrollHandler) { tab.removeEventListener("scroll", tab._topButtonScrollHandler); tab._topButtonScrollHandler = null; } }); } catch (e) { console.warn("[YouTube+] Error cleaning up tab scroll handlers:", e); } try { const rightTabsEl = document.getElementById("right-tabs"); if (rightTabsEl) { if (rightTabsEl._topButtonScrollHandler) { rightTabsEl.removeEventListener("scroll", rightTabsEl._topButtonScrollHandler); rightTabsEl._topButtonScrollHandler = null; } if (rightTabsEl._scrollCleanup) { rightTabsEl._scrollCleanup(); rightTabsEl._scrollCleanup = null; } } } catch {} try { const playlistScroll = $("ytd-playlist-panel-renderer #items"); if (playlistScroll && playlistScroll._topButtonScrollHandler) { playlistScroll.removeEventListener("scroll", playlistScroll._topButtonScrollHandler); playlistScroll._topButtonScrollHandler = null; } } catch {} }; let tabChangesObserver = null; let watchInitToken = 0; let isTabClickListenerAttached = !1; let tabDelegationHandler = null; let tabDelegationRegistered = !1; let tabCheckTimeoutId = null; let playlistPanelCheckTimeoutId = null; const isWatchPage = () => "/watch" === window.location.pathname; const isShortsPage = () => window.location.pathname.startsWith("/shorts"); const isTopButton = el => el && ("right-tabs-top-button" === el.id || "universal-top-button" === el.id || "playlist-panel-top-button" === el.id || "music-side-top-button" === el.id); const handleTopButtonActivate = button => { try { if (!button) { return; } if ("right-tabs-top-button" === button.id) { const activeTab = document.querySelector("#right-tabs .tab-content-cld:not(.tab-content-hidden)"); const rightTabsEl = document.getElementById("right-tabs"); const scrollTarget = rightTabsEl && rightTabsEl.scrollTop > 0 ? rightTabsEl : activeTab && activeTab.scrollTop > 0 ? activeTab : activeTab || rightTabsEl; if (scrollTarget) { "scrollBehavior" in document.documentElement.style ? scrollTarget.scrollTo({ top: 0, behavior: "smooth" }) : scrollTarget.scrollTop = 0; button.setAttribute("aria-label", "Scrolled to top"); setTimeout(() => { button.setAttribute("aria-label", t("scrollToTop")); }, 1e3); } return; } if ("universal-top-button" === button.id) { const host = window.location.hostname; const isMusic = "music.youtube.com" === host; const isStudio = "studio.youtube.com" === host; const target = isMusic || isStudio ? getUniversalScrollContainer() : universalScrollContainer || getUniversalScrollContainer(); const scrollToTop = el => { "scrollBehavior" in document.documentElement.style ? el.scrollTo({ top: 0, behavior: "smooth" }) : el.scrollTop = 0; }; target === window || target === document || target === document.body || target === document.documentElement ? window.scrollTo({ top: 0, behavior: "smooth" }) : target && "function" == typeof target.scrollTo && scrollToTop(target); if (isMusic) { window.scrollTo({ top: 0, behavior: "smooth" }); for (const c of resolveMusicContainers()) { c && c.scrollTop > 0 && scrollToTop(c); } } return; } if ("playlist-panel-top-button" === button.id) { const playlistPanel = $("ytd-playlist-panel-renderer"); const scrollContainer = playlistPanel ? $("#items", playlistPanel) : null; scrollContainer && ("scrollBehavior" in document.documentElement.style ? scrollContainer.scrollTo({ top: 0, behavior: "smooth" }) : scrollContainer.scrollTop = 0); return; } if ("music-side-top-button" === button.id) { const target = getMusicSidePanelContainer() || musicSideScrollContainer; target && ("scrollBehavior" in document.documentElement.style ? target.scrollTo({ top: 0, behavior: "smooth" }) : target.scrollTop = 0); } } catch (error) { console.error("[YouTube+][Enhanced] Error scrolling to top:", error); } }; const setupTopButtonDelegation = (() => { let attached = !1; return () => { if (attached) { return; } attached = !0; const delegator = window.YouTubePlusEventDelegation; if (delegator?.on) { delegator.on(document, "click", ".top-button", (ev, target) => { isTopButton(target) && handleTopButtonActivate(target); }); delegator.on(document, "keydown", ".top-button", (ev, target) => { if (isTopButton(target) && ("Enter" === ev.key || " " === ev.key)) { ev.preventDefault(); handleTopButtonActivate(target); } }); } else { const _cm = window.YouTubeUtils?.cleanupManager; const _clickHandler = ev => { const target = ev.target?.closest?.(".top-button"); isTopButton(target) && handleTopButtonActivate(target); }; const _keyHandler = ev => { const target = ev.target?.closest?.(".top-button"); if (isTopButton(target) && ("Enter" === ev.key || " " === ev.key)) { ev.preventDefault(); handleTopButtonActivate(target); } }; if (_cm?.registerListener) { _cm.registerListener(document, "click", _clickHandler, !0); _cm.registerListener(document, "keydown", _keyHandler, !0); } else { document.addEventListener("click", _clickHandler, !0); document.addEventListener("keydown", _keyHandler, !0); } } }; })(); const clearTimeoutSafe = id => { id && clearTimeout(id); return null; }; const addStyles = () => { if (byId("custom-styles")) { return; } const style = document.createElement("style"); style.id = "custom-styles"; style.textContent = '\n :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);}\n ::-webkit-scrollbar{width:var(--scrollbar-width)!important;height:var(--scrollbar-width)!important;}\n ::-webkit-scrollbar-track{background:var(--scrollbar-track)!important;border-radius:4px!important;}\n ::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb)!important;border-radius:4px!important;transition:background .2s!important;}\n ::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)!important;}\n ::-webkit-scrollbar-thumb:active{background:var(--scrollbar-thumb-active)!important;}\n ::-webkit-scrollbar-corner{background:transparent!important;}\n html,body,#content,#guide-content,#secondary,#comments,#chat,ytd-comments,ytd-watch-flexy,ytd-browse,ytd-search,ytd-playlist-panel-renderer,#right-tabs,.tab-content-cld,ytmusic-app-layout{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);}\n 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);}\n .top-button{position:fixed;bottom:16px;right:16px;width:40px;height:40px;background:var(--yt-top-btn-bg,rgba(0,0,0,.7));color:var(--yt-top-btn-color,#fff);border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2100;opacity:0;visibility:hidden;transition:all .3s cubic-bezier(0.4, 0, 0.2, 1);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);}\n .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);}\n .top-button:active{transform:translateY(-1px) scale(1.03);}\n .top-button:focus{outline:2px solid rgba(255,255,255,0.5);outline-offset:2px;}\n .top-button.visible{opacity:1;visibility:visible;}\n .top-button svg{transition:transform .2s ease;}\n .top-button:hover svg{transform:translateY(-1px) scale(1.1);}\n 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);}\n 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);}\n #right-tabs .top-button{position:absolute;z-index:1000;}\n ytd-watch-flexy:not([tyt-tab^="#"]) #right-tabs .top-button{display:none;}\n ytd-playlist-panel-renderer .top-button{position:absolute;z-index:1000;}\n ytd-watch-flexy[flexy] #movie_player, ytd-watch-flexy[flexy] #movie_player .html5-video-container, ytd-watch-flexy[flexy] .html5-main-video{width:100%!important; max-width:100%!important;}\n ytd-watch-flexy[flexy] .html5-main-video{height:auto!important; max-height:100%!important; object-fit:contain!important; transform:none!important;}\n ytd-watch-flexy[flexy] #player-container-outer, ytd-watch-flexy[flexy] #movie_player{display:flex!important; align-items:center!important; justify-content:center!important;}\n /* Return YouTube Dislike button styling */\n dislike-button-view-model button{min-width:fit-content!important;width:auto!important;}\n dislike-button-view-model .yt-spec-button-shape-next__button-text-content{display:inline-flex!important;align-items:center!important;justify-content:center!important;}\n #ytp-plus-dislike-text{display:inline-block!important;visibility:visible!important;opacity:1!important;margin-left:6px!important;font-size:1.4rem!important;line-height:2rem!important;font-weight:500!important;}\n ytd-segmented-like-dislike-button-renderer dislike-button-view-model button{min-width:fit-content!important;}\n ytd-segmented-like-dislike-button-renderer .yt-spec-button-shape-next__button-text-content{min-width:2.4rem!important;}\n /* Shorts-specific dislike button styling */\n ytd-reel-video-renderer dislike-button-view-model #ytp-plus-dislike-text{font-size:1.2rem!important;line-height:1.8rem!important;margin-left:4px!important;}\n ytd-reel-video-renderer dislike-button-view-model button{padding:8px 12px!important;min-width:auto!important;}\n ytd-shorts dislike-button-view-model .yt-spec-button-shape-next__button-text-content{display:inline-flex!important;min-width:auto!important;}\n '; (document.head || document.documentElement).appendChild(style); }; const handleScroll = (scrollContainer, button) => { try { if (!button || !scrollContainer) { return; } button.classList.toggle("visible", scrollContainer.scrollTop > 100); } catch (error) { console.error("[YouTube+][Enhanced] Error in handleScroll:", error); } }; const setupScrollListener = (() => { let timeout; return () => { timeout && clearTimeout(timeout); timeout = setTimeout(() => { try { $$(".tab-content-cld").forEach(tab => { if (tab._topButtonScrollHandler) { tab.removeEventListener("scroll", tab._topButtonScrollHandler); delete tab._topButtonScrollHandler; } if (tab._scrollObserver) { tab._scrollObserver.disconnect(); delete tab._scrollObserver; } window.YouTubePlusScrollManager?.removeAllListeners?.(tab); }); try { const prevRtEl = document.getElementById("right-tabs"); if (prevRtEl) { if (prevRtEl._topButtonScrollHandler) { prevRtEl.removeEventListener("scroll", prevRtEl._topButtonScrollHandler); delete prevRtEl._topButtonScrollHandler; } if (prevRtEl._scrollCleanup) { prevRtEl._scrollCleanup(); delete prevRtEl._scrollCleanup; } } } catch (e) { console.warn("[YouTube+] Error cleaning up right-tabs scroll handler:", e); } const activeTab = document.querySelector("#right-tabs .tab-content-cld:not(.tab-content-hidden)"); const button = byId("right-tabs-top-button"); if (activeTab && button) { const rightTabsEl = document.getElementById("right-tabs"); const rtIsScrollHost = rightTabsEl && rightTabsEl !== activeTab && rightTabsEl.scrollHeight > rightTabsEl.clientHeight + 10; const scrollTarget = rtIsScrollHost ? rightTabsEl : activeTab; if (window.YouTubePlusScrollManager) { const cleanup = window.YouTubePlusScrollManager.addScrollListener(scrollTarget, () => handleScroll(scrollTarget, button), { debounce: 100, runInitial: !0 }); scrollTarget._scrollCleanup = cleanup; } else { const debounceFunc = void 0 !== YouTubeUtils && YouTubeUtils.debounce ? YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounceFunc(() => handleScroll(scrollTarget, button), 100); scrollTarget._topButtonScrollHandler = scrollHandler; scrollTarget.addEventListener("scroll", scrollHandler, { passive: !0, capture: !1 }); handleScroll(scrollTarget, button); } } } catch (error) { console.error("[YouTube+][Enhanced] Error in setupScrollListener:", error); } }, 100); }; })(); const createUniversalButton = () => { try { setupTopButtonDelegation(); if (byId("universal-top-button")) { return; } if (!config.enabled) { return; } const rawContainer = getUniversalScrollContainer(); const scrollContainer = rawContainer === document.scrollingElement || rawContainer === document.documentElement || rawContainer === document.body ? window : rawContainer; universalScrollContainer = scrollContainer; const button = document.createElement("button"); button.id = "universal-top-button"; button.className = "top-button"; button.title = t("scrollToTop"); button.setAttribute("aria-label", t("scrollToTop")); button.innerHTML = _createHTML('<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>'); const host = window.location.hostname; "music.youtube.com" !== host && "studio.youtube.com" !== host || (button.style.zIndex = "10000"); document.body.appendChild(button); const debounceFunc = void 0 !== YouTubeUtils && YouTubeUtils.debounce ? YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounceFunc(() => { const offset = scrollContainer === window ? window.scrollY : scrollContainer.scrollTop; button.classList.toggle("visible", offset > 100); }, 100); universalScrollHandler = scrollHandler; scrollContainer.addEventListener("scroll", scrollHandler, { passive: !0 }); const initialOffset = scrollContainer === window ? window.scrollY : scrollContainer.scrollTop; button.classList.toggle("visible", initialOffset > 100); if ("music.youtube.com" === host || "studio.youtube.com" === host) { const getMusicContainers = () => { const base = resolveMusicContainers(); return scrollContainer === window || base.includes(scrollContainer) ? base : [ ...base, scrollContainer ]; }; const musicScrollCheck = debounceFunc(() => { let anyScrolled = window.scrollY > 100; if (!anyScrolled) { for (const c of getMusicContainers()) { if (c.scrollTop > 100) { anyScrolled = !0; break; } } } button.classList.toggle("visible", anyScrolled); }, 100); window.addEventListener("scroll", musicScrollCheck, { passive: !0 }); universalWindowScrollHandler = musicScrollCheck; const attachMusicScrollListeners = () => { const targets = [ document.querySelector("ytmusic-app-layout #layout"), document.querySelector("ytmusic-app-layout") ]; for (const target of targets) { if (target && !target._ytpScrollAttached) { target._ytpScrollAttached = !0; target.addEventListener("scroll", musicScrollCheck, { passive: !0 }); universalExtraScrollTargets.add(target); } } }; attachMusicScrollListeners(); universalAttachTimeoutIds.push(setTimeout(attachMusicScrollListeners, 1e3)); universalAttachTimeoutIds.push(setTimeout(attachMusicScrollListeners, 3e3)); } } catch (error) { console.error("[YouTube+][Enhanced] Error creating universal button:", error); } }; const createMusicSidePanelButton = () => { try { if ("music.youtube.com" !== window.location.hostname) { return; } setupTopButtonDelegation(); if (byId("music-side-top-button")) { return; } if (!config.enabled) { return; } const panel = getMusicSidePanelContainer(); if (!panel) { window.YouTubeUtils?.createRetryScheduler?.({ check: () => !(!byId("music-side-top-button") && config.enabled) || !!getMusicSidePanelContainer() && (createMusicSidePanelButton(), !0), maxAttempts: 8, interval: 500 }); return; } const button = document.createElement("button"); button.id = "music-side-top-button"; button.className = "top-button"; button.title = t("scrollToTop"); button.setAttribute("aria-label", t("scrollToTop")); button.innerHTML = _createHTML('<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>'); panel.style.position = panel.style.position || "relative"; button.style.position = "absolute"; button.style.bottom = "16px"; button.style.right = "16px"; button.style.zIndex = "1000"; panel.appendChild(button); const debounceFunc = void 0 !== YouTubeUtils && YouTubeUtils.debounce ? YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounceFunc(() => { button.classList.toggle("visible", panel.scrollTop > 100); }, 100); musicSideScrollContainer = panel; musicSideScrollHandler = scrollHandler; panel.addEventListener("scroll", scrollHandler, { passive: !0 }); button.classList.toggle("visible", panel.scrollTop > 100); } catch (error) { console.error("[YouTube+][Enhanced] Error creating music side button:", error); } }; const dislikeCache = new Map; let dislikeObserver = null; let dislikePollTimer = null; const formatCompactNumber = number => { try { return new Intl.NumberFormat((() => { if (window.YouTubePlusI18n?.getLanguage) { return window.YouTubePlusI18n.getLanguage(); } if (window.YouTubeUtils?.getLanguage) { return window.YouTubeUtils.getLanguage(); } const htmlLang = document.documentElement.lang || "en"; return htmlLang.startsWith("ru") ? "ru" : "en"; })() || "en", { notation: "compact", compactDisplay: "short" }).format(Number(number) || 0); } catch { return String(number || 0); } }; const getVideoIdForDislike = () => { try { const urlObj = new URL(window.location.href); const pathname = urlObj.pathname || ""; if (pathname.startsWith("/shorts/")) { return pathname.slice(8); } if (pathname.startsWith("/clip/")) { const meta = $("meta[itemprop='videoId'], meta[itemprop='identifier']"); return meta?.getAttribute("content") || null; } return urlObj.searchParams.get("v"); } catch { return null; } }; const getDislikeButton = () => { const isShorts = window.location.pathname.startsWith("/shorts"); if (isShorts) { return (() => { const activeReel = $("ytd-reel-video-renderer[is-active]"); if (activeReel) { const btn = $("dislike-button-view-model", activeReel) || $("like-button-view-model", activeReel)?.parentElement?.querySelector('[aria-label*="islike"]')?.closest("button")?.parentElement || $("#dislike-button", activeReel); if (btn) { return btn; } } const shortsContainer = $("ytd-shorts"); if (shortsContainer) { const btn = $("dislike-button-view-model", shortsContainer) || $("#dislike-button", shortsContainer); if (btn) { return btn; } } return $("dislike-button-view-model") || $("#dislike-button") || null; })(); } const buttons = $("ytd-menu-renderer.ytd-watch-metadata > div#top-level-buttons-computed") || $("ytd-menu-renderer.ytd-video-primary-info-renderer > div") || $("#menu-container #top-level-buttons-computed") || null; return (buttons => { if (!buttons) { return null; } const segmented = buttons.querySelector("ytd-segmented-like-dislike-button-renderer"); if (segmented) { const dislikeViewModel = segmented.querySelector("dislike-button-view-model") || segmented.querySelector("#segmented-dislike-button") || segmented.children[1]; if (dislikeViewModel) { return dislikeViewModel; } } const viewModel = buttons.querySelector("dislike-button-view-model"); if (viewModel) { return viewModel; } const dislikeBtn = buttons.querySelector('button[aria-label*="islike"]') || buttons.querySelector('button[aria-label*="Не нравится"]'); return dislikeBtn ? dislikeBtn.closest("dislike-button-view-model") || dislikeBtn.parentElement : buttons.children && buttons.children[1] ? buttons.children[1] : null; })(buttons); }; const setDislikeDisplay = (dislikeButton, count) => { try { const container = (dislikeButton => { if (!dislikeButton) { return null; } const existingCustom = dislikeButton.querySelector("#ytp-plus-dislike-text"); if (existingCustom) { return existingCustom; } const textSpan = dislikeButton.querySelector("span.yt-core-attributed-string:not(#ytp-plus-dislike-text)") || dislikeButton.querySelector("#text") || dislikeButton.querySelector("yt-formatted-string") || dislikeButton.querySelector('span[role="text"]:not(#ytp-plus-dislike-text)') || dislikeButton.querySelector(".yt-spec-button-shape-next__button-text-content"); if (textSpan && "ytp-plus-dislike-text" !== textSpan.id) { textSpan.id = "ytp-plus-dislike-text"; return textSpan; } const viewModelHost = dislikeButton.closest("ytDislikeButtonViewModelHost") || dislikeButton; const buttonShape = viewModelHost.querySelector("button-view-model button") || viewModelHost.querySelector("button[aria-label]") || dislikeButton.querySelector("button") || dislikeButton; let textContainer = buttonShape.querySelector(".yt-spec-button-shape-next__button-text-content"); const created = document.createElement("span"); created.id = "ytp-plus-dislike-text"; created.setAttribute("role", "text"); created.className = "yt-core-attributed-string yt-core-attributed-string--white-space-no-wrap"; const isShorts = window.location.pathname.startsWith("/shorts"); created.style.cssText = isShorts ? "margin-left: 4px; font-size: 1.2rem; line-height: 1.8rem; font-weight: 500; min-width: 1.5em; display: inline-block; text-align: center;" : "margin-left: 6px; font-size: 1.4rem; line-height: 2rem; font-weight: 500; min-width: 2em; display: inline-block; text-align: center;"; try { if (textContainer) { textContainer.appendChild(created); } else { textContainer = document.createElement("div"); textContainer.className = "yt-spec-button-shape-next__button-text-content"; textContainer.appendChild(created); buttonShape.appendChild(textContainer); } buttonShape.style.minWidth = "auto"; buttonShape.style.width = "auto"; viewModelHost !== dislikeButton && (viewModelHost.style.minWidth = "auto"); } catch (e) { console.warn("YTP: Failed to create dislike text:", e); } return created; })(dislikeButton); if (!container) { return; } const formatted = formatCompactNumber(count); if (container.innerText !== String(formatted)) { container.innerText = String(formatted); container.style.display = "inline-block"; container.style.visibility = "visible"; container.style.opacity = "1"; const buttonShape = container.closest("button") || dislikeButton.querySelector("button"); if (buttonShape) { buttonShape.style.minWidth = "fit-content"; buttonShape.style.width = "auto"; } } } catch (e) { console.warn("YTP: Failed to set dislike display:", e); } }; const initReturnDislike = async () => { try { if (dislikePollTimer) { return; } const checkButton = async () => { const btn = getDislikeButton(); if (btn) { if (dislikePollTimer) { dislikePollTimer.disconnect(); dislikePollTimer = null; } const vid = getVideoIdForDislike(); const val = await (async videoId => { if (!videoId) { return 0; } const cached = dislikeCache.get(videoId); if (cached && Date.now() < cached.expiresAt) { return cached.value; } if (dislikeCache.size > 50) { const now = Date.now(); for (const [key, entry] of dislikeCache) { now >= entry.expiresAt && dislikeCache.delete(key); } if (dislikeCache.size > 50) { const iter = dislikeCache.keys(); for (;dislikeCache.size > 25; ) { const next = iter.next(); if (next.done) { break; } dislikeCache.delete(next.value); } } } try { if ("undefined" != typeof GM_xmlhttpRequest) { const text = await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => reject(new Error("timeout")), 8e3); GM_xmlhttpRequest({ method: "GET", url: `https://returnyoutubedislikeapi.com/votes?videoId=${encodeURIComponent(videoId)}`, timeout: 8e3, headers: { Accept: "application/json" }, onload: r => { clearTimeout(timeoutId); r.status >= 200 && r.status < 300 ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status}`)); }, onerror: e => { clearTimeout(timeoutId); reject(e || new Error("network")); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error("timeout")); } }); }); const parsed = JSON.parse(text || "{}"); const val = Number(parsed.dislikes || 0) || 0; dislikeCache.set(videoId, { value: val, expiresAt: Date.now() + 6e5 }); return val; } const controller = new AbortController; const id = setTimeout(() => controller.abort(), 8e3); try { const resp = await fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${encodeURIComponent(videoId)}`, { method: "GET", cache: "no-cache", signal: controller.signal, headers: { Accept: "application/json" } }); clearTimeout(id); if (!resp.ok) { throw new Error(`HTTP ${resp.status}`); } const json = await resp.json(); const val = Number(json.dislikes || 0) || 0; dislikeCache.set(videoId, { value: val, expiresAt: Date.now() + 6e5 }); return val; } finally { clearTimeout(id); } } catch { return 0; } })(vid); setDislikeDisplay(btn, val); (dislikeButton => { if (!dislikeButton) { return; } if (dislikeObserver) { dislikeObserver.disconnect(); dislikeObserver = null; } const existingText = dislikeButton.querySelector("#ytp-plus-dislike-text"); if (!existingText?.textContent || "0" === existingText.textContent) { dislikeObserver = new MutationObserver(() => { const vid = getVideoIdForDislike(); const cached = dislikeCache.get(vid); if (cached) { const btn = getDislikeButton(); btn && setDislikeDisplay(btn, cached.value); } }); try { dislikeObserver.observe(dislikeButton, { childList: !0, subtree: !0, attributes: !0 }); window.YouTubeUtils?.cleanupManager?.registerObserver?.(dislikeObserver, dislikeButton); } catch {} } })(btn); return !0; } return !1; }; if (await checkButton()) { return; } const isShorts = window.location.pathname.startsWith("/shorts"); const maxTime = 1e4; const startTime = Date.now(); dislikePollTimer = new MutationObserver(async () => { if (Date.now() - startTime > maxTime) { dislikePollTimer.disconnect(); dislikePollTimer = null; } else { await checkButton(); } }); const targetEl = $(isShorts ? "#shorts-container" : "ytd-watch-flexy #below"); if (targetEl) { dislikePollTimer.observe(targetEl, { childList: !0, subtree: !0 }); window.YouTubeUtils?.cleanupManager?.registerObserver?.(dislikePollTimer, targetEl); } else { const fallbackObs = new MutationObserver(async () => { if (Date.now() - startTime > maxTime) { fallbackObs.disconnect(); return; } const el = $(isShorts ? "#shorts-container" : "ytd-watch-flexy #below"); if (el) { fallbackObs.disconnect(); if (await checkButton()) { return; } dislikePollTimer.observe(el, { childList: !0, subtree: !0 }); window.YouTubeUtils?.cleanupManager?.registerObserver?.(dislikePollTimer, el); } }); if (document.body) { const narrowTarget = document.querySelector("#page-manager") || document.body; fallbackObs.observe(narrowTarget, { childList: !0, subtree: !0 }); window.YouTubeUtils?.cleanupManager?.registerObserver?.(fallbackObs); } } } catch (e) { console.warn("[YouTube+] Failed to initialize Return YouTube Dislike:", e); } }; const needsUniversalButton = () => { const host = window.location.hostname; if ("music.youtube.com" === host || "studio.youtube.com" === host) { return !0; } if (isWatchPage() || isShortsPage()) { return !1; } const path = window.location.pathname; const {search} = window.location; return "/results" === path && search.includes("search_query=") || "/playlist" === path && search.includes("list="), !0; }; const handleTabButtonClick = e => { try { const {target} = e; const tabButton = target?.closest?.(".tab-btn[tyt-tab-content]"); tabButton && setTimeout(setupScrollListener, 100); } catch (error) { console.error("[YouTube+][Enhanced] Error in click handler:", error); } }; const stopWatchEnhancements = () => { watchInitToken++; try { tabCheckTimeoutId && "object" == typeof tabCheckTimeoutId && tabCheckTimeoutId.stop ? tabCheckTimeoutId.stop() : tabCheckTimeoutId = clearTimeoutSafe(tabCheckTimeoutId); } catch {} tabCheckTimeoutId = null; try { playlistPanelCheckTimeoutId && "object" == typeof playlistPanelCheckTimeoutId && playlistPanelCheckTimeoutId.stop ? playlistPanelCheckTimeoutId.stop() : playlistPanelCheckTimeoutId = clearTimeoutSafe(playlistPanelCheckTimeoutId); } catch {} playlistPanelCheckTimeoutId = null; try { tabChangesObserver?.disconnect?.(); if (tabChangesObserver) { try { window.YouTubeUtils?.ObserverRegistry?.untrack?.(); } catch {} } } catch {} tabChangesObserver = null; (() => { try { if (!isTabClickListenerAttached) { return; } const delegator = window.YouTubePlusEventDelegation; tabDelegationRegistered && delegator?.off && tabDelegationHandler ? delegator.off(document, "click", ".tab-btn[tyt-tab-content]", tabDelegationHandler) : document.removeEventListener("click", handleTabButtonClick, !0); tabDelegationHandler = null; tabDelegationRegistered = !1; isTabClickListenerAttached = !1; } catch (error) { console.error("[YouTube+][Enhanced] Error cleaning up events:", error); } })(); try { (() => { try { if (dislikePollTimer) { "function" == typeof dislikePollTimer.disconnect ? dislikePollTimer.disconnect() : "number" == typeof dislikePollTimer && clearInterval(dislikePollTimer); dislikePollTimer = null; } if (dislikeObserver) { dislikeObserver.disconnect(); dislikeObserver = null; } $$("#ytp-plus-dislike-text").forEach(el => { try { el.parentNode && el.parentNode.removeChild(el); } catch {} }); dislikeCache.clear(); } catch (e) { console.warn("YTP: Dislike cleanup error:", e); } })(); } catch {} }; const startWatchEnhancements = () => { if (!config.enabled) { return; } if (!isWatchPage()) { return; } const token = ++watchInitToken; (() => { try { if (isTabClickListenerAttached) { return; } const delegator = window.YouTubePlusEventDelegation; if (delegator?.on) { tabDelegationHandler = (ev, target) => { target && setTimeout(setupScrollListener, 100); }; delegator.on(document, "click", ".tab-btn[tyt-tab-content]", tabDelegationHandler, { capture: !0 }); tabDelegationRegistered = !0; } else { document.addEventListener("click", handleTabButtonClick, !0); } isTabClickListenerAttached = !0; } catch (error) { console.error("[YouTube+][Enhanced] Error in setupEvents:", error); } })(); const tabScheduler = window.YouTubeUtils?.createRetryScheduler?.({ check: () => { if (token !== watchInitToken || !isWatchPage()) { return !0; } if ($("#right-tabs")) { (() => { try { setupTopButtonDelegation(); const rightTabs = $("#right-tabs"); if (!rightTabs || byId("right-tabs-top-button")) { return; } if (!config.enabled) { return; } const button = document.createElement("button"); button.id = "right-tabs-top-button"; button.className = "top-button"; button.title = t("scrollToTop"); button.setAttribute("aria-label", t("scrollToTop")); button.innerHTML = _createHTML('<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>'); rightTabs.style.position = "relative"; rightTabs.appendChild(button); setupScrollListener(); } catch (error) { console.error("[YouTube+][Enhanced] Error creating button:", error); } })(); try { tabChangesObserver?.disconnect?.(); } catch {} tabChangesObserver = (() => { try { const observer = new MutationObserver(mutations => { try { mutations.some(m => "attributes" === m.type && "class" === m.attributeName && m.target instanceof Element && m.target.classList.contains("tab-content-cld")) && setTimeout(setupScrollListener, 100); } catch (error) { console.error("[YouTube+][Enhanced] Error in mutation observer:", error); } }); try { window.YouTubeUtils?.ObserverRegistry?.track?.(); } catch {} const rightTabs = $("#right-tabs"); if (rightTabs) { observer.observe(rightTabs, { attributes: !0, subtree: !0, attributeFilter: [ "class" ] }); try { window.YouTubeUtils?.cleanupManager?.registerObserver?.(observer, rightTabs); } catch {} return observer; } try { window.YouTubeUtils?.ObserverRegistry?.untrack?.(); } catch {} return null; } catch (error) { console.error("[YouTube+][Enhanced] Error in observeTabChanges:", error); return null; } })(); return !0; } return !1; }, maxAttempts: 40, interval: 250 }); const playlistScheduler = window.YouTubeUtils?.createRetryScheduler?.({ check: () => { if (token !== watchInitToken || !isWatchPage()) { return !0; } try { const playlistPanel = $("ytd-playlist-panel-renderer"); if (playlistPanel && !byId("playlist-panel-top-button")) { (() => { try { setupTopButtonDelegation(); const playlistPanel = $("ytd-playlist-panel-renderer"); if (!playlistPanel || byId("playlist-panel-top-button")) { return; } if (!config.enabled) { return; } const button = document.createElement("button"); button.id = "playlist-panel-top-button"; button.className = "top-button"; button.title = t("scrollToTop"); button.setAttribute("aria-label", t("scrollToTop")); button.innerHTML = _createHTML('<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>'); const scrollContainer = $("#items", playlistPanel); if (!scrollContainer) { return; } playlistPanel.style.position = playlistPanel.style.position || "relative"; button.style.position = "absolute"; button.style.bottom = "16px"; button.style.right = "16px"; button.style.zIndex = "1000"; playlistPanel.appendChild(button); const debounceFunc = void 0 !== YouTubeUtils && YouTubeUtils.debounce ? YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const scrollHandler = debounceFunc(() => handleScroll(scrollContainer, button), 100); scrollContainer._topButtonScrollHandler = scrollHandler; scrollContainer.addEventListener("scroll", scrollHandler, { passive: !0 }); handleScroll(scrollContainer, button); const updateVisibility = () => { try { if (!playlistPanel.isConnected || playlistPanel.hidden) { button.style.display = "none"; return; } if (null === playlistPanel.offsetParent && "fixed" !== playlistPanel.style.position) { button.style.display = "none"; return; } const {width, height} = playlistPanel.getBoundingClientRect(); if (width < 40 || height < 40) { button.style.display = "none"; return; } if (!scrollContainer || 0 === scrollContainer.offsetHeight || 0 === scrollContainer.scrollHeight) { button.style.display = "none"; return; } button.style.display = ""; } catch { try { button.style.display = "none"; } catch {} } }; let ro = null; try { if ("undefined" != typeof ResizeObserver) { ro = new ResizeObserver(updateVisibility); ro.observe(playlistPanel); scrollContainer && ro.observe(scrollContainer); } } catch { ro = null; } const mo = new MutationObserver(updateVisibility); try { mo.observe(playlistPanel, { attributes: !0, attributeFilter: [ "class", "style", "hidden" ] }); } catch {} updateVisibility(); try { if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { YouTubeUtils.cleanupManager.registerObserver(mo, playlistPanel); ro && YouTubeUtils.cleanupManager.register(() => { try { ro.disconnect(); } catch {} }); } } catch {} } catch (error) { console.error("[YouTube+][Enhanced] Error creating playlist panel button:", error); } })(); return !0; } } catch (error) { console.error("[YouTube+][Enhanced] Error checking for playlist panel:", error); } return !1; }, maxAttempts: 30, interval: 300 }); tabCheckTimeoutId = tabScheduler; playlistPanelCheckTimeoutId = playlistScheduler; }; const init = () => { try { addStyles(); const checkPageType = () => { try { needsUniversalButton() && !byId("universal-top-button") && createUniversalButton(); "music.youtube.com" !== window.location.hostname || byId("music-side-top-button") || createMusicSidePanelButton(); } catch (error) { console.error("[YouTube+][Enhanced] Error checking page type:", error); } }; const onNavigate = () => { stopWatchEnhancements(); (() => { _musicContainersCache = null; _musicContainersCacheTime = 0; })(); checkPageType(); if (isWatchPage() || isShortsPage()) { try { initReturnDislike(); } catch (e) { console.warn("[YouTube+] initReturnDislike error:", e); } } startWatchEnhancements(); }; onNavigate(); window.YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", () => setTimeout(onNavigate, 200), { passive: !0 }) : window.addEventListener("yt-navigate-finish", () => { setTimeout(onNavigate, 200); }); if ("music.youtube.com" === window.location.hostname) { window.addEventListener("popstate", () => setTimeout(onNavigate, 200)); const sidePanelObserver = new MutationObserver(() => { !byId("music-side-top-button") && config.enabled && createMusicSidePanelButton(); }); const observeTarget = $("ytmusic-app-layout") || $("ytmusic-app") || document.body; if (observeTarget) { sidePanelObserver.observe(observeTarget, { childList: !0, subtree: !0 }); try { window.YouTubeUtils?.cleanupManager?.registerObserver?.(sidePanelObserver, observeTarget); } catch {} } } } catch (error) { console.error("[YouTube+][Enhanced] Error in initialization:", error); } }; window.addEventListener("youtube-plus-settings-updated", e => { try { const nextEnabled = !1 !== e?.detail?.enableScrollToTopButton; if (nextEnabled === config.enabled) { return; } config.enabled = nextEnabled; if (!config.enabled) { cleanupTopButtons(); stopWatchEnhancements(); return; } addStyles(); needsUniversalButton() && !byId("universal-top-button") && createUniversalButton(); "music.youtube.com" !== window.location.hostname || byId("music-side-top-button") || createMusicSidePanelButton(); startWatchEnhancements(); } catch {} }); onDomReady(() => { "function" == typeof requestIdleCallback ? requestIdleCallback(init, { timeout: 4e3 }) : setTimeout(init, 0); }); })(); !(function() { try { const host = "undefined" == typeof location ? "" : location.hostname; if (!host) { return; } if (!/(^|\.)youtube\.com$/.test(host) && !/\.youtube\.google/.test(host)) { return; } const SETTINGS_KEY = window.YouTubeUtils?.SETTINGS_KEY || "youtube_plus_settings"; const STYLE_ELEMENT_ID = "ytp-zen-features-style"; const NON_CRITICAL_STYLE_ID = "ytp-zen-features-style-noncritical"; const STYLE_MANAGER_KEY = "zen-features-style"; let nonCriticalTimer = null; const DEFAULTS = { enableZenStyles: !0, hideSideGuide: !1, zenStyles: { thumbnailHover: !0, immersiveSearch: !0, hideVoiceSearch: !0, transparentHeader: !0, hideSideGuide: !0, cleanSideGuide: !1, fixFeedLayout: !0, betterCaptions: !0, playerBlur: !0, theaterEnhancements: !0, misc: !0 } }; const loadSettings = () => { let parsed = null; try { const raw = localStorage.getItem(SETTINGS_KEY); raw && (parsed = JSON.parse(raw)); } catch (e) { console.warn("[YouTube+] Zen settings parse error:", e); } const merged = { ...DEFAULTS, ...parsed && "object" == typeof parsed ? parsed : null }; merged.zenStyles = { ...DEFAULTS.zenStyles, ...merged.zenStyles && "object" == typeof merged.zenStyles ? merged.zenStyles : null }; !0 === merged.hideSideGuide && !0 !== merged.zenStyles.hideSideGuide && (merged.zenStyles.hideSideGuide = !0); return merged; }; const CSS_BLOCKS = { thumbnailHover: "\n /* yt-thumbnail hover */\n #inline-preview-player {transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) 1s !important; transform: scale(1) !important;}\n #video-preview-container:has(#inline-preview-player) {transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; border-radius: 1.2em !important; overflow: hidden !important; transform: scale(1) !important;}\n #video-preview-container:has(#inline-preview-player):hover {transform: scale(1.25) !important; box-shadow: #0008 0px 0px 60px !important; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s !important;}\n ytd-app #content {opacity: 1 !important; transition: opacity 0.3s ease-in-out !important;}\n ytd-app:has(#video-preview-container:hover) #content {opacity: 0.5 !important; transition: opacity 4s ease-in-out 1s !important;}\n ", immersiveSearch: '\n /* yt-Immersive search */\n #page-manager, yt-searchbox {transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.35) !important;}\n #masthead yt-searchbox button[aria-label="Search"] {display: none !important;}\n .ytSearchboxComponentInputBox {border-radius: 2em !important;}\n yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) {position: relative !important; left: 0vw !important; top: -30vh !important; height: 40px !important; max-width: 600px !important; transform: scale(1) !important;}\n @media only screen and (min-width: 1400px) {yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) { height: 60px !important; max-width: 700px !important; transform: scale(1.1) !important;}}\n yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) .ytSearchboxComponentInputBox,\n yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {background-color: #fffb !important; box-shadow: black 0 0 30px !important;}\n @media (prefers-color-scheme: dark) {\n yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) .ytSearchboxComponentInputBox,\n yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {background-color: #000b !important;}\n }\n yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {margin-top: 10px !important;}\n @media only screen and (min-width: 1400px) {yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {margin-top: 30px !important;}}\n .ytd-masthead #center:has(.ytSearchboxComponentInputBoxHasFocus) {height: 100vh !important; width: 100vw !important; left: 0 !important; top: 0 !important; position: fixed !important; justify-content: center !important; align-items: center !important;}\n #content:has(.ytSearchboxComponentInputBoxHasFocus) #page-manager {filter: blur(20px) !important; transform: scale(1.05) !important;}\n ', hideVoiceSearch: "\n /* No voice search button */\n #voice-search-button {display: none !important;}\n ", transparentHeader: "\n /* Transparent header */\n #masthead-container, #background.ytd-masthead { background-color: transparent !important; }\n ", hideSideGuide: '\n /* Hide side guide */\n ytd-mini-guide-renderer, [theater=""] #contentContainer::after {display: none !important;}\n tp-yt-app-drawer > #contentContainer:not([opened=""]),\n #contentContainer:not([opened=""]) #guide-content,\n ytd-mini-guide-renderer,\n ytd-mini-guide-entry-renderer {background-color: var(--yt-spec-text-primary-inverse) !important; background: var(--yt-spec-text-primary-inverse) !important;}\n #content:not(:has(#contentContainer[opened=""])) #page-manager {margin-left: 0 !important;}\n ytd-app:not([guide-persistent-and-visible=""]) tp-yt-app-drawer > #contentContainer {background-color: var(--yt-spec-text-primary-inverse) !important;}\n ytd-alert-with-button-renderer {align-items: center !important; justify-content: center !important;}\n ', cleanSideGuide: '\n /* Clean side guide */\n ytd-guide-section-renderer:has([title="YouTube Premium"]),\n ytd-guide-renderer #footer {display: none !important;}\n ytd-guide-section-renderer, ytd-guide-collapsible-section-entry-renderer {border: none !important;}\n ', fixFeedLayout: '\n /* Fix new feed layout */\n ytd-rich-item-renderer[rendered-from-rich-grid] { @media only screen and (min-width: 1400px) { --ytd-rich-grid-items-per-row: 4 !important; @media only screen and (min-width: 1700px) { --ytd-rich-grid-items-per-row: 5 !important; @media only screen and (min-width: 2180px) {--ytd-rich-grid-items-per-row: 6 !important;}}}} ytd-rich-item-renderer[is-in-first-column=""] { margin-left: calc(var(--ytd-rich-grid-item-margin) / 2) !important;}#contents { padding-left: calc(var(--ytd-rich-grid-item-margin) / 2 + var(--ytd-rich-grid-gutter-margin)) !important;}\n ', betterCaptions: "\n /* Better captions */\n .caption-window { backdrop-filter: blur(10px) brightness(70%) !important; border-radius: 1em !important; padding: 1em !important; box-shadow: #0008 0 0 20px !important; width: fit-content !important; }\n .ytp-caption-segment { background: none !important; }\n ", playerBlur: "\n /* Player controls blur */\n .ytp-left-controls .ytp-play-button,\n .ytp-left-controls .ytp-volume-area,\n .ytp-left-controls .ytp-time-display.notranslate > span,\n .ytp-left-controls .ytp-chapter-container > button,\n .ytp-left-controls .ytp-prev-button,\n .ytp-left-controls .ytp-next-button,\n .ytp-right-controls,\n .ytp-time-wrapper,\n .ytPlayerQuickActionButtonsHost,\n .ytPlayerQuickActionButtonsHostCompactControls,\n .ytPlayerQuickActionButtonsHostDisableBackdropFilter { backdrop-filter: blur(5px) !important; background-color: #0004 !important; }\n .ytp-popup { backdrop-filter: blur(10px) !important; background-color: #0007 !important; }\n ", theaterEnhancements: "\n /* Zen view comments (from zeninternet) */\n /* Hide secondary column visually but break containment so fixed children can escape */\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #columns #secondary { display: block !important;width: 0 !important;min-width: 0 !important;max-width: 0 !important;padding: 0 !important;margin: 0 !important;border: 0 !important;overflow: visible !important;pointer-events: none !important;flex: 0 0 0px !important;contain: none !important;\n }\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #secondary-inner { overflow: visible !important;contain: none !important;position: static !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #secondary-inner secondary-wrapper,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #secondary-inner .tabview-secondary-wrapper { contain: none !important;overflow: visible !important;position: static !important;max-height: none !important;height: auto !important;padding: 0 !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #right-tabs { display: block !important;overflow: visible !important;contain: none !important;position: static !important;width: 0 !important;height: 0 !important;padding: 0 !important;margin: 0 !important;border: 0 !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #right-tabs > header { display: none !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #right-tabs .tab-content { display: block !important;overflow: visible !important;contain: none !important;position: static !important;width: 0 !important;height: 0 !important;padding: 0 !important;margin: 0 !important;border: 0 !important;}\n /* Break containment on tab-comments so its fixed-position child can escape */\n /* Extra .tab-content-hidden selector to beat main.js specificity (line 5169) */\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-cld { contain: none !important;overflow: visible !important;position: static !important;display: block !important;visibility: visible !important;width: 0 !important;height: 0 !important;padding: 0 !important;margin: 0 !important;z-index: auto !important;pointer-events: none !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments > ytd-item-section-renderer#sections,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments > ytd-item-section-renderer#sections > #contents,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments #contents { contain: none !important;width: auto !important;height: auto !important;max-height: none !important;overflow: visible !important;visibility: visible !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments #contents > * { display: block !important;}\n /* Hide other tabs content */\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-info,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-videos,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-list { display: none !important;}\n /* Comments overlay panel */\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) ytd-comments { visibility: visible !important;display: block !important;background-color: var(--yt-live-chat-shimmer-background-color) !important;backdrop-filter: blur(20px) !important;padding: 0 2em !important;border-radius: 2em 0 0 2em !important;max-height: calc(100vh - 120px) !important;overflow-y: auto !important;position: fixed !important;z-index: 2000 !important;top: 3vh !important;right: -42em !important;width: 40em !important;height: 90vh !important;opacity: 0 !important;pointer-events: auto !important;transition: opacity 0.4s ease, right 0.4s ease !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) ytd-comments:hover { opacity: 1 !important;right: 0 !important;}\n /* Transparent overlay chat — fixed panel */\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) [tyt-chat-container],\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat-container { contain: none !important;overflow: visible !important;position: static !important;display: block !important;pointer-events: none !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat { visibility: visible !important;display: block !important;position: fixed !important;top: 3vh !important;right: 0 !important;width: 400px !important;height: calc(100vh - 120px) !important;max-height: calc(100vh - 120px) !important;z-index: 2001 !important;opacity: 0.85 !important;pointer-events: auto !important;border-radius: 2em 0 0 2em !important;overflow: hidden !important;backdrop-filter: blur(20px) !important;transition: opacity 0.4s ease !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] { visibility: visible !important;display: block !important;position: fixed !important;top: 3vh !important;right: 0 !important;width: 400px !important;height: calc(100vh - 120px) !important;max-height: calc(100vh - 120px) !important;z-index: 2001 !important;opacity: 0.85 !important;pointer-events: auto !important;overflow: hidden !important;border-radius: 2em 0 0 2em !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] > #show-hide-button,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] > .ytd-live-chat-frame#show-hide-button { display: none !important;visibility: hidden !important;opacity: 0 !important;pointer-events: none !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] iframe { display: block !important;visibility: visible !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat iframe { height: 100% !important;width: 100% !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) yt-live-chat-renderer { background: transparent !important;}\n /* Ambient mode: fix black bars in theater */\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics-container,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics { position: absolute !important;top: 0 !important;left: 0 !important;width: 100% !important;height: 100% !important;overflow: hidden !important;pointer-events: none !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics canvas,\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics video { position: absolute !important;top: 50% !important;left: 50% !important;transform: translate(-50%, -50%) scale(1.2) !important;min-width: 100% !important;min-height: 100% !important;object-fit: cover !important;}\n ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #player-full-bleed-container { overflow: hidden !important;}\n ytd-watch-flexy[fullscreen] ytd-live-chat-frame { background-color: var(--app-drawer-content-container-background-color) !important;}\n ", misc: "\n /* Compact feed – reduced spacing, hover menus, inline details */\n ytd-rich-item-renderer { margin-bottom: 15px !important;}\n ytd-rich-item-renderer[rendered-from-rich-grid] { --ytd-rich-item-row-usable-width: calc(100% - var(--ytd-rich-grid-gutter-margin) * 1) !important;}\n ytd-rich-item-renderer #metadata.ytd-video-meta-block { flex-direction: row !important;}\n ytd-rich-item-renderer #metadata.ytd-video-meta-block #metadata-line span:nth-child(3) { height: 1em !important;margin-left: 1em !important;}\n ytd-rich-grid-media { border-radius: 1.2em;height: 100% !important;}\n ytd-rich-grid-media ytd-menu-renderer #button { opacity: 0 !important;transition: opacity 0.3s ease-in-out !important;}\n ytd-rich-grid-media:hover ytd-menu-renderer #button { opacity: 1 !important;}\n /* Show video meta on hover */\n #content #dismissible:hover ytd-video-meta-block { opacity: 1 !important;}\n #frosted-glass { display: none !important;}\n ", clsPrevention: "\n /* CLS Prevention - Reserve space for dynamic elements */\n #ytp-plus-dislike-text { min-width: 1.5em;display: inline-block !important;}\n /* Contain layout only for our own panels (not YouTube layout elements) */\n .ytp-plus-stats-panel, .ytp-plus-modal-content { contain: layout style;}\n /* Prevent layout shifts from search box animations */\n yt-searchbox { will-change: transform;}\n /* Reduce CLS from late-loading channel avatars */\n #owner #avatar { min-width: 40px; min-height: 40px; }\n /* Reserve space for action buttons to prevent shift */\n ytd-menu-renderer.ytd-watch-metadata { min-height: 36px; }\n /* Subscribe button stability */\n ytd-subscribe-button-renderer { min-width: 90px; }\n " }; const buildCriticalCss = settings => { const z = settings?.zenStyles || {}; let css = CSS_BLOCKS.clsPrevention; z.hideSideGuide && (css += CSS_BLOCKS.hideSideGuide); z.fixFeedLayout && (css += CSS_BLOCKS.fixFeedLayout); z.theaterEnhancements && (css += CSS_BLOCKS.theaterEnhancements); return css.trim(); }; const buildNonCriticalCss = settings => { const z = settings?.zenStyles || {}; let css = ""; z.thumbnailHover && (css += CSS_BLOCKS.thumbnailHover); z.immersiveSearch && (css += CSS_BLOCKS.immersiveSearch); z.hideVoiceSearch && (css += CSS_BLOCKS.hideVoiceSearch); z.transparentHeader && (css += CSS_BLOCKS.transparentHeader); z.cleanSideGuide && (css += CSS_BLOCKS.cleanSideGuide); z.betterCaptions && (css += CSS_BLOCKS.betterCaptions); z.playerBlur && (css += CSS_BLOCKS.playerBlur); z.misc && (css += CSS_BLOCKS.misc); return css.trim(); }; const removeStyles = () => { try { window.YouTubeUtils?.StyleManager?.remove && window.YouTubeUtils.StyleManager.remove(STYLE_MANAGER_KEY); } catch {} if (nonCriticalTimer) { if ("undefined" != typeof window && "function" == typeof window.cancelIdleCallback) { try { window.cancelIdleCallback(nonCriticalTimer); } catch {} } else { clearTimeout(nonCriticalTimer); } nonCriticalTimer = null; } const el = document.getElementById(STYLE_ELEMENT_ID); if (el) { try { el.remove(); } catch {} } const ncEl = document.getElementById(NON_CRITICAL_STYLE_ID); if (ncEl) { try { ncEl.remove(); } catch {} } }; const applyNonCriticalStyles = css => { if (!css) { const ncEl = document.getElementById(NON_CRITICAL_STYLE_ID); ncEl && ncEl.remove(); return; } let ncEl = document.getElementById(NON_CRITICAL_STYLE_ID); if (!ncEl) { ncEl = document.createElement("style"); ncEl.id = NON_CRITICAL_STYLE_ID; (document.head || document.documentElement).appendChild(ncEl); } ncEl.textContent = css; }; const applyStyles = settings => { const enabled = !1 !== settings?.enableZenStyles; if (!enabled) { removeStyles(); return; } const criticalCss = buildCriticalCss(settings); const nonCriticalCss = buildNonCriticalCss(settings); if (!criticalCss && !nonCriticalCss) { removeStyles(); return; } try { if (window.YouTubeUtils?.StyleManager?.add) { window.YouTubeUtils.StyleManager.add(STYLE_MANAGER_KEY, criticalCss || ""); const el = document.getElementById(STYLE_ELEMENT_ID); el && el.remove(); if (nonCriticalTimer) { if ("undefined" != typeof window && "function" == typeof window.cancelIdleCallback) { try { window.cancelIdleCallback(nonCriticalTimer); } catch {} } else { clearTimeout(nonCriticalTimer); } } nonCriticalTimer = "function" == typeof requestIdleCallback ? requestIdleCallback(() => applyNonCriticalStyles(nonCriticalCss), { timeout: 5e3 }) : setTimeout(() => applyNonCriticalStyles(nonCriticalCss), 200); return; } } catch {} let el = document.getElementById(STYLE_ELEMENT_ID); if (!el) { el = document.createElement("style"); el.id = STYLE_ELEMENT_ID; (document.head || document.documentElement).appendChild(el); } el.textContent = criticalCss || ""; if (nonCriticalTimer) { if ("undefined" != typeof window && "function" == typeof window.cancelIdleCallback) { try { window.cancelIdleCallback(nonCriticalTimer); } catch {} } else { clearTimeout(nonCriticalTimer); } } nonCriticalTimer = "function" == typeof requestIdleCallback ? requestIdleCallback(() => applyNonCriticalStyles(nonCriticalCss), { timeout: 5e3 }) : setTimeout(() => applyNonCriticalStyles(nonCriticalCss), 200); }; const applyFromStorage = () => applyStyles(loadSettings()); applyFromStorage(); window.addEventListener("youtube-plus-settings-updated", e => { try { applyStyles(e?.detail || loadSettings()); } catch { applyFromStorage(); } }); } catch (err) { console.error("zen-youtube-features injection failed", err); } })(); !(function() { "use strict"; const host = "undefined" == typeof location ? "" : location.hostname; if (!host) { return; } if (!/(^|\.)youtube\.com$/.test(host) && !/\.youtube\.google/.test(host)) { return; } const SETTINGS_KEY = window.YouTubeUtils?.SETTINGS_KEY || "youtube_plus_settings"; const isTheaterEnhancementEnabled = () => { const settings = (() => { try { const raw = localStorage.getItem(SETTINGS_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } })(); return !settings || !1 !== settings.enableZenStyles && (!settings.zenStyles || !1 !== settings.zenStyles.theaterEnhancements); }; const clickElement = element => { if (element) { try { element.dispatchEvent(new window.MouseEvent("click", { bubbles: !0, cancelable: !0, view: window })); } catch { try { element.click(); } catch {} } } }; let expandAttempts = 0; const runOverlayFixes = () => { if ("/watch" !== location.pathname) { return; } if (!isTheaterEnhancementEnabled()) { return; } const flexy = document.querySelector("ytd-watch-flexy"); if (!flexy || flexy.hasAttribute("fullscreen")) { return; } const isTheaterLike = flexy.hasAttribute("theater") || flexy.hasAttribute("full-bleed-player") || flexy.hasAttribute("theater-requested_"); if (isTheaterLike) { (() => { const chat = document.querySelector("ytd-live-chat-frame#chat"); if (!chat) { return; } if (chat.hasAttribute("collapsed")) { if (expandAttempts >= 3) { return; } expandAttempts++; let expanded = !1; try { const cnt = chat.polymerController || (void 0 !== chat.__data ? chat : null) || (chat.inst ? chat.inst : null); if (cnt && "function" == typeof cnt.setCollapsedState) { cnt.setCollapsedState({ setLiveChatCollapsedStateAction: { collapsed: !1 } }); expanded = !1 === cnt.collapsed; } if (!expanded && cnt && "boolean" == typeof cnt.collapsed) { cnt.collapsed = !1; !0 === cnt.isHiddenByUser && (cnt.isHiddenByUser = !1); expanded = !1 === cnt.collapsed; } } catch {} if (!expanded) { const showBtn = chat.querySelector("#show-hide-button div.yt-spec-touch-feedback-shape, #show-hide-button ytd-toggle-button-renderer, #show-hide-button button"); showBtn && clickElement(showBtn); } } const iframe = chat.querySelector("iframe#chatframe"); iframe && !iframe.src && chat.url && (iframe.src = chat.url); })(); (flexy => { const commentsTab = document.querySelector("#tab-comments"); const commentsBtn = document.querySelector('#material-tabs a[tyt-tab-content="#tab-comments"]'); if (!commentsTab || !commentsBtn || "1" === commentsTab.getAttribute("data-ytp-zen-comments-preloaded")) { return; } flexy && !flexy.hasAttribute("keep-comments-scroller") && flexy.setAttribute("keep-comments-scroller", ""); const activeBtn = document.querySelector("#material-tabs a[tyt-tab-content].active"); clickElement(commentsBtn); requestAnimationFrame(() => { commentsTab.setAttribute("data-ytp-zen-comments-preloaded", "1"); activeBtn && activeBtn !== commentsBtn && activeBtn.isConnected && clickElement(activeBtn); }); })(flexy); } }; let debounceTimer = 0; const scheduleRun = () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(runOverlayFixes, 150); }; const setupOverlayObservers = () => { const flexyObserver = new MutationObserver(scheduleRun); let observedFlexy = null; const attachFlexyObserver = () => { const flexy = document.querySelector("ytd-watch-flexy"); if (flexy && flexy !== observedFlexy) { observedFlexy && flexyObserver.disconnect(); flexyObserver.observe(flexy, { attributes: !0, attributeFilter: [ "theater", "full-bleed-player", "theater-requested_", "fullscreen" ] }); observedFlexy = flexy; } }; const chatObserver = new MutationObserver(scheduleRun); let observedChat = null; const attachChatObserver = () => { const chat = document.querySelector("ytd-live-chat-frame#chat"); if (chat && chat !== observedChat) { observedChat && chatObserver.disconnect(); chatObserver.observe(chat, { attributes: !0, attributeFilter: [ "collapsed" ] }); observedChat = chat; } }; attachFlexyObserver(); attachChatObserver(); window.addEventListener("yt-navigate-finish", () => { expandAttempts = 0; setTimeout(() => { attachFlexyObserver(); attachChatObserver(); scheduleRun(); }, 180); }, { passive: !0 }); try { if (window.YouTubeUtils?.cleanupManager?.registerObserver) { window.YouTubeUtils.cleanupManager.registerObserver(flexyObserver); window.YouTubeUtils.cleanupManager.registerObserver(chatObserver); } } catch {} }; if ("loading" === document.readyState) { document.addEventListener("DOMContentLoaded", () => { scheduleRun(); setupOverlayObservers(); }, { once: !0 }); } else { scheduleRun(); setupOverlayObservers(); } })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const t = window.YouTubeUtils?.t || (key => key || ""); const LANG_MAP = { cn: "zh-CN", tw: "zh-TW", kr: "ko", jp: "ja", ng: "en", du: "nl", be: "be", bg: "bg", kk: "kk", ky: "ky", uz: "uz", uk: "uk", "zh-hans": "zh-CN", "zh-hant": "zh-TW", "zh-cn": "zh-CN", "zh-tw": "zh-TW", "zh-hk": "zh-TW", iw: "he", jv: "jw", "sr-latn": "sr", "pt-br": "pt", "pt-pt": "pt", ar: "ar", az: "az", cs: "cs", da: "da", de: "de", el: "el", en: "en", es: "es", fi: "fi", fr: "fr", hi: "hi", hr: "hr", hu: "hu", id: "id", it: "it", lt: "lt", lv: "lv", ms: "ms", nl: "nl", no: "no", pl: "pl", ro: "ro", ru: "ru", sk: "sk", sl: "sl", sq: "sq", sv: "sv", th: "th", tr: "tr", vi: "vi" }; const toGoogleLang = code => { if (!code) { return "en"; } const lower = code.toLowerCase(); if (LANG_MAP[lower]) { return LANG_MAP[lower]; } const base = lower.split("-")[0]; return LANG_MAP[base] || base || "en"; }; const getTranslateLabel = () => t("translateComment") || "Translate"; const getShowOriginalLabel = () => t("showOriginal") || "Show original"; const injectStyles = (() => { let injected = !1; return () => { if (injected) { return; } injected = !0; const css = "\n .ytp-comment-translate-btn{\n display:inline-flex;align-items:center;gap:4px;\n background:none;border:none;cursor:pointer;\n color:var(--yt-spec-text-secondary,#aaa);\n font-size:1.2rem;line-height:1.8rem;font-weight:400;\n padding:4px 0;margin-top:4px;\n font-family:'Roboto','Arial',sans-serif;\n transition:color .2s;\n }\n .ytp-comment-translate-btn:hover{color:var(--yt-spec-text-primary,#fff);}\n .ytp-comment-translate-btn[disabled]{opacity:.5;cursor:wait;}\n .ytp-comment-translate-btn svg{flex-shrink:0;}\n "; try { if (window.YouTubeUtils?.StyleManager?.add) { window.YouTubeUtils.StyleManager.add("ytp-comment-translate-styles", css); return; } } catch {} const style = document.createElement("style"); style.id = "ytp-comment-translate-styles"; style.textContent = css; (document.head || document.documentElement).appendChild(style); }; })(); const translateIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12.87 15.07l-2.54-2.51.03-.03A17.52 17.52 0 0014.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>'; const addTranslateButton = commentEl => { if (commentEl.querySelector(".ytp-comment-translate-btn")) { return; } const contentEl = commentEl.querySelector("#content-text.ytd-comment-view-model, #content-text.ytd-comment-renderer, yt-attributed-string#content-text, yt-formatted-string#content-text, #content-text"); if (!contentEl) { return; } const text = (contentEl.textContent || "").trim(); if (!text || text.length < 2) { return; } const userLang = (() => { try { if (window.YouTubePlusI18n?.getLanguage) { return toGoogleLang(window.YouTubePlusI18n.getLanguage()); } const htmlLang = document.documentElement.lang; if (htmlLang) { return toGoogleLang(htmlLang); } } catch {} return toGoogleLang(navigator.language) || "en"; })(); const btn = document.createElement("button"); btn.className = "ytp-comment-translate-btn"; btn.type = "button"; btn.innerHTML = _createHTML(`${translateIcon} ${getTranslateLabel()}`); btn.setAttribute("aria-label", getTranslateLabel()); btn.addEventListener("click", async e => { e.preventDefault(); e.stopPropagation(); if (contentEl.hasAttribute("data-ytp-translated")) { const original = contentEl.getAttribute("data-ytp-original-text"); if (original) { contentEl.textContent = original; contentEl.removeAttribute("data-ytp-translated"); btn.innerHTML = _createHTML(`${translateIcon} ${getTranslateLabel()}`); btn.setAttribute("aria-label", getTranslateLabel()); } return; } btn.disabled = !0; btn.innerHTML = _createHTML(`${translateIcon} ...`); const originalText = contentEl.textContent || ""; const translated = await (async (text, targetLang) => { try { const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${encodeURIComponent(targetLang)}&dt=t&q=${encodeURIComponent(text)}`; const resp = await fetch(url); if (!resp.ok) { throw new Error(`HTTP ${resp.status}`); } const data = await resp.json(); if (Array.isArray(data) && Array.isArray(data[0])) { return data[0].map(s => s && s[0] || "").join(""); } } catch (e) { console.warn("[YouTube+] Translation failed:", e); } return null; })(originalText, userLang); if (translated && translated !== originalText) { contentEl.setAttribute("data-ytp-original-text", originalText); contentEl.setAttribute("data-ytp-translated", "true"); contentEl.textContent = translated; btn.innerHTML = _createHTML(`${translateIcon} ${getShowOriginalLabel()}`); btn.setAttribute("aria-label", getShowOriginalLabel()); } else { btn.innerHTML = _createHTML(`${translateIcon} ${getTranslateLabel()}`); btn.setAttribute("aria-label", getTranslateLabel()); } btn.disabled = !1; }); const actionBar = commentEl.querySelector("#action-buttons, ytd-comment-action-buttons-renderer, #toolbar"); actionBar ? actionBar.parentElement.insertBefore(btn, actionBar) : contentEl.after(btn); }; const processComments = () => { const commentSelectors = [ "ytd-comment-view-model", "ytd-comment-renderer", "ytd-comment-thread-renderer" ]; for (const sel of commentSelectors) { document.querySelectorAll(sel).forEach(addTranslateButton); } }; let processTimeout = null; const init = () => { injectStyles(); processComments(); const commentsContainer = document.querySelector("#comments, #tab-comments, #content"); const target = commentsContainer || document.body; const observer = new MutationObserver(mutations => { let hasNewComments = !1; for (const m of mutations) { for (const node of m.addedNodes) { if (node instanceof Element && (node.matches?.("ytd-comment-view-model, ytd-comment-renderer, ytd-comment-thread-renderer") || node.querySelector?.("ytd-comment-view-model, ytd-comment-renderer, #content-text"))) { hasNewComments = !0; break; } } if (hasNewComments) { break; } } hasNewComments && (() => { processTimeout && clearTimeout(processTimeout); processTimeout = setTimeout(processComments, 300); })(); }); observer.observe(target, { childList: !0, subtree: !0 }); try { window.YouTubeUtils?.cleanupManager && window.YouTubeUtils.cleanupManager.registerObserver(observer); } catch {} }; const scheduleInit = () => { const isVideoPage = "/watch" === location.pathname || location.pathname.startsWith("/shorts/"); isVideoPage && ("function" == typeof requestIdleCallback ? requestIdleCallback(() => init(), { timeout: 3e3 }) : setTimeout(init, 1500)); }; "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", scheduleInit, { once: !0 }) : scheduleInit(); window.addEventListener("yt-navigate-finish", scheduleInit, { passive: !0 }); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const U = window.YouTubeUtils || {}; const $ = (sel, ctx) => U.$(sel, ctx) || (ctx || document).querySelector(sel); const t = U.t || (key => key || ""); /** * Ad blocking functionality for YouTube * @namespace AdBlocker */ const AdBlocker = { config: { skipInterval: 1e3, removeInterval: 3e3, enableLogging: !1, maxRetries: 2, enabled: !0, storageKey: "youtube_adblocker_settings" }, state: { isYouTubeShorts: !1, isYouTubeMusic: "music.youtube.com" === location.hostname, lastSkipAttempt: 0, retryCount: 0, initialized: !1 }, cache: { moviePlayer: null, ytdPlayer: null, lastCacheTime: 0, cacheTimeout: 1e4 }, selectors: { ads: ".ytp-ad-timed-pie-countdown-container,.ytp-ad-survey-questions,.ytp-ad-overlay-container,.ytp-ad-progress,.ytp-ad-progress-list", elements: "#masthead-ad,ytd-merch-shelf-renderer,.yt-mealbar-promo-renderer,ytmusic-mealbar-promo-renderer,ytmusic-statement-banner-renderer,.ytp-featured-product,ytd-in-feed-ad-layout-renderer,ytd-banner-promo-renderer,ytd-statement-banner-renderer,ytd-brand-video-singleton-renderer,ytd-brand-video-shelf-renderer,ytd-promoted-sparkles-web-renderer,ytd-display-ad-renderer,ytd-promoted-video-renderer,.ytd-mealbar-promo-renderer", video: "video.html5-main-video", removal: "ytd-reel-video-renderer .ytd-ad-slot-renderer, ytd-ad-slot-renderer, #player-ads, ytd-in-feed-ad-layout-renderer, ytd-display-ad-renderer, ytd-promoted-sparkles-web-renderer, ytd-promoted-video-renderer, ad-slot-renderer, ytd-player-legacy-desktop-watch-ads-renderer" }, wrappers: [ "ytd-rich-item-renderer", "ytd-grid-video-renderer", "ytd-compact-video-renderer", "ytd-rich-grid-media", "ytd-rich-shelf-renderer", "ytd-rich-grid-row", "ytd-video-renderer", "ytd-playlist-renderer", "ytd-reel-video-renderer" ], settings: { load() { try { const saved = localStorage.getItem(AdBlocker.config.storageKey); if (!saved) { return; } const parsed = JSON.parse(saved); if ("object" != typeof parsed || null === parsed) { console.warn("[AdBlocker] Invalid settings format"); return; } AdBlocker.config.enabled = "boolean" != typeof parsed.enabled || parsed.enabled; AdBlocker.config.enableLogging = "boolean" == typeof parsed.enableLogging && parsed.enableLogging; } catch (error) { console.error("[AdBlocker] Error loading settings:", error); AdBlocker.config.enabled = !0; AdBlocker.config.enableLogging = !1; } }, save() { try { const settingsToSave = { enabled: AdBlocker.config.enabled, enableLogging: AdBlocker.config.enableLogging }; localStorage.setItem(AdBlocker.config.storageKey, JSON.stringify(settingsToSave)); } catch (error) { console.error("[AdBlocker] Error saving settings:", error); } } }, getPlayer() { const now = Date.now(); if (now - AdBlocker.cache.lastCacheTime > AdBlocker.cache.cacheTimeout) { AdBlocker.cache.moviePlayer = $("#movie_player"); AdBlocker.cache.ytdPlayer = $("#ytd-player"); AdBlocker.cache.lastCacheTime = now; } const playerEl = AdBlocker.cache.ytdPlayer; return { element: AdBlocker.cache.moviePlayer, player: playerEl?.getPlayer?.() || playerEl }; }, skipAd() { if (!AdBlocker.config.enabled) { return; } const now = Date.now(); if (now - AdBlocker.state.lastSkipAttempt < 300) { return; } AdBlocker.state.lastSkipAttempt = now; if (location.pathname.startsWith("/shorts/")) { return; } const moviePlayer = $("#movie_player"); const isAdShowing = moviePlayer && (moviePlayer.classList.contains("ad-showing") || moviePlayer.classList.contains("ad-interrupting")); if (isAdShowing) { try { const SKIP_SELECTOR = [ ".ytp-ad-skip-button", ".ytp-ad-skip-button-modern", ".ytp-skip-ad-button", ".videoAdUiSkipButton", "button.ytp-ad-skip-button-modern", ".ytp-ad-skip-button-slot button", ".ytp-ad-skip-button-container button", ".ytp-ad-skip-button-modern .ytp-ad-skip-button-container", ".ytp-skip-ad-button__text", 'button[class*="skip"]', ".ytp-ad-skip-button-modern button", "ytd-button-renderer.ytp-ad-skip-button-renderer button" ].join(","); const skipButtons = document.querySelectorAll(SKIP_SELECTOR); for (const skipButton of skipButtons) { const rect = skipButton.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { skipButton.click(); AdBlocker.state.retryCount = 0; return; } } const video = $(AdBlocker.selectors.video); if (video) { video.muted = !0; if (video.duration && isFinite(video.duration) && video.duration > 0) { try { video.currentTime = Math.max(video.duration - .1, 0); } catch (e) { console.warn("[YouTube+] Ad seek error:", e); } } } const overlaySelectors = [ ".ytp-ad-overlay-close-button", ".ytp-ad-overlay-close-container button", ".ytp-ad-overlay-close-button button", ".ytp-ad-overlay-ad-info-button-container", 'button[id="dismiss-button"]' ]; for (const sel of overlaySelectors) { const overlayClose = $(sel); if (overlayClose) { overlayClose.click(); break; } } AdBlocker.state.retryCount = 0; } catch { if (AdBlocker.state.retryCount < AdBlocker.config.maxRetries) { AdBlocker.state.retryCount++; setTimeout(AdBlocker.skipAd, 800); } } } else { AdBlocker.state.retryCount = 0; } }, dismissAdBlockerWarning() { if (AdBlocker.config.enabled) { try { const enforcement = document.querySelector("ytd-enforcement-message-view-model"); if (enforcement) { const btns = enforcement.querySelectorAll("button, tp-yt-paper-button, a.yt-spec-button-shape-next--outline"); for (const btn of btns) { const btnText = (btn.textContent || "").toLowerCase().trim(); if (btnText.includes("allow") || btnText.includes("dismiss") || btnText.includes("разрешить") || btn.getAttribute("aria-label")?.includes("close")) { btn.click(); return; } } enforcement.remove(); return; } const dialogs = document.querySelectorAll("tp-yt-paper-dialog, ytd-popup-container tp-yt-paper-dialog, yt-dialog-container"); for (const dialog of dialogs) { const text = (dialog.textContent || "").toLowerCase(); const isAdBlockWarning = text.includes("ad blocker") || text.includes("ad blockers") || text.includes("блокировщик") || text.includes("will be blocked") || text.includes("будет заблокирован") || text.includes("allow") && text.includes("ads") || text.includes("blocker") && text.includes("video"); if (!isAdBlockWarning) { continue; } const dismissBtns = dialog.querySelectorAll('#dismiss-button button, .dismiss-button, button[id*="dismiss"], tp-yt-paper-button, yt-button-renderer button, a[href]'); for (const btn of dismissBtns) { const btnText = (btn.textContent || "").toLowerCase(); if (btnText.includes("dismiss") || btnText.includes("allow") || btnText.includes("not using") || btnText.includes("report")) { btn.click(); return; } } dialog.style.display = "none"; dialog.remove(); return; } const overlays = document.querySelectorAll("tp-yt-iron-overlay-backdrop, .yt-dialog-overlay"); for (const overlay of overlays) { "none" !== overlay.style.display && null !== overlay.offsetParent && (overlay.style.display = "none"); } } catch {} } }, addCss() { if ($("#yt-ab-styles") || !AdBlocker.config.enabled) { return; } const styles = `${AdBlocker.selectors.ads}{display:none!important;}`; YouTubeUtils.StyleManager.add("yt-ab-styles", styles); }, removeCss() { YouTubeUtils.StyleManager.remove("yt-ab-styles"); }, removeElements() { if (!AdBlocker.config.enabled || AdBlocker.state.isYouTubeMusic) { return; } const remove = () => { try { const adElements = document.querySelectorAll(AdBlocker.selectors.elements); adElements.forEach(el => { try { el.remove(); } catch {} }); } catch {} const elements = document.querySelectorAll(AdBlocker.selectors.removal); elements.forEach(el => { try { for (const w of AdBlocker.wrappers) { const wrap = el.closest(w); if (wrap) { wrap.remove(); return; } } const reel = el.closest("ytd-reel-video-renderer"); if (reel) { reel.remove(); return; } const container = el.closest("ytd-ad-slot-renderer") || el.closest(".ad-container") || el; container && container.remove && container.remove(); } catch (e) { AdBlocker.config.enableLogging && console.warn("[AdBlocker] removeElements error", e); } }); }; window.requestIdleCallback ? requestIdleCallback(remove, { timeout: 100 }) : setTimeout(remove, 0); }, addSettingsUI() { const section = $('.ytp-plus-settings-section[data-section="basic"]'); if (section && !section.querySelector(".ab-settings")) { try { const item = document.createElement("div"); item.className = "ytp-plus-settings-item ab-settings"; item.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label">${t("adBlocker")}</label>\n <div class="ytp-plus-settings-item-description">${t("adBlockerDescription")}</div>\n </div>\n <input type="checkbox" class="ytp-plus-settings-checkbox" ${AdBlocker.config.enabled ? "checked" : ""}>\n `); section.appendChild(item); item.querySelector("input").addEventListener("change", e => { const target = 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); } } }, init() { if (AdBlocker.state.initialized) { return; } AdBlocker.state.initialized = !0; AdBlocker.settings.load(); if (AdBlocker.config.enabled) { AdBlocker.addCss(); AdBlocker.removeElements(); } const isAdActive = () => { const mp = $("#movie_player"); return mp && (mp.classList.contains("ad-showing") || mp.classList.contains("ad-interrupting")); }; const adInterval = setInterval(() => { if (AdBlocker.config.enabled && isAdActive()) { AdBlocker.skipAd(); AdBlocker.dismissAdBlockerWarning(); } }, 3e3); YouTubeUtils.cleanupManager.registerInterval(adInterval); try { const attachAdObserver = () => { const moviePlayer = $("#movie_player"); if (!moviePlayer) { setTimeout(attachAdObserver, 1500); return; } const adObserver = new MutationObserver(() => { if (AdBlocker.config.enabled && isAdActive()) { AdBlocker.skipAd(); AdBlocker.dismissAdBlockerWarning(); } }); adObserver.observe(moviePlayer, { attributes: !0, attributeFilter: [ "class" ] }); YouTubeUtils.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(adObserver); }; attachAdObserver(); } catch (e) { console.warn("[YouTube+] Ad observer setup error:", e); } try { const handleVideoPlay = () => { if (AdBlocker.config.enabled) { setTimeout(AdBlocker.skipAd, 50); setTimeout(AdBlocker.skipAd, 200); setTimeout(AdBlocker.skipAd, 500); } }; YouTubeUtils.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "playing", handleVideoPlay, { capture: !0, passive: !0 }) : document.addEventListener("playing", handleVideoPlay, { capture: !0, passive: !0 }); } catch (e) { console.warn("[YouTube+] Ad play listener error:", e); } const handleNavigation = () => { AdBlocker.state.isYouTubeShorts = location.pathname.startsWith("/shorts/"); AdBlocker.cache.lastCacheTime = 0; AdBlocker.config.enabled && AdBlocker.removeElements(); }; const navHandler = () => setTimeout(handleNavigation, 50); YouTubeUtils.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(window, "ytp-history-navigate", navHandler) : window.addEventListener("ytp-history-navigate", navHandler); const settingsHandler = () => { setTimeout(AdBlocker.addSettingsUI, 50); }; YouTubeUtils.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "youtube-plus-settings-modal-opened", settingsHandler) : document.addEventListener("youtube-plus-settings-modal-opened", settingsHandler); try { const adSlotObserver = new MutationObserver(mutations => { let shouldRemove = !1; for (const m of mutations) { for (const node of m.addedNodes) { if (node instanceof Element) { try { if (node.matches && node.matches("ytd-ad-slot-renderer, ytd-merch-shelf-renderer")) { shouldRemove = !0; break; } if (node.querySelector && node.querySelector("ytd-ad-slot-renderer")) { shouldRemove = !0; break; } } catch (e) { AdBlocker.config.enableLogging && console.warn("[AdBlocker] adSlotObserver node check", e); } } } if (shouldRemove) { break; } } shouldRemove && AdBlocker.removeElements(); }); const observeContentContainers = () => { const containers = [ document.querySelector("#content"), document.querySelector("#page-manager"), document.querySelector("ytd-browse"), document.querySelector("ytd-search") ].filter(Boolean); 0 === containers.length ? setTimeout(() => { const retryContainers = [ document.querySelector("#content"), document.querySelector("#page-manager") ].filter(Boolean); retryContainers.length > 0 ? retryContainers.forEach(c => adSlotObserver.observe(c, { childList: !0, subtree: !0 })) : adSlotObserver.observe(document.body, { childList: !0, subtree: !0 }); }, 1e3) : containers.forEach(container => { adSlotObserver.observe(container, { childList: !0, subtree: !0 }); }); }; document.body ? observeContentContainers() : document.addEventListener("DOMContentLoaded", observeContentContainers); YouTubeUtils.cleanupManager.registerObserver(adSlotObserver); } catch (e) { AdBlocker.config.enableLogging && console.warn("[AdBlocker] Failed to create adSlotObserver", e); } YouTubeUtils.cleanupManager.registerListener(document, "click", e => { const target = e.target; "basic" === target.dataset?.section && setTimeout(AdBlocker.addSettingsUI, 25); }, { passive: !0, capture: !0 }); if (AdBlocker.config.enabled) { setTimeout(AdBlocker.skipAd, 200); setTimeout(AdBlocker.dismissAdBlockerWarning, 500); } } }; "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", AdBlocker.init, { once: !0 }) : AdBlocker.init(); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const t = window.YouTubeUtils?.t || (key => key || ""); const pipSettings = { enabled: !0, shortcut: { key: "P", shiftKey: !0, altKey: !1, ctrlKey: !1 }, storageKey: "youtube_pip_settings" }; const getVideoElement = () => { try { const candidate = "function" == typeof YouTubeUtils?.querySelector && YouTubeUtils.querySelector("video") || document.querySelector("video"); return candidate && candidate.tagName && "video" === candidate.tagName.toLowerCase() ? candidate : null; } catch (error) { console.error("[PiP] Error getting video element:", error); return null; } }; const setSessionActive = isActive => { try { isActive ? sessionStorage.setItem("youtube_plus_pip_session", "true") : sessionStorage.removeItem("youtube_plus_pip_session"); } catch {} }; const loadSettings = () => { try { const saved = localStorage.getItem(pipSettings.storageKey); if (!saved) { return; } const parsed = JSON.parse(saved); if ("object" != typeof parsed || null === parsed) { console.warn("[PiP] Invalid settings format"); return; } "boolean" == typeof parsed.enabled && (pipSettings.enabled = parsed.enabled); if (parsed.shortcut && "object" == typeof parsed.shortcut) { "string" == typeof parsed.shortcut.key && parsed.shortcut.key.length > 0 && (pipSettings.shortcut.key = parsed.shortcut.key); "boolean" == typeof parsed.shortcut.shiftKey && (pipSettings.shortcut.shiftKey = parsed.shortcut.shiftKey); "boolean" == typeof parsed.shortcut.altKey && (pipSettings.shortcut.altKey = parsed.shortcut.altKey); "boolean" == typeof parsed.shortcut.ctrlKey && (pipSettings.shortcut.ctrlKey = parsed.shortcut.ctrlKey); } } catch (e) { console.error("[PiP] Error loading settings:", e); } }; const saveSettings = () => { try { const settingsToSave = { enabled: pipSettings.enabled, shortcut: pipSettings.shortcut }; localStorage.setItem(pipSettings.storageKey, JSON.stringify(settingsToSave)); } catch (e) { console.error("[PiP] Error saving settings:", e); } }; const getCurrentPiPElement = () => { const current = document.pictureInPictureElement; if (current && "object" == typeof current && "tagName" in current) { const tag = current.tagName; if ("string" == typeof tag && "video" === tag.toLowerCase()) { return current; } } return null; }; const togglePictureInPicture = async video => { if (pipSettings.enabled && video) { try { const currentPiP = getCurrentPiPElement(); if (currentPiP && currentPiP !== video) { await document.exitPictureInPicture(); setSessionActive(!1); } if (getCurrentPiPElement() === video) { await document.exitPictureInPicture(); setSessionActive(!1); return; } if (video.disablePictureInPicture) { throw new Error("Picture-in-Picture is disabled by the video element"); } await (video => video ? video.readyState >= 1 && !video.seeking ? Promise.resolve() : new Promise((resolve, reject) => { let settled = !1; const cleanup = () => { video.removeEventListener("loadedmetadata", onLoaded); video.removeEventListener("error", onError); timeoutId && clearTimeout(timeoutId); }; const onLoaded = () => { if (!settled) { settled = !0; cleanup(); resolve(); } }; const onError = () => { if (!settled) { settled = !0; cleanup(); reject(new Error("[PiP] Video metadata failed to load")); } }; let timeoutId = setTimeout(() => { if (!settled) { settled = !0; cleanup(); reject(new Error("[PiP] Timed out waiting for video metadata")); } }, 3e3); const registeredTimeout = YouTubeUtils?.cleanupManager?.registerTimeout?.(timeoutId); registeredTimeout && (timeoutId = registeredTimeout); video.addEventListener("loadedmetadata", onLoaded, { once: !0 }); video.addEventListener("error", onError, { once: !0 }); }) : Promise.reject(new Error("[PiP] Invalid video element")))(video); await video.requestPictureInPicture(); setSessionActive(!0); } catch (error) { console.error("[YouTube+][PiP] Failed to toggle Picture-in-Picture:", error); } } }; const addPipSettingsToModal = () => { const advancedSection = document.querySelector('.ytp-plus-settings-section[data-section="advanced"]'); if (!advancedSection || advancedSection.querySelector(".pip-settings-item")) { return !1; } const getSubmenuExpanded = () => { try { const raw = localStorage.getItem("ytp-plus-submenu-states"); if (!raw) { return null; } const parsed = JSON.parse(raw); if (parsed && "boolean" == typeof parsed.pip) { return parsed.pip; } } catch {} return null; }; const storedExpanded = getSubmenuExpanded(); const initialExpanded = "boolean" != typeof storedExpanded || storedExpanded; if (!document.getElementById("pip-styles")) { const styles = "\n .pip-shortcut-editor { display: flex; align-items: center; gap: 8px; }\n .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;}\n "; YouTubeUtils.StyleManager.add("pip-styles", styles); } const enableItem = document.createElement("div"); enableItem.className = "ytp-plus-settings-item pip-settings-item ytp-plus-settings-item--with-submenu"; enableItem.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label" for="pip-enable-checkbox">${t("pipTitle")}</label>\n <div class="ytp-plus-settings-item-description">${t("pipDescription")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <button\n type="button"\n class="ytp-plus-submenu-toggle"\n data-submenu="pip"\n aria-label="Toggle PiP submenu"\n aria-expanded="${initialExpanded ? "true" : "false"}"\n ${pipSettings.enabled ? "" : "disabled"}\n style="display:${pipSettings.enabled ? "inline-flex" : "none"};"\n >\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="6 9 12 15 18 9"></polyline>\n </svg>\n </button>\n <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enablePiP" id="pip-enable-checkbox" ${pipSettings.enabled ? "checked" : ""}>\n </div>\n `); advancedSection.appendChild(enableItem); const submenuWrap = document.createElement("div"); submenuWrap.className = "pip-submenu"; submenuWrap.dataset.submenu = "pip"; submenuWrap.style.display = pipSettings.enabled && initialExpanded ? "block" : "none"; submenuWrap.style.marginLeft = "12px"; submenuWrap.style.marginBottom = "12px"; const submenuCard = document.createElement("div"); submenuCard.className = "glass-card"; submenuCard.style.display = "flex"; submenuCard.style.flexDirection = "column"; submenuCard.style.gap = "8px"; const shortcutItem = document.createElement("div"); shortcutItem.className = "ytp-plus-settings-item pip-shortcut-item"; shortcutItem.style.display = "flex"; 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 = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label">${t("pipShortcutTitle")}</label>\n <div class="ytp-plus-settings-item-description">${t("pipShortcutDescription")}</div>\n </div>\n <div class="pip-shortcut-editor">\n \x3c!-- hidden native select kept for compatibility --\x3e\n <select id="pip-modifier-combo" style="display:none;">\n ${[ "none", "ctrl", "alt", "shift", "ctrl+alt", "ctrl+shift", "alt+shift", "ctrl+alt+shift" ].map(v => `<option value="${v}" ${v === modifierValue ? "selected" : ""}>${"none" === v ? t("none") : v.replace(/\+/g, "+").split("+").map(k => t(k.toLowerCase())).join("+").split("+").map(k => k.charAt(0).toUpperCase() + k.slice(1)).join("+")}</option>`).join("")}\n </select>\n\n <div class="glass-dropdown" id="pip-modifier-dropdown" tabindex="0" role="listbox" aria-expanded="false">\n <button class="glass-dropdown__toggle" type="button" aria-haspopup="listbox">\n <span class="glass-dropdown__label">${"none" === modifierValue ? t("none") : modifierValue.replace(/\+/g, "+").split("+").map(k => t(k.toLowerCase())).map(k => k.charAt(0).toUpperCase() + k.slice(1)).join("+")}</span>\n <svg class="glass-dropdown__chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>\n </button>\n <ul class="glass-dropdown__list" role="presentation">\n ${[ "none", "ctrl", "alt", "shift", "ctrl+alt", "ctrl+shift", "alt+shift", "ctrl+alt+shift" ].map(v => { const label = "none" === v ? t("none") : v.replace(/\+/g, "+").split("+").map(k => t(k.toLowerCase())).map(k => k.charAt(0).toUpperCase() + k.slice(1)).join("+"); const sel = v === modifierValue ? ' aria-selected="true"' : ""; return `<li class="glass-dropdown__item" data-value="${v}" role="option"${sel}>${label}</li>`; }).join("")}\n </ul>\n </div>\n\n <span>+</span>\n <input type="text" id="pip-key" value="${pipSettings.shortcut.key}" maxlength="1" style="width: 30px; text-align: center;">\n </div>\n `); submenuCard.appendChild(shortcutItem); submenuWrap.appendChild(submenuCard); advancedSection.appendChild(submenuWrap); setTimeout(() => { const hidden = document.getElementById("pip-modifier-combo"); const dropdown = document.getElementById("pip-modifier-dropdown"); if (!hidden || !dropdown) { return; } const toggle = dropdown.querySelector(".glass-dropdown__toggle"); const list = dropdown.querySelector(".glass-dropdown__list"); const label = dropdown.querySelector(".glass-dropdown__label"); let items = Array.from(list.querySelectorAll(".glass-dropdown__item")); let idx = items.findIndex(it => "true" === it.getAttribute("aria-selected")); idx < 0 && (idx = 0); const openList = () => { dropdown.setAttribute("aria-expanded", "true"); list.style.display = "block"; items = Array.from(list.querySelectorAll(".glass-dropdown__item")); }; const closeList = () => { dropdown.setAttribute("aria-expanded", "false"); list.style.display = "none"; }; toggle.addEventListener("click", () => { const expanded = "true" === dropdown.getAttribute("aria-expanded"); expanded ? closeList() : openList(); }); const outsideClickHandler = e => { dropdown.contains(e.target) || closeList(); }; window.YouTubeUtils && YouTubeUtils.cleanupManager ? YouTubeUtils.cleanupManager.registerListener(document, "click", outsideClickHandler) : document.addEventListener("click", outsideClickHandler); dropdown.addEventListener("keydown", e => { const expanded = "true" === dropdown.getAttribute("aria-expanded"); if ("ArrowDown" === e.key) { e.preventDefault(); expanded || openList(); idx = Math.min(idx + 1, items.length - 1); items.forEach(it => it.removeAttribute("aria-selected")); items[idx].setAttribute("aria-selected", "true"); items[idx].scrollIntoView({ block: "nearest" }); } else if ("ArrowUp" === e.key) { e.preventDefault(); expanded || openList(); idx = Math.max(idx - 1, 0); items.forEach(it => it.removeAttribute("aria-selected")); items[idx].setAttribute("aria-selected", "true"); items[idx].scrollIntoView({ block: "nearest" }); } else if ("Enter" === e.key || " " === e.key) { e.preventDefault(); if (!expanded) { openList(); return; } const it = items[idx]; if (it) { hidden.value = it.dataset.value; hidden.dispatchEvent(new Event("change", { bubbles: !0 })); label.textContent = it.textContent; closeList(); } } else { "Escape" === e.key && closeList(); } }); list.addEventListener("click", e => { const it = e.target.closest(".glass-dropdown__item"); if (!it) { return; } const val = it.dataset.value; hidden.value = val; list.querySelectorAll(".glass-dropdown__item").forEach(li => li.removeAttribute("aria-selected")); it.setAttribute("aria-selected", "true"); label.textContent = it.textContent; hidden.dispatchEvent(new Event("change", { bubbles: !0 })); closeList(); }); }, 0); document.getElementById("pip-enable-checkbox").addEventListener("change", e => { const target = e.target; pipSettings.enabled = target.checked; const submenuToggle = enableItem.querySelector('.ytp-plus-submenu-toggle[data-submenu="pip"]'); if (submenuToggle instanceof HTMLElement) { if (pipSettings.enabled) { const stored = getSubmenuExpanded(); const nextExpanded = "boolean" != typeof stored || stored; submenuToggle.removeAttribute("disabled"); submenuToggle.style.display = "inline-flex"; submenuToggle.setAttribute("aria-expanded", nextExpanded ? "true" : "false"); submenuWrap.style.display = nextExpanded ? "block" : "none"; } else { submenuToggle.setAttribute("disabled", ""); submenuToggle.style.display = "none"; submenuWrap.style.display = "none"; } } saveSettings(); }); document.getElementById("pip-modifier-combo").addEventListener("change", e => { const target = 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 = e.target; if (target.value) { pipSettings.shortcut.key = target.value.toUpperCase(); saveSettings(); } }); document.getElementById("pip-key").addEventListener("keydown", e => e.stopPropagation()); return !0; }; loadSettings(); YouTubeUtils.cleanupManager.registerListener(document, "keydown", e => { if (!pipSettings.enabled) { return; } const {shiftKey, altKey, ctrlKey, key} = pipSettings.shortcut; if (e.shiftKey === shiftKey && e.altKey === altKey && e.ctrlKey === ctrlKey && e.key.toUpperCase() === key) { const video = getVideoElement(); video && togglePictureInPicture(video); e.preventDefault(); } }); YouTubeUtils.cleanupManager.registerListener(window, "storage", e => { e.key === pipSettings.storageKey && loadSettings(); }); window.addEventListener("load", () => { if (!pipSettings.enabled || !(() => { try { return "true" === sessionStorage.getItem("youtube_plus_pip_session"); } catch { return !1; } })() || document.pictureInPictureElement) { return; } const resumePiP = () => { const video = getVideoElement(); video && togglePictureInPicture(video).catch(() => { setSessionActive(!1); }); }; const ensureCleanup = handler => { if (handler) { try { document.removeEventListener("pointerdown", handler, !0); } catch {} } }; const cleanupListeners = () => { ensureCleanup(pointerListener); ensureCleanup(keyListener); }; const pointerListener = () => { cleanupListeners(); resumePiP(); }; const keyListener = () => { cleanupListeners(); resumePiP(); }; document.addEventListener("pointerdown", pointerListener, { once: !0, capture: !0 }); document.addEventListener("keydown", keyListener, { once: !0, capture: !0 }); }); const ensurePipSettings = () => { if (window.YouTubeUtils?.createRetryScheduler) { window.YouTubeUtils.createRetryScheduler({ check: () => !0 === addPipSettingsToModal(), maxAttempts: 20, interval: 120 }); return; } let attempts = 0; const retry = () => { attempts += 1; addPipSettingsToModal() || attempts >= 20 || setTimeout(retry, 120); }; retry(); }; YouTubeUtils.cleanupManager.registerListener(document, "youtube-plus-settings-modal-opened", () => { setTimeout(ensurePipSettings, 50); }); YouTubeUtils.cleanupManager.registerListener(document, "leavepictureinpicture", () => { setSessionActive(!1); }); YouTubeUtils.cleanupManager.registerListener(document, "click", e => { const target = e.target; target.classList && target.classList.contains("ytp-plus-settings-nav-item") && "advanced" === target.dataset?.section && setTimeout(ensurePipSettings, 25); }, !0); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const {$, $$, byId} = window.YouTubeUtils || {}; if ("www.youtube.com" !== window.location.hostname || window.frameElement) { return; } if (window._timecodeModuleInitialized) { return; } window._timecodeModuleInitialized = !0; const t = window.YouTubeUtils?.t || (key => key || ""); const config = { enabled: !0, autoDetect: !0, shortcut: { key: "T", shiftKey: !0, altKey: !1, ctrlKey: !1 }, storageKey: "youtube_timecode_settings", autoSave: !0, autoTrackPlayback: !0, panelPosition: null, export: !0 }; const state = { timecodes: new Map, dom: {}, isReloading: !1, activeIndex: null, trackingId: 0, dragging: !1, editingIndex: null, resizeListenerKey: null }; let initStarted = !1; const isRelevantRoute = () => { try { return "/watch" === location.pathname; } catch { return !1; } }; const saveSettings = () => { try { const settingsToSave = { enabled: config.enabled, autoDetect: config.autoDetect, shortcut: config.shortcut, autoSave: config.autoSave, autoTrackPlayback: config.autoTrackPlayback, panelPosition: config.panelPosition, export: config.export }; localStorage.setItem(config.storageKey, JSON.stringify(settingsToSave)); } catch (error) { console.error("[Timecode] Error saving settings:", error); } }; const clampPanelPosition = (panel, left, top) => { try { if (!(panel && panel instanceof HTMLElement)) { console.warn("[Timecode] Invalid panel element"); return { left: 0, top: 0 }; } if ("number" != typeof left || "number" != typeof top || isNaN(left) || isNaN(top)) { console.warn("[Timecode] Invalid position coordinates"); 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) }; } catch (error) { console.error("[Timecode] Error clamping panel position:", error); return { left: 0, top: 0 }; } }; const savePanelPosition = (left, top) => { try { if ("number" != typeof left || "number" != typeof top || isNaN(left) || isNaN(top)) { console.warn("[Timecode] Invalid position coordinates for saving"); return; } config.panelPosition = { left, top }; saveSettings(); } catch (error) { console.error("[Timecode] Error saving panel position:", error); } }; const applySavedPanelPosition = panel => { panel && config.panelPosition && 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 = 2e3, type = "info") => { YouTubeUtils.NotificationManager.show(message, { duration, type }); }; 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 removeDuplicateText = text => { if (!text || text.length < 10) { return text; } let cleaned = text.trim(); cleaned = cleaned.replace(/\s*\.{2,}$/, "").replace(/\s*…$/, ""); const words = cleaned.split(/\s+/); if (words.length < 4) { return cleaned; } const half = Math.floor(words.length / 2); if (half >= 2) { const firstHalf = words.slice(0, half).join(" "); const secondHalf = words.slice(half, 2 * half).join(" "); if (firstHalf === secondHalf) { return firstHalf; } } const minPatternLength = Math.max(2, Math.floor(words.length / 4)); const maxPatternLength = Math.floor(words.length / 2); for (let len = maxPatternLength; len >= minPatternLength; len--) { const pattern = words.slice(0, len).join(" "); const patternWords = words.slice(0, len); for (let offset = 1; offset <= words.length - len; offset++) { let matchCount = 0; let partialWordMatch = !1; const testWords = words.slice(offset, Math.min(offset + len, words.length)); for (let i = 0; i < patternWords.length; i++) { const patternWord = patternWords[i]; const testWord = testWords[i]; if (!testWord) { break; } if (patternWord === testWord) { matchCount++; } else if (testWord.length >= 3 && patternWord.startsWith(testWord)) { matchCount += .8; partialWordMatch = !0; } else if (patternWord.length >= 3 && testWord.startsWith(patternWord)) { matchCount += .8; partialWordMatch = !0; } } const similarity = matchCount / patternWords.length; const effectiveMatches = Math.floor(matchCount); if (similarity >= .7 && (effectiveMatches >= 2 || matchCount >= 1.5 && partialWordMatch)) { return pattern; } } } return cleaned; }; const parseTime = timeStr => { try { if (!timeStr || "string" != typeof timeStr) { return null; } const str = timeStr.trim(); if (0 === str.length || str.length > 12) { return null; } let match = str.match(/^(\d+):(\d{1,2}):(\d{2})$/); if (match) { const [, h, m, s] = match.map(Number); if (isNaN(h) || isNaN(m) || isNaN(s)) { return null; } if (m >= 60 || s >= 60 || h < 0 || m < 0 || s < 0) { return null; } const total = 3600 * h + 60 * m + s; return total <= 86400 ? total : null; } match = str.match(/^(\d{1,2}):(\d{2})$/); if (match) { const [, m, s] = match.map(Number); return isNaN(m) || isNaN(s) || (m >= 60 || s >= 60 || m < 0 || s < 0) ? null : 60 * m + s; } return null; } catch (error) { console.error("[Timecode] Error parsing time:", error); return null; } }; const extractTimecodes = text => { try { if (!text || "string" != typeof text) { return []; } if (text.length > 5e4) { console.warn("[Timecode] Text too long, truncating"); text = text.substring(0, 5e4); } const timecodes = []; const seen = new Set; const patterns = [ /(\d{1,2}:\d{2}(?::\d{2})?)\s*[-–—]\s*(.+?)$/gm, /^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+?)$/gm, /(\d{1,2}:\d{2}(?::\d{2})?)\s*[-–—:]\s*([^\n\r]{1,100}?)(?=\s*\d{1,2}:\d{2}|\s*$)/g, /(\d{1,2}:\d{2}(?::\d{2})?)\s*[–—-]\s*([^\n]+)/gm, /^(\d{1,2}:\d{2}(?::\d{2})?)\s*(.+)$/gm ]; for (const pattern of patterns) { let match; let iterations = 0; const maxIterations = 1e3; for (;null !== (match = pattern.exec(text)) && iterations++ < maxIterations; ) { const time = parseTime(match[1]); if (null !== time && !seen.has(time)) { seen.add(time); let label = (match[2] || "").trim().replace(/^\d+[\.\)]\s*/, "").replace(/\s+/g, " ").substring(0, 100); const originalLabel = label; label = label.replace(/[<>\"']/g, ""); label = removeDuplicateText(label); originalLabel !== label && label.length > 0 && console.warn("[Timecode] Description deduplicated:", originalLabel, "->", label); timecodes.push({ time, label: label || "", originalText: match[1] }); } } iterations >= maxIterations && console.warn("[Timecode] Maximum iterations reached during extraction"); } return timecodes.sort((a, b) => a.time - b.time); } catch (error) { console.error("[Timecode] Error extracting timecodes:", error); return []; } }; const DESCRIPTION_SELECTORS = [ "#description-inline-expander yt-attributed-string", "#description-inline-expander yt-formatted-string", "#description-inline-expander ytd-text-inline-expander", "#description-inline-expander .yt-core-attributed-string", "#description ytd-text-inline-expander", "#description ytd-expandable-video-description-body-renderer", "#description.ytd-watch-metadata yt-formatted-string", "#description.ytd-watch-metadata #description-inline-expander", "#tab-info ytd-expandable-video-description-body-renderer yt-formatted-string", "#tab-info ytd-expandable-video-description-body-renderer yt-attributed-string", "#structured-description ytd-text-inline-expander", "#structured-description yt-formatted-string", 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] yt-formatted-string', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] yt-attributed-string', "ytd-watch-metadata #description", "ytd-watch-metadata #description-inline-expander", "#description" ]; const DESCRIPTION_SELECTOR_COMBINED = DESCRIPTION_SELECTORS.join(","); const DESCRIPTION_EXPANDERS = [ "#description-inline-expander yt-button-shape button", "#description-inline-expander tp-yt-paper-button#expand", "#description-inline-expander tp-yt-paper-button[aria-label]", "ytd-watch-metadata #description-inline-expander yt-button-shape button", "ytd-text-inline-expander[collapsed] yt-button-shape button", "ytd-text-inline-expander[collapsed] tp-yt-paper-button#expand", "ytd-expandable-video-description-body-renderer #expand", 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] #expand' ]; const sleep = (ms = 250) => new Promise(resolve => setTimeout(resolve, ms)); const collectDescriptionText = () => { const snippets = []; DESCRIPTION_SELECTORS.forEach(selector => { $$(selector).forEach(node => { const text = node?.textContent?.trim(); text && snippets.push(text); }); }); return snippets.join("\n"); }; const COMMENT_SELECTORS = [ "ytd-comment-thread-renderer #content-text", "ytd-comment-renderer #content-text", "ytd-comment-thread-renderer yt-formatted-string#content-text", "ytd-comment-renderer yt-formatted-string#content-text", "#comments ytd-comment-thread-renderer #content-text" ]; const expandDescriptionIfNeeded = async () => { for (const selector of DESCRIPTION_EXPANDERS) { const button = $(selector); if (!button) { continue; } const ariaExpanded = button.getAttribute("aria-expanded"); if ("true" === ariaExpanded) { return !1; } const ariaLabel = button.getAttribute("aria-label")?.toLowerCase(); if (ariaLabel && ariaLabel.includes("less")) { return !1; } if (null !== button.offsetParent) { try { button.click(); await sleep(400); return !0; } catch (error) { console.warn("[Timecode] Failed to click expand button:", error); } } } const inlineExpander = $("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 !0; } return !1; }; const detectTimecodes = async (options = {}) => { const {force = !1} = options; if (!config.enabled) { return []; } if (!force && !config.autoDetect) { return []; } const videoId = new URLSearchParams(window.location.search).get("v"); 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 (async () => { const initialText = collectDescriptionText(); if (initialText) { return; } for (let attempt = 0; attempt < 3; attempt++) { try { await YouTubeUtils.waitForElement(DESCRIPTION_SELECTOR_COMBINED, 1500); } catch {} await sleep(200); const expanded = await expandDescriptionIfNeeded(); await sleep(expanded ? 500 : 200); const text = collectDescriptionText(); if (text && text.length > initialText.length) { return; } } })(); const uniqueMap = new Map; const descriptionText = collectDescriptionText(); if (descriptionText) { const extracted = extractTimecodes(descriptionText); extracted.forEach(tc => { tc.time >= 0 && uniqueMap.set(tc.time.toString(), tc); }); } const chapters = getYouTubeChapters(); chapters.forEach(chapter => { if (chapter.time >= 0) { const key = chapter.time.toString(); const existing = uniqueMap.get(key); uniqueMap.set(key, existing && chapter.label && chapter.label.length > existing.label.length ? { ...existing, label: chapter.label, isChapter: !0 } : existing ? { ...existing, isChapter: !0 } : chapter); } }); if (0 === uniqueMap.size) { try { const commentsText = ((maxComments = 30) => { try { const snippets = []; for (const sel of COMMENT_SELECTORS) { $$(sel).forEach(node => { if (snippets.length >= maxComments) { return; } const text = node?.textContent?.trim(); text && snippets.push(text); }); if (snippets.length >= maxComments) { break; } } return snippets.join("\n"); } catch (error) { YouTubeUtils.logError("TimecodePanel", "collectCommentsText failed", error); return ""; } })(); if (commentsText) { const extractedComments = extractTimecodes(commentsText); extractedComments.forEach(tc => { tc.time >= 0 && uniqueMap.set(tc.time.toString(), tc); }); } } catch (error) { YouTubeUtils.logError("TimecodePanel", "Comment scanning failed", error); } } 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); config.autoSave && saveTimecodesToStorage(result); } else { !force && hadExistingItems || updateTimecodePanel([]); force && state.timecodes.delete(cacheKey); } return result; }; const getYouTubeChapters = () => { const items = $$([ "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" ].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 (null !== time) { let cleanTitle = titleText?.trim().replace(/\s+/g, " ") || ""; cleanTitle && cleanTitle.length > 0 && console.warn("[Timecode Debug] Raw chapter title:", cleanTitle); cleanTitle = cleanTitle.replace(/^\d{1,2}:\d{2}(?::\d{2})?\s*[-–—:]?\s*/, ""); const deduplicated = removeDuplicateText(cleanTitle); cleanTitle !== deduplicated && console.warn("[Timecode] Removed duplicate:", cleanTitle, "->", deduplicated); cleanTitle = deduplicated; chapters.set(time.toString(), { time, label: cleanTitle, isChapter: !0 }); } } }); const result = Array.from(chapters.values()).sort((a, b) => a.time - b.time); return result; }; const ensureTimecodePanelSettings = (attempt = 0) => { const advancedVisible = $('.ytp-plus-settings-section[data-section="advanced"]:not(.hidden)'); if (advancedVisible) { (() => { const advancedSection = document.querySelector('.ytp-plus-settings-section[data-section="advanced"]'); if (!advancedSection || advancedSection.querySelector(".timecode-settings-item")) { return; } const getSubmenuExpanded = () => { try { const raw = localStorage.getItem("ytp-plus-submenu-states"); if (!raw) { return null; } const parsed = JSON.parse(raw); if (parsed && "boolean" == typeof parsed.timecode) { return parsed.timecode; } } catch {} return null; }; const storedExpanded = getSubmenuExpanded(); const initialExpanded = "boolean" != typeof storedExpanded || storedExpanded; 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 ytp-plus-settings-item--with-submenu"; enableDiv.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label" for="timecode-enable-checkbox">${t("enableTimecode")}</label>\n <div class="ytp-plus-settings-item-description">${t("enableDescription")}</div>\n </div>\n <div class="ytp-plus-settings-item-actions">\n <button\n type="button"\n class="ytp-plus-submenu-toggle"\n data-submenu="timecode"\n aria-label="Toggle timecode submenu"\n aria-expanded="${initialExpanded ? "true" : "false"}"\n ${config.enabled ? "" : "disabled"}\n style="display:${config.enabled ? "inline-flex" : "none"};"\n >\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="6 9 12 15 18 9"></polyline>\n </svg>\n </button>\n <input type="checkbox" id="timecode-enable-checkbox" class="ytp-plus-settings-checkbox" data-setting="enabled" ${config.enabled ? "checked" : ""}>\n </div>\n `); const submenuWrap = document.createElement("div"); submenuWrap.className = "timecode-submenu"; submenuWrap.dataset.submenu = "timecode"; submenuWrap.style.display = config.enabled && initialExpanded ? "block" : "none"; submenuWrap.style.marginLeft = "12px"; submenuWrap.style.marginBottom = "12px"; const submenuCard = document.createElement("div"); submenuCard.className = "glass-card"; submenuCard.style.display = "flex"; submenuCard.style.flexDirection = "column"; submenuCard.style.gap = "8px"; const shortcutDiv = document.createElement("div"); shortcutDiv.className = "ytp-plus-settings-item timecode-settings-item timecode-shortcut-item"; shortcutDiv.style.display = "flex"; shortcutDiv.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label">${t("keyboardShortcut")}</label>\n <div class="ytp-plus-settings-item-description">${t("shortcutDescription")}</div>\n </div>\n <div style="display: flex; align-items: center; gap: 8px;">\n \x3c!-- Hidden native select kept for programmatic compatibility --\x3e\n <select id="timecode-modifier-combo" style="display:none;">\n ${[ "none", "ctrl", "alt", "shift", "ctrl+alt", "ctrl+shift", "alt+shift", "ctrl+alt+shift" ].map(v => `<option value="${v}" ${v === modifierValue ? "selected" : ""}>${"none" === v ? "None" : v.split("+").map(k => k.charAt(0).toUpperCase() + k.slice(1)).join("+")}</option>`).join("")}\n </select>\n\n <div class="glass-dropdown" id="timecode-modifier-dropdown" tabindex="0" role="listbox" aria-expanded="false">\n <button class="glass-dropdown__toggle" type="button" aria-haspopup="listbox">\n <span class="glass-dropdown__label">${"none" === modifierValue ? "None" : modifierValue.split("+").map(k => k.charAt(0).toUpperCase() + k.slice(1)).join("+")}</span>\n <svg class="glass-dropdown__chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>\n </button>\n <ul class="glass-dropdown__list" role="presentation">\n ${[ "none", "ctrl", "alt", "shift", "ctrl+alt", "ctrl+shift", "alt+shift", "ctrl+alt+shift" ].map(v => { const label = "none" === v ? "None" : v.split("+").map(k => k.charAt(0).toUpperCase() + k.slice(1)).join("+"); const sel = v === modifierValue ? ' aria-selected="true"' : ""; return `<li class="glass-dropdown__item" data-value="${v}" role="option"${sel}>${label}</li>`; }).join("")}\n </ul>\n </div>\n\n <span style="color:inherit;opacity:0.8;">+</span>\n <input type="text" id="timecode-key" value="${config.shortcut.key}" maxlength="1" style="width: 30px; text-align: center; background: rgba(34, 34, 34, var(--yt-header-bg-opacity)); color: white; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 4px;">\n </div>\n `); submenuCard.appendChild(shortcutDiv); submenuWrap.appendChild(submenuCard); advancedSection.append(enableDiv, submenuWrap); setTimeout(() => { const hiddenSelect = byId("timecode-modifier-combo"); const dropdown = byId("timecode-modifier-dropdown"); if (!hiddenSelect || !dropdown) { return; } const toggle = $(".glass-dropdown__toggle", dropdown); const list = $(".glass-dropdown__list", dropdown); const label = $(".glass-dropdown__label", dropdown); let items = Array.from($$(".glass-dropdown__item", list)); let idx = items.findIndex(it => "true" === it.getAttribute("aria-selected")); idx < 0 && (idx = 0); const closeList = () => { dropdown.setAttribute("aria-expanded", "false"); list.style.display = "none"; }; const openList = () => { dropdown.setAttribute("aria-expanded", "true"); list.style.display = "block"; items = Array.from($$(".glass-dropdown__item", list)); }; closeList(); toggle.addEventListener("click", () => { const expanded = "true" === dropdown.getAttribute("aria-expanded"); expanded ? closeList() : openList(); }); const outsideClickHandler = e => { dropdown.contains(e.target) || closeList(); }; window.YouTubeUtils && YouTubeUtils.cleanupManager ? YouTubeUtils.cleanupManager.registerListener(document, "click", outsideClickHandler) : document.addEventListener("click", outsideClickHandler); list.addEventListener("click", e => { const it = e.target.closest(".glass-dropdown__item"); if (!it) { return; } const val = it.dataset.value; hiddenSelect.value = val; list.querySelectorAll(".glass-dropdown__item").forEach(li => li.removeAttribute("aria-selected")); it.setAttribute("aria-selected", "true"); idx = items.indexOf(it); label.textContent = it.textContent; hiddenSelect.dispatchEvent(new Event("change", { bubbles: !0 })); closeList(); }); dropdown.addEventListener("keydown", e => { const expanded = "true" === dropdown.getAttribute("aria-expanded"); if ("ArrowDown" === e.key) { e.preventDefault(); expanded || openList(); idx = Math.min(idx + 1, items.length - 1); items.forEach(it => it.removeAttribute("aria-selected")); items[idx].setAttribute("aria-selected", "true"); items[idx].scrollIntoView({ block: "nearest" }); } else if ("ArrowUp" === e.key) { e.preventDefault(); expanded || openList(); idx = Math.max(idx - 1, 0); items.forEach(it => it.removeAttribute("aria-selected")); items[idx].setAttribute("aria-selected", "true"); items[idx].scrollIntoView({ block: "nearest" }); } else if ("Enter" === e.key || " " === e.key) { e.preventDefault(); if (!expanded) { openList(); return; } const it = items[idx]; if (it) { hiddenSelect.value = it.dataset.value; hiddenSelect.dispatchEvent(new Event("change", { bubbles: !0 })); label.textContent = it.textContent; closeList(); } } else { "Escape" === e.key && closeList(); } }); }, 0); advancedSection.addEventListener("change", e => { const target = e.target; if (target.matches && target.matches('.ytp-plus-settings-checkbox[data-setting="enabled"]')) { config.enabled = target.checked; const submenuToggle = enableDiv.querySelector('.ytp-plus-submenu-toggle[data-submenu="timecode"]'); if (submenuToggle instanceof HTMLElement) { if (config.enabled) { const stored = getSubmenuExpanded(); const nextExpanded = "boolean" != typeof stored || stored; submenuToggle.removeAttribute("disabled"); submenuToggle.style.display = "inline-flex"; submenuToggle.setAttribute("aria-expanded", nextExpanded ? "true" : "false"); submenuWrap.style.display = nextExpanded ? "block" : "none"; } else { submenuToggle.setAttribute("disabled", ""); submenuToggle.style.display = "none"; submenuWrap.style.display = "none"; } } toggleTimecodePanel(config.enabled); saveSettings(); } }); byId("timecode-modifier-combo")?.addEventListener("change", e => { const target = 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(); }); byId("timecode-key")?.addEventListener("input", e => { const target = e.target; if (target.value) { config.shortcut.key = target.value.toUpperCase(); saveSettings(); } }); })(); !$(".timecode-settings-item") && attempt < 20 && setTimeout(() => ensureTimecodePanelSettings(attempt + 1), 80); } else { attempt < 20 && setTimeout(() => ensureTimecodePanelSettings(attempt + 1), 80); } }; const handlePanelClick = e => { const {target} = e; const item = target.closest(".timecode-item"); const reloadButton = target.closest ? target.closest("#timecode-reload") : "timecode-reload" === target.id ? target : null; if (reloadButton) { e.preventDefault(); (async (buttonOverride = null) => { const button = buttonOverride || state.dom.reloadButton || byId("timecode-reload"); if (!state.isReloading && config.enabled) { state.isReloading = !0; if (button) { button.disabled = !0; button.classList.add("loading"); } try { const result = await detectTimecodes({ force: !0 }); if (Array.isArray(result) && result.length) { showNotification(t("foundTimecodes").replace("{count}", result.length)); } else { updateTimecodePanel([]); showNotification(t("noTimecodesFound")); } } catch (error) { YouTubeUtils.logError("TimecodePanel", "Reload failed", error); showNotification(t("reloadError")); } finally { if (button) { button.disabled = !1; button.classList.remove("loading"); } state.isReloading = !1; } } })(reloadButton); return; } const closeButton = target.closest ? target.closest("#timecode-close") : "timecode-close" === target.id ? target : null; if (closeButton) { toggleTimecodePanel(!1); } else if ("timecode-add-btn" === target.id) { const video = YouTubeUtils.querySelector ? YouTubeUtils.querySelector("video") : $("video"); video && showTimecodeForm(video.currentTime); } else if ("timecode-track-toggle" === target.id) { config.autoTrackPlayback = !config.autoTrackPlayback; target.textContent = t(config.autoTrackPlayback ? "tracking" : "track"); target.classList.toggle("active", config.autoTrackPlayback); state.dom.panel.classList.toggle("auto-tracking", config.autoTrackPlayback); saveSettings(); config.autoTrackPlayback && startTracking(); } else if ("timecode-export-btn" === target.id) { exportTimecodes(); } else if ("timecode-form-cancel" === target.id) { hideTimecodeForm(); } else if ("timecode-form-save" === target.id) { saveTimecodeForm(); } else if (target.classList.contains("timecode-action")) { e.stopPropagation(); const action = target.dataset.action; const index = parseInt(target.closest(".timecode-item").dataset.index, 10); "edit" === action ? editTimecode(index) : "delete" === action && deleteTimecode(index); } else if (item && !target.closest(".timecode-actions")) { const time = parseFloat(item.dataset.time); const video = $("video"); if (video && !isNaN(time)) { video.currentTime = time; video.paused && video.play(); updateActiveItem(item); } } }; const editTimecode = index => { const timecodes = getCurrentTimecodes(); if (index < 0 || index >= timecodes.length) { return; } const timecode = timecodes[index]; state.editingIndex = index; const item = state.dom.list.querySelector(`.timecode-item[data-index="${index}"]`); if (item) { item.classList.add("editing"); state.dom.list.querySelectorAll(".timecode-item.editing").forEach(el => { el !== item && el.classList.remove("editing"); }); } showTimecodeForm(timecode.time, timecode.label); }; const deleteTimecode = index => { const timecodes = getCurrentTimecodes(); if (index < 0 || index >= timecodes.length) { return; } const timecode = timecodes[index]; if (!timecode.isChapter || timecode.isUserAdded) { if (confirm(t("confirmDelete").replace("{label}", timecode.label))) { timecodes.splice(index, 1); updateTimecodePanel(timecodes); saveTimecodesToStorage(timecodes); showNotification(t("timecodeDeleted")); } } else { showNotification(t("cannotDeleteChapter")); } }; const showTimecodeForm = (currentTime, existingLabel = "") => { const {form, timeInput, labelInput} = state.dom; form.classList.add("visible"); timeInput.value = formatTime(currentTime); labelInput.value = existingLabel; requestAnimationFrame(() => labelInput.focus()); }; const hideTimecodeForm = () => { state.dom.form.classList.remove("visible"); state.editingIndex = null; state.dom.list?.querySelectorAll(".timecode-item.editing").forEach(el => { el.classList.remove("editing"); }); }; const saveTimecodeForm = () => { const {timeInput, labelInput} = state.dom; const timeValue = timeInput.value.trim(); const labelValue = labelInput.value.trim(); const time = parseTime(timeValue); if (null === time) { showNotification(t("invalidTimeFormat")); return; } const timecodes = getCurrentTimecodes(); const newTimecode = { time, label: labelValue || "", isUserAdded: !0, isChapter: !1 }; if (null !== state.editingIndex) { const oldTimecode = timecodes[state.editingIndex]; if (oldTimecode.isChapter && !oldTimecode.isUserAdded) { showNotification(t("cannotEditChapter")); hideTimecodeForm(); return; } timecodes[state.editingIndex] = { ...oldTimecode, ...newTimecode }; showNotification(t("timecodeUpdated")); } else { timecodes.push(newTimecode); showNotification(t("timecodeAdded")); } const sorted = timecodes.sort((a, b) => a.time - b.time); updateTimecodePanel(sorted); saveTimecodesToStorage(sorted); hideTimecodeForm(); }; const exportTimecodes = () => { const timecodes = getCurrentTimecodes(); if (!timecodes.length) { showNotification(t("noTimecodesToExport")); return; } const exportBtn = state.dom.panel?.querySelector("#timecode-export-btn"); if (exportBtn) { exportBtn.textContent = t("copied"); exportBtn.style.backgroundColor = "rgba(0,220,0,0.8)"; setTimeout(() => { exportBtn.textContent = t("export"); exportBtn.style.backgroundColor = ""; }, 2e3); } const videoTitle = document.title.replace(/\s-\sYouTube$/, ""); let content = `${videoTitle}\n\nTimecodes:\n`; timecodes.forEach(tc => { const label = tc.label?.trim(); content += label ? `${formatTime(tc.time)} - ${label}\n` : `${formatTime(tc.time)}\n`; }); navigator.clipboard?.writeText && navigator.clipboard.writeText(content).then(() => { showNotification(t("timecodesCopied")); }); }; const updateTimecodePanel = timecodes => { const {list, empty} = state.dom; if (!list || !empty) { return; } const isEmpty = !timecodes.length; empty.style.display = isEmpty ? "flex" : "none"; list.style.display = isEmpty ? "none" : "block"; isEmpty ? list.replaceChildren() : list.innerHTML = _createHTML(timecodes.map((tc, i) => { const timeStr = formatTime(tc.time); let rawLabel = tc.label?.trim() || ""; rawLabel = rawLabel.replace(/^\d{1,2}:\d{2}(?::\d{2})?\s*[-–—:]?\s*/, ""); const beforeDedup = rawLabel; rawLabel = removeDuplicateText(rawLabel); beforeDedup !== rawLabel && rawLabel.length > 0 && console.warn("[Timecode] Display deduplicated:", beforeDedup, "->", rawLabel); const normalizedTime = timeStr.replace(/^0+:/, ""); const normalizedLabel = rawLabel.replace(/^0+:/, ""); const hasCustomLabel = rawLabel && rawLabel !== timeStr && normalizedLabel !== normalizedTime && rawLabel !== tc.originalText && rawLabel.length > 0; const displayLabel = hasCustomLabel ? rawLabel : ""; const safeLabel = displayLabel.replace(/[<>&"']/g, c => ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" }[c])); const isEditable = !tc.isChapter || tc.isUserAdded; return `\n <div class="timecode-item ${tc.isChapter ? "has-chapter" : ""}" data-time="${tc.time}" data-index="${i}">\n <div class="timecode-time">${timeStr}</div>\n ${safeLabel ? `<div class="timecode-label" title="${safeLabel}">${safeLabel}</div>` : ""}\n <div class="timecode-progress"></div>\n ${isEditable ? `\n <div class="timecode-actions">\n <button class="timecode-action edit" data-action="edit" title="${t("edit")}">✎</button>\n <button class="timecode-action delete" data-action="delete" title="${t("delete")}">✕</button>\n </div>\n ` : ""}\n </div>\n `; }).join("")); }; const updateActiveItem = activeItem => { const items = state.dom.list?.querySelectorAll(".timecode-item"); if (items) { items.forEach(item => item.classList.remove("active", "pulse")); if (activeItem) { activeItem.classList.add("active", "pulse"); setTimeout(() => activeItem.classList.remove("pulse"), 800); } } }; const startTracking = () => { if (state.trackingId) { return; } const track = () => { try { const video = $("video"); const {panel, currentTime, list} = state.dom; if (!video || !panel || panel.classList.contains("hidden") || !config.autoTrackPlayback) { if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } return; } currentTime && !isNaN(video.currentTime) && (currentTime.textContent = formatTime(video.currentTime)); 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); isNaN(time) || (video.currentTime >= time ? activeIndex = i : -1 === nextIndex && (nextIndex = i)); } if (state.activeIndex !== activeIndex) { null !== state.activeIndex && state.activeIndex >= 0 && items[state.activeIndex] && items[state.activeIndex].classList.remove("active"); if (activeIndex >= 0 && items[activeIndex]) { items[activeIndex].classList.add("active"); try { items[activeIndex].scrollIntoView({ behavior: "smooth", block: "center" }); } catch { items[activeIndex].scrollIntoView(!1); } } state.activeIndex = activeIndex; } 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}%`; } } } } } config.autoTrackPlayback && (state.trackingId = requestAnimationFrame(track)); } catch (error) { console.warn("Timecode tracking error:", error); if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } } }; state.trackingId = requestAnimationFrame(track); }; const makeDraggable = panel => { const header = panel.querySelector("#timecode-header"); if (!header) { return; } let startX, startY, startLeft, startTop; YouTubeUtils.cleanupManager.registerListener(header, "mousedown", e => { if (0 !== e.button) { return; } state.dragging = !0; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); panel.style.left || (panel.style.left = `${rect.left}px`); 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 = !1; 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); }); }; const saveTimecodesToStorage = timecodes => { const videoId = new URLSearchParams(window.location.search).get("v"); if (videoId) { try { const minimal = timecodes.map(tc => ({ t: tc.time, l: tc.label?.trim() || "", c: tc.isChapter || !1, u: tc.isUserAdded || !1 })); 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 || !1 })).sort((a, b) => a.time - b.time) : null; } catch { return null; } }; const getCurrentTimecodes = () => { const items = state.dom.list?.querySelectorAll(".timecode-item"); return items ? Array.from(items).map(item => { const time = parseFloat(item.dataset.time); const labelEl = item.querySelector(".timecode-label"); const label = labelEl?.textContent?.trim() || ""; return { time, label, isChapter: item.classList.contains("has-chapter"), isUserAdded: !item.classList.contains("has-chapter") || !1 }; }).sort((a, b) => a.time - b.time) : []; }; const toggleTimecodePanel = show => { $$("#timecode-panel").forEach(panel => { panel !== state.dom.panel && panel.remove(); }); const panel = state.dom.panel || (() => { if (state.dom.panel) { return state.dom.panel; } $$("#timecode-panel").forEach(p => p.remove()); const panel = document.createElement("div"); panel.id = "timecode-panel"; panel.className = config.enabled ? "" : "hidden"; config.autoTrackPlayback && panel.classList.add("auto-tracking"); panel.innerHTML = _createHTML(`\n <div id="timecode-header">\n <h3 id="timecode-title">\n <div id="timecode-tracking-indicator"></div>\n ${t("timecodes")}\n <span id="timecode-current-time"></span>\n </h3>\n <div id="timecode-header-controls">\n <button id="timecode-reload" title="${t("reload")}" aria-label="${t("reload")}">⟳</button>\n <button id="timecode-close" title="${t("close")}" aria-label="${t("close")}">\n <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\n </svg>\n </button>\n </div>\n </div>\n <div id="timecode-list"></div>\n <div id="timecode-empty">\n <div>${t("noTimecodesFound")}</div>\n <div style="margin-top:5px;font-size:12px">${t("clickToAdd")}</div>\n </div>\n <div id="timecode-form">\n <input type="text" id="timecode-form-time" placeholder="${t("timePlaceholder")}">\n <input type="text" id="timecode-form-label" placeholder="${t("labelPlaceholder")}">\n <div id="timecode-form-buttons">\n <button type="button" id="timecode-form-cancel">${t("cancel")}</button>\n <button type="button" id="timecode-form-save" class="save">${t("save")}</button>\n </div>\n </div>\n <div id="timecode-actions">\n <button id="timecode-add-btn">${t("add")}</button>\n <button id="timecode-export-btn" ${config.export ? "" : 'style="display:none"'}>${t("export")}</button>\n <button id="timecode-track-toggle" class="${config.autoTrackPlayback ? "active" : ""}">${t(config.autoTrackPlayback ? "tracking" : "track")}</button>\n </div>\n `); state.dom = { panel, list: panel.querySelector("#timecode-list"), empty: panel.querySelector("#timecode-empty"), form: panel.querySelector("#timecode-form"), timeInput: panel.querySelector("#timecode-form-time"), labelInput: panel.querySelector("#timecode-form-label"), currentTime: panel.querySelector("#timecode-current-time"), trackToggle: panel.querySelector("#timecode-track-toggle"), reloadButton: panel.querySelector("#timecode-reload") }; panel.addEventListener("click", handlePanelClick); makeDraggable(panel); document.body.appendChild(panel); applySavedPanelPosition(panel); return panel; })(); void 0 === show && (show = panel.classList.contains("hidden")); panel.classList.toggle("hidden", !show); if (show) { applySavedPanelPosition(panel); const saved = loadTimecodesFromStorage(); saved?.length ? updateTimecodePanel(saved) : config.autoDetect && detectTimecodes().catch(err => console.error("[Timecode] Detection failed:", err)); config.autoTrackPlayback && startTracking(); } else if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } }; const cleanup = () => { (() => { if (state.trackingId) { cancelAnimationFrame(state.trackingId); state.trackingId = 0; } })(); if (state.dom.panel) { state.dom.panel.remove(); state.dom.panel = null; } }; const init = () => { if (initStarted) { return; } if (!isRelevantRoute()) { return; } const appRoot = "function" == typeof YouTubeUtils?.querySelector && YouTubeUtils.querySelector("ytd-app") || $("ytd-app"); if (!appRoot) { (() => { const timeoutId = setTimeout(init, 250); YouTubeUtils.cleanupManager?.registerTimeout?.(timeoutId); })(); return; } initStarted = !0; (() => { try { const saved = localStorage.getItem(config.storageKey); if (!saved) { return; } const parsed = JSON.parse(saved); if ("object" != typeof parsed || null === parsed) { console.warn("[Timecode] Invalid settings format"); return; } "boolean" == typeof parsed.enabled && (config.enabled = parsed.enabled); "boolean" == typeof parsed.autoDetect && (config.autoDetect = parsed.autoDetect); "boolean" == typeof parsed.autoSave && (config.autoSave = parsed.autoSave); "boolean" == typeof parsed.autoTrackPlayback && (config.autoTrackPlayback = parsed.autoTrackPlayback); "boolean" == typeof parsed.export && (config.export = parsed.export); if (parsed.shortcut && "object" == typeof parsed.shortcut) { "string" == typeof parsed.shortcut.key && (config.shortcut.key = parsed.shortcut.key); "boolean" == typeof parsed.shortcut.shiftKey && (config.shortcut.shiftKey = parsed.shortcut.shiftKey); "boolean" == typeof parsed.shortcut.altKey && (config.shortcut.altKey = parsed.shortcut.altKey); "boolean" == typeof parsed.shortcut.ctrlKey && (config.shortcut.ctrlKey = parsed.shortcut.ctrlKey); } if (parsed.panelPosition && "object" == typeof parsed.panelPosition) { const {left, top} = parsed.panelPosition; "number" == typeof left && "number" == typeof top && !isNaN(left) && !isNaN(top) && left >= 0 && top >= 0 && (config.panelPosition = { left, top }); } } catch (error) { console.error("[Timecode] Error loading settings:", error); } })(); (() => { if (byId("timecode-panel-styles")) { return; } YouTubeUtils.StyleManager.add("timecode-panel-styles", "\n :root{--tc-panel-bg:rgba(255,255,255,0.06);--tc-panel-border:rgba(255,255,255,0.12);--tc-panel-color:#fff}\n html[dark],body[dark]{--tc-panel-bg:rgba(34,34,34,0.75);--tc-panel-border:rgba(255,255,255,0.12);--tc-panel-color:#fff}\n html:not([dark]){--tc-panel-bg:rgba(255,255,255,0.95);--tc-panel-border:rgba(0,0,0,0.08);--tc-panel-color:#222}\n #timecode-panel{position:fixed;right:20px;top:80px;background:var(--tc-panel-bg);border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,0.45);width:320px;max-height:70vh;z-index:10000;color:var(--tc-panel-color);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%);border:1.5px solid var(--tc-panel-border);transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .28s;overflow:hidden;display:flex;flex-direction:column}\n #timecode-panel.hidden{transform:translateX(300px);opacity:0;pointer-events:none}\n #timecode-panel.auto-tracking{box-shadow:0 12px 48px rgba(255,0,0,0.12);border-color:rgba(255,0,0,0.25)}\n #timecode-header{display:flex;justify-content:space-between;align-items:center;padding:14px;border-bottom:1px solid rgba(255,255,255,0.04);background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);cursor:move}\n #timecode-title{font-weight:600;margin:0;font-size:15px;user-select:none;display:flex;align-items:center;gap:8px}\n #timecode-tracking-indicator{width:8px;height:8px;background:red;border-radius:50%;opacity:0;transition:opacity .3s}\n #timecode-panel.auto-tracking #timecode-tracking-indicator{opacity:1}\n #timecode-current-time{font-family:monospace;font-size:12px;padding:2px 6px;background:rgba(255,0,0,.3);border-radius:3px;margin-left:auto}\n #timecode-header-controls{display:flex;align-items:center;gap:6px}\n #timecode-reload,#timecode-close{background:transparent;border:none;color:inherit;cursor:pointer;width:28px;height:28px;padding:0;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:background .18s,color .18s}\n #timecode-reload:hover,#timecode-close:hover{background:rgba(255,255,255,0.04)}\n #timecode-reload.loading{animation:spin .8s linear infinite}\n #timecode-list{overflow-y:auto;padding:8px 0;max-height:calc(70vh - 80px);scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}\n #timecode-list::-webkit-scrollbar{width:6px}\n #timecode-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.3);border-radius:3px}\n .timecode-item{padding:10px 14px;display:flex;align-items:center;cursor:pointer;transition:background-color .16s,transform .12s;border-left:3px solid transparent;position:relative;border-radius:8px;margin:6px 10px}\n .timecode-item:hover{background:rgba(255,255,255,0.04);transform:translateY(-2px)}\n .timecode-item:hover .timecode-actions{opacity:1}\n .timecode-item.active{background:linear-gradient(90deg, rgba(255,68,68,0.12), rgba(255,68,68,0.04));border-left-color:#ff6666;box-shadow:inset 0 0 0 1px rgba(255,68,68,0.03)}\n .timecode-item.active.pulse{animation:pulse .8s ease-out}\n .timecode-item.editing{background:linear-gradient(90deg, rgba(255,170,0,0.08), rgba(255,170,0,0.03));border-left-color:#ffaa00}\n .timecode-item.editing .timecode-actions{opacity:1}\n @keyframes pulse{0%{transform:scale(1)}50%{transform:scale(1.02)}100%{transform:scale(1)}}\n /* spin keyframe defined in shared-keyframes (basic.js) */\n .timecode-time{font-family:monospace;margin-right:10px;color:rgba(255,255,255,.8);font-size:13px;min-width:45px;flex-shrink:0}\n .timecode-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;flex:1;margin-left:4px}\n .timecode-item:not(:has(.timecode-label)) .timecode-time{flex:1;text-align:left}\n .timecode-item.has-chapter .timecode-time{color:#ff4444}\n .timecode-progress{width:0;height:2px;background:#ff4444;position:absolute;bottom:0;left:0;transition:width .3s;opacity:.8}\n .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}\n .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}\n .timecode-action:hover{color:#fff;background:rgba(255,255,255,.2)}\n .timecode-action.edit:hover{color:#ffaa00}\n .timecode-action.delete:hover{color:#ff4444}\n #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}\n #timecode-form{padding:12px;border-top:1px solid rgba(255,255,255,.04);display:none}\n #timecode-form.visible{display:block}\n #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}\n #timecode-form input::placeholder{color:rgba(255,255,255,.6)}\n #timecode-form-buttons{display:flex;gap:8px;justify-content:flex-end}\n #timecode-form-buttons button{padding:6px 12px;border:none;border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s}\n #timecode-form-cancel{background:rgba(255,255,255,.2);color:#fff}\n #timecode-form-cancel:hover{background:rgba(255,255,255,.3)}\n #timecode-form-save{background:#ff4444;color:#fff}\n #timecode-form-save:hover{background:#ff6666}\n #timecode-actions{padding:10px;border-top:1px solid rgba(255,255,255,.04);display:flex;gap:8px;background:linear-gradient(180deg,transparent,rgba(0,0,0,0.03))}\n #timecode-actions button{padding:8px 12px;border:none;border-radius:8px;cursor:pointer;font-size:13px;transition:background .18s;color:inherit;background:rgba(255,255,255,0.02)}\n #timecode-actions button:hover{background:rgba(255,255,255,0.04)}\n #timecode-track-toggle.active{background:linear-gradient(90deg,#ff6b6b,#ff4444);color:#fff}\n "); })(); (() => { const keydownHandler = e => { if (!config.enabled) { return; } const target = 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(); } }; window.YouTubeUtils && YouTubeUtils.cleanupManager ? YouTubeUtils.cleanupManager.registerListener(document, "keydown", keydownHandler) : document.addEventListener("keydown", keydownHandler); })(); (() => { let currentVideoId = new URLSearchParams(window.location.search).get("v"); const handleNavigationChange = () => { const newVideoId = new URLSearchParams(window.location.search).get("v"); if (newVideoId !== currentVideoId && "/watch" === window.location.pathname) { 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(); saved?.length ? updateTimecodePanel(saved) : config.autoDetect && setTimeout(() => detectTimecodes().catch(err => console.error("[Timecode] Detection failed:", err)), 500); config.autoTrackPlayback && startTracking(); } } }; window.YouTubeUtils && YouTubeUtils.cleanupManager ? YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", handleNavigationChange) : document.addEventListener("yt-navigate-finish", handleNavigationChange); })(); let modalObserver = null; let modalObserverTimeout = null; const settingsModalHandler = () => { const modal = document.querySelector(".ytp-plus-settings-modal"); if (modal) { (modalEl => { if (modalEl && modalEl instanceof Element) { if (modalObserver) { try { modalObserver.disconnect(); } catch {} modalObserver = null; } modalObserver = new MutationObserver(() => { modalObserverTimeout || (modalObserverTimeout = setTimeout(() => { modalObserverTimeout = null; $('.ytp-plus-settings-section[data-section="advanced"]:not(.hidden)') && !$(".timecode-settings-item") && setTimeout(() => ensureTimecodePanelSettings(), 50); }, 30)); }); YouTubeUtils.cleanupManager.registerObserver(modalObserver); modalObserver.observe(modalEl, { childList: !0, subtree: !0, attributes: !0, attributeFilter: [ "class" ] }); } })(modal); setTimeout(() => ensureTimecodePanelSettings(), 100); } }; YouTubeUtils.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "youtube-plus-settings-modal-opened", settingsModalHandler) : document.addEventListener("youtube-plus-settings-modal-opened", settingsModalHandler); YouTubeUtils.cleanupManager.registerListener(document, "click", e => { const target = e.target; const navItem = target?.closest?.(".ytp-plus-settings-nav-item"); "advanced" === navItem?.dataset?.section && setTimeout(() => ensureTimecodePanelSettings(), 50); }, !0); if (config.enabled && !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 handleNavigate = () => { isRelevantRoute() ? init() : initStarted && cleanup(); }; window.YouTubePlusLazyLoader ? window.YouTubePlusLazyLoader.register("timecode", handleNavigate, { priority: 1 }) : "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", handleNavigate, { once: !0 }) : handleNavigate(); window.YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", handleNavigate, { passive: !0 }) : document.addEventListener("yt-navigate-finish", handleNavigate, { passive: !0 }); window.addEventListener("beforeunload", cleanup); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); let featureEnabled = !0; const loadFeatureEnabled = () => window.YouTubeUtils?.loadFeatureEnabled?.("enablePlaylistSearch") ?? !0; const setFeatureEnabled = nextEnabled => { featureEnabled = !1 !== nextEnabled; if (featureEnabled) { ensureInit(); handleNavigation(); } else { cleanup(); } }; featureEnabled = loadFeatureEnabled(); if (window._playlistSearchInitialized) { return; } window._playlistSearchInitialized = !0; const qs = sel => window.YouTubeUtils?.$(sel) || document.querySelector(sel); const t = window.YouTubeUtils?.t || (key => key || ""); const shouldRunOnThisPage = () => window.location.hostname.endsWith("youtube.com") && "music.youtube.com" !== window.location.hostname && ("/watch" === window.location.pathname || "/playlist" === window.location.pathname); const isRelevantRoute = () => { if (!shouldRunOnThisPage()) { return !1; } try { const params = new URLSearchParams(window.location.search); return params.has("list"); } catch { return !1; } }; const debounce = window.YouTubeUtils?.debounce || ((fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }); const throttle = window.YouTubeUtils?.throttle || ((fn, ms) => { let t; return (...a) => { if (!t) { fn(...a); t = !0; setTimeout(() => t = !1, ms); } }; }); const config = { enabled: !0, storageKey: "youtube_playlist_search_settings", searchDebounceMs: 150, observerThrottleMs: 300, maxPlaylistItems: 1e4, maxQueryLength: 300, deleteDelay: 250 }; const state = { searchInput: null, searchResults: null, originalItems: [], currentPlaylistId: null, mutationObserver: null, rafId: null, itemsCache: new Map, itemsContainer: null, itemSelector: null, itemTagName: null, playlistPanel: null, isPlaylistPage: !1, isDeleting: !1, deleteMode: !1, selectedItems: new Set }; const inputDebouncers = new WeakMap; const setupInputDelegation = (() => { let attached = !1; return () => { if (attached) { return; } attached = !0; const handleFocus = input => { input.style.borderColor = "var(--yt-spec-call-to-action)"; }; const handleBlur = input => { input.style.borderColor = "var(--yt-spec-10-percent-layer)"; }; const handleInput = input => { let debounced = inputDebouncers.get(input); if (!debounced) { debounced = debounce(value => { if (value.length > config.maxQueryLength) { const truncated = value.substring(0, config.maxQueryLength); input.value = truncated; filterPlaylistItems(truncated); return; } filterPlaylistItems(value); }, config.searchDebounceMs); inputDebouncers.set(input, debounced); } debounced(input.value || ""); }; const delegator = window.YouTubePlusEventDelegation; if (delegator?.on) { delegator.on(document, "focusin", ".ytplus-playlist-search-input", (ev, target) => { target && handleFocus(target); }); delegator.on(document, "focusout", ".ytplus-playlist-search-input", (ev, target) => { target && handleBlur(target); }); delegator.on(document, "input", ".ytplus-playlist-search-input", (ev, target) => { target && handleInput(target); }); } else { document.addEventListener("focusin", ev => { const target = ev.target?.closest?.(".ytplus-playlist-search-input"); target && handleFocus(target); }, !0); document.addEventListener("focusout", ev => { const target = ev.target?.closest?.(".ytplus-playlist-search-input"); target && handleBlur(target); }, !0); document.addEventListener("input", ev => { const target = ev.target?.closest?.(".ytplus-playlist-search-input"); target && handleInput(target); }, !0); } }; })(); const getCurrentPlaylistId = () => { try { const urlParams = new URLSearchParams(window.location.search); const listId = urlParams.get("list"); return listId && /^[a-zA-Z0-9_-]+$/.test(listId) ? listId : null; } catch (error) { console.warn("[Playlist Search] Failed to get playlist ID:", error); return null; } }; const getPlaylistContext = () => { if ("/playlist" === window.location.pathname) { const panel = qs("ytd-playlist-video-list-renderer"); if (!panel) { return null; } const itemsContainer = panel.querySelector("#contents") || panel.querySelector("ytd-playlist-video-list-renderer #contents"); return { panel, itemsContainer, itemSelector: "ytd-playlist-video-renderer", itemTagName: "YTD-PLAYLIST-VIDEO-RENDERER", isPlaylistPage: !0 }; } if ("/watch" === window.location.pathname) { const panel = qs("ytd-playlist-panel-renderer"); if (!panel) { return null; } const itemsContainer = panel.querySelector("#items") || panel.querySelector(".playlist-items.style-scope.ytd-playlist-panel-renderer") || panel.querySelector(".playlist-items"); return { panel, itemsContainer, itemSelector: "ytd-playlist-panel-video-renderer", itemTagName: "YTD-PLAYLIST-PANEL-VIDEO-RENDERER", isPlaylistPage: !1 }; } return null; }; const addSearchUI = () => { if (!config.enabled) { return; } if (!shouldRunOnThisPage()) { return; } const playlistId = getCurrentPlaylistId(); if (!playlistId) { return; } const context = getPlaylistContext(); if (!context) { return; } const {panel: playlistPanel, itemsContainer, itemSelector, itemTagName} = context; if (playlistPanel.querySelector(".ytplus-playlist-search")) { return; } state.currentPlaylistId = playlistId; state.itemsContainer = itemsContainer || null; state.itemSelector = itemSelector; state.itemTagName = itemTagName; state.playlistPanel = playlistPanel; state.isPlaylistPage = context.isPlaylistPage; const searchContainer = document.createElement("div"); searchContainer.className = "ytplus-playlist-search"; searchContainer.style.cssText = "\n padding: 8px 16px;\n background: transparent;\n border-bottom: 1px solid var(--yt-spec-10-percent-layer);\n z-index: 50;\n width: 94%;\n "; setTimeout(() => { try { if (!state.isPlaylistPage) { searchContainer.style.position = "sticky"; searchContainer.style.top = "0"; searchContainer.style.zIndex = "1"; searchContainer.style.background = "transparent"; return; } const panel = state.playlistPanel || getPlaylistContext()?.panel; const topOffset = state.isPlaylistPage ? 84 : 8; let scrollAncestor = panel; for (;scrollAncestor && scrollAncestor !== document.body; ) { const style = window.getComputedStyle(scrollAncestor); const overflowY = style.overflowY; if (("auto" === overflowY || "scroll" === overflowY) && scrollAncestor.scrollHeight > scrollAncestor.clientHeight) { break; } scrollAncestor = scrollAncestor.parentElement; } if (scrollAncestor && scrollAncestor !== document.body) { searchContainer.style.position = "sticky"; searchContainer.style.top = `${topOffset}px`; searchContainer.style.background = "var(--yt-spec-badge-chip-background)"; searchContainer.style.backdropFilter = "blur(6px)"; searchContainer.style.boxShadow = "var(--yt-shadow)"; } else if (panel) { const rect = panel.getBoundingClientRect(); searchContainer.style.position = "fixed"; searchContainer.style.top = `${topOffset}px`; searchContainer.style.left = `${rect.left}px`; searchContainer.style.width = `${rect.width}px`; searchContainer.style.background = "var(--yt-spec-badge-chip-background)"; searchContainer.style.backdropFilter = "blur(6px)"; searchContainer.style.boxShadow = "0 6px 20px rgba(0,0,0,0.4)"; searchContainer.style.zIndex = "9999"; const recompute = debounce(() => { const r = panel.getBoundingClientRect(); searchContainer.style.left = `${r.left}px`; searchContainer.style.width = `${r.width}px`; }, 120); window.addEventListener("resize", recompute, { passive: !0 }); window.addEventListener("scroll", recompute, { passive: !0 }); } else { searchContainer.style.position = "sticky"; searchContainer.style.top = `${topOffset}px`; searchContainer.style.background = "var(--yt-spec-badge-chip-background)"; } } catch {} }, 100); const searchInput = document.createElement("input"); searchInput.type = "text"; const playlistName = ((playlistPanel, listId) => { try { const sel = [ "ytd-playlist-header-renderer #title", "ytd-playlist-header-renderer .title", ".title", "h3 a", "#header-title", "#title", ".playlist-title", "h1.title" ]; for (const s of sel) { const el = playlistPanel.querySelector(s) || qs(s); if (el && el.textContent && el.textContent.trim()) { const title = el.textContent.trim(); return title.length > 100 ? title.substring(0, 100) + "..." : title; } } const meta = qs('meta[name="title"]') || qs('meta[property="og:title"]'); if (meta && meta.content) { const title = meta.content.trim(); return title.length > 100 ? title.substring(0, 100) + "..." : title; } } catch (error) { console.warn("[Playlist Search] Failed to get display name:", error); } return listId && "string" == typeof listId ? listId.substring(0, 50) : "playlist"; })(playlistPanel, playlistId); const placeholderKey = state.isPlaylistPage ? "searchPlaceholderPlaylistPage" : "searchPlaceholder"; searchInput.placeholder = t(placeholderKey, { playlist: playlistName }); searchInput.className = "ytplus-playlist-search-input"; searchInput.style.cssText = "\n width: 93%;\n padding: 8px 16px;\n border: 1px solid var(--yt-spec-10-percent-layer);\n border-radius: 20px;\n background: var(--yt-spec-badge-chip-background);\n color: var(--yt-spec-text-primary);\n font-size: 14px;\n font-family: 'Roboto', Arial, sans-serif;\n outline: none;\n transition: border-color 0.2s;\n "; setupInputDelegation(); searchContainer.appendChild(searchInput); state.searchInput = searchInput; if (itemsContainer) { const firstVideo = itemsContainer.querySelector(itemSelector); firstVideo && firstVideo.parentElement === itemsContainer ? itemsContainer.insertBefore(searchContainer, firstVideo) : itemsContainer.appendChild(searchContainer); } else { playlistPanel.firstChild ? playlistPanel.insertBefore(searchContainer, playlistPanel.firstChild) : playlistPanel.appendChild(searchContainer); } collectOriginalItems(); addDeleteUI(searchContainer); setupPlaylistObserver(); }; const setupPlaylistObserver = () => { state.mutationObserver && state.mutationObserver.disconnect(); const playlistPanel = state.playlistPanel || getPlaylistContext()?.panel; if (!playlistPanel || !state.itemTagName) { return; } let lastUpdateCount = state.originalItems.length; let updateScheduled = !1; const itemTagName = state.itemTagName; const itemSelector = state.itemSelector; const itemsRoot = state.itemsContainer || playlistPanel; const handleMutations = throttle(mutations => { if (updateScheduled) { return; } const hasRelevantChange = mutations.some(mutation => { if ("childList" !== mutation.type) { return !1; } if (0 === mutation.addedNodes.length && 0 === mutation.removedNodes.length) { return !1; } for (let i = 0; i < mutation.addedNodes.length; i++) { const node = mutation.addedNodes[i]; if (1 === node.nodeType) { const element = node; if (element.tagName === itemTagName) { return !0; } } } for (let i = 0; i < mutation.removedNodes.length; i++) { const node = mutation.removedNodes[i]; if (1 === node.nodeType) { const element = node; if (element.tagName === itemTagName) { return !0; } } } return !1; }); if (hasRelevantChange) { updateScheduled = !0; requestAnimationFrame(() => { const currentCount = lastUpdateCount; const newItems = itemsRoot ? itemsRoot.querySelectorAll(itemSelector) : []; if (newItems.length !== currentCount) { lastUpdateCount = newItems.length; collectOriginalItems(); state.searchInput && state.searchInput.value && filterPlaylistItems(state.searchInput.value); } updateScheduled = !1; }); } }, config.observerThrottleMs); state.mutationObserver = new MutationObserver(handleMutations); const targetElement = itemsRoot || playlistPanel; state.mutationObserver.observe(targetElement, { childList: !0, subtree: !itemsRoot }); window.YouTubeUtils?.cleanupManager?.registerObserver && window.YouTubeUtils.cleanupManager.registerObserver(state.mutationObserver); }; const collectOriginalItems = () => { const itemsRoot = state.itemsContainer || state.playlistPanel; if (!itemsRoot || !state.itemSelector) { return; } const items = itemsRoot.querySelectorAll(state.itemSelector); items.length > config.maxPlaylistItems && console.warn(`[Playlist Search] Playlist has ${items.length} items, limiting to ${config.maxPlaylistItems}`); const currentVideoIds = new Set; const itemsArray = Array.from(items).slice(0, config.maxPlaylistItems); state.originalItems = itemsArray.map((item, index) => { const videoId = item.getAttribute("video-id") || `item-${index}`; currentVideoIds.add(videoId); if (state.itemsCache.has(videoId)) { const cached = state.itemsCache.get(videoId); if (cached.element === item) { return cached; } } const titleEl = item.querySelector("#video-title") || item.querySelector("a#video-title"); const bylineEl = item.querySelector("#byline") || item.querySelector("#channel-name") || item.querySelector("ytd-channel-name a"); const title = titleEl?.textContent || ""; const channel = bylineEl?.textContent || ""; const itemData = { element: item, videoId, titleOriginal: title, channelOriginal: channel, title: title.trim().toLowerCase(), channel: channel.trim().toLowerCase() }; state.itemsCache.set(videoId, itemData); return itemData; }); for (const [videoId] of state.itemsCache) { currentVideoIds.has(videoId) || state.itemsCache.delete(videoId); } }; const filterPlaylistItems = query => { state.rafId && cancelAnimationFrame(state.rafId); if (query && "string" != typeof query) { console.warn("[Playlist Search] Invalid query type"); return; } query && query.length > config.maxQueryLength && (query = query.substring(0, config.maxQueryLength)); if (!query || "" === query.trim()) { state.rafId = requestAnimationFrame(() => { state.originalItems.forEach(item => { item.element.style.display = ""; }); state.rafId = null; }); return; } const searchTerm = query.toLowerCase().trim(); let visibleCount = 0; state.rafId = requestAnimationFrame(() => { const updates = []; state.originalItems.forEach(item => { const matches = item.title.includes(searchTerm) || item.channel.includes(searchTerm); if (matches) { "none" === item.element.style.display && updates.push({ element: item.element, display: "" }); visibleCount++; } else { "none" !== item.element.style.display && updates.push({ element: item.element, display: "none" }); } }); updates.forEach(update => { update.element.style.display = update.display; }); updateResultsCount(visibleCount, state.originalItems.length); state.rafId = null; }); }; const updateResultsCount = (visible, total) => { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug(`[Playlist Search] Showing ${visible} of ${total} videos`); }; const logError = (context, error) => { const errorObj = error instanceof Error ? error : new Error(String(error)); window.YouTubeErrorBoundary ? window.YouTubeErrorBoundary.logError(errorObj, { context }) : console.error(`[YouTube+][PlaylistSearch] ${context}:`, error); }; const withErrorBoundary = (fn, context) => window.YouTubeErrorBoundary?.withErrorBoundary ? window.YouTubeErrorBoundary.withErrorBoundary(fn, "PlaylistSearch") : (...args) => { try { return fn(...args); } catch (e) { logError(context, e); return null; } }; const toggleDeleteMode = withErrorBoundary(() => { state.deleteMode = !state.deleteMode; state.selectedItems.clear(); const container = state.playlistPanel || getPlaylistContext()?.panel; if (!container) { return; } const toggleBtn = container.querySelector(".ytplus-playlist-delete-toggle"); const deleteBar = container.querySelector(".ytplus-playlist-delete-bar"); if (state.deleteMode) { if (toggleBtn) { toggleBtn.classList.add("active"); toggleBtn.setAttribute("aria-pressed", "true"); toggleBtn.title = t("playlistDeleteModeExit"); } deleteBar && (deleteBar.style.display = ""); addCheckboxesToItems(); } else { if (toggleBtn) { toggleBtn.classList.remove("active"); toggleBtn.setAttribute("aria-pressed", "false"); toggleBtn.title = t("playlistDeleteMode"); } deleteBar && (deleteBar.style.display = "none"); removeCheckboxesFromItems(); } updateDeleteBarState(); }, "toggleDeleteMode"); const addCheckboxesToItems = withErrorBoundary(() => { const itemsRoot = state.itemsContainer || state.playlistPanel; if (!itemsRoot || !state.itemSelector) { return; } const items = itemsRoot.querySelectorAll(state.itemSelector); items.forEach((item, idx) => { if (item.querySelector(".ytplus-playlist-item-checkbox")) { return; } const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.className = "ytplus-playlist-item-checkbox ytp-plus-settings-checkbox"; checkbox.setAttribute("aria-label", t("playlistSelectVideo")); checkbox.dataset.index = String(idx); checkbox.style.cssText = "\n position: absolute;\n top: 8px;\n left: 8px;\n z-index: 2;\n cursor: pointer;\n "; checkbox.addEventListener("change", () => { const videoId = item.getAttribute("video-id") || `item-${idx}`; checkbox.checked ? state.selectedItems.add(videoId) : state.selectedItems.delete(videoId); updateDeleteBarState(); }); checkbox.addEventListener("click", e => e.stopPropagation()); item.style.position = "relative"; item.insertBefore(checkbox, item.firstChild); }); }, "addCheckboxesToItems"); const removeCheckboxesFromItems = withErrorBoundary(() => { const itemsRoot = state.itemsContainer || state.playlistPanel; if (itemsRoot) { itemsRoot.querySelectorAll(".ytplus-playlist-item-checkbox").forEach(cb => cb.remove()); state.selectedItems.clear(); } }, "removeCheckboxesFromItems"); const updateDeleteBarState = withErrorBoundary(() => { const container = state.playlistPanel || getPlaylistContext()?.panel; if (!container) { return; } const deleteBtn = container.querySelector(".ytplus-playlist-delete-selected"); const countSpan = container.querySelector(".ytplus-playlist-selected-count"); if (deleteBtn) { deleteBtn.disabled = 0 === state.selectedItems.size; deleteBtn.style.opacity = state.selectedItems.size > 0 ? "1" : "0.5"; } countSpan && (countSpan.textContent = t("playlistSelectedCount", { count: state.selectedItems.size })); }, "updateDeleteBarState"); const selectAllItems = withErrorBoundary(() => { const itemsRoot = state.itemsContainer || state.playlistPanel; if (itemsRoot) { itemsRoot.querySelectorAll(".ytplus-playlist-item-checkbox").forEach(cb => { const item = cb.closest(state.itemSelector); if (item && "none" !== item.style.display) { cb.checked = !0; const videoId = item.getAttribute("video-id") || `item-${cb.dataset.index}`; state.selectedItems.add(videoId); } }); updateDeleteBarState(); } }, "selectAllItems"); const clearAllItems = withErrorBoundary(() => { const itemsRoot = state.itemsContainer || state.playlistPanel; if (itemsRoot) { itemsRoot.querySelectorAll(".ytplus-playlist-item-checkbox").forEach(cb => { cb.checked = !1; }); state.selectedItems.clear(); updateDeleteBarState(); } }, "clearAllItems"); const removeItemViaMenu = item => new Promise(resolve => { try { const menuBtn = item.querySelector("button#button[aria-label]") || item.querySelector("yt-icon-button#button") || item.querySelector("ytd-menu-renderer button") || item.querySelector('[aria-haspopup="menu"]') || item.querySelector("button.yt-icon-button"); if (!menuBtn) { console.warn("[Playlist Search] Could not find menu button for item"); resolve(!1); return; } menuBtn.click(); setTimeout(() => { try { const menuItems = document.querySelectorAll("tp-yt-paper-listbox ytd-menu-service-item-renderer, ytd-menu-popup-renderer ytd-menu-service-item-renderer, tp-yt-iron-dropdown ytd-menu-service-item-renderer"); let removeOption = null; for (const mi of menuItems) { const text = (mi.textContent || "").toLowerCase(); if (text.includes("remove") || text.includes("удалить") || text.includes("supprimer") || text.includes("entfernen") || text.includes("eliminar") || text.includes("rimuovi") || text.includes("kaldır") || text.includes("削除") || text.includes("삭제") || text.includes("移除") || text.includes("oʻchirish") || text.includes("жою") || text.includes("өчүрүү") || text.includes("выдаліць") || text.includes("премахване") || text.includes("xóa")) { removeOption = mi; break; } } if (removeOption) { removeOption.click(); setTimeout(() => { document.body.click(); resolve(!0); }, 100); } else { document.body.click(); console.warn('[Playlist Search] Could not find "Remove" option in menu'); resolve(!1); } } catch (err) { document.body.click(); logError("removeItemViaMenu:findOption", err); resolve(!1); } }, 350); } catch (err) { logError("removeItemViaMenu", err); resolve(!1); } }); const deleteSelectedItems = withErrorBoundary(async () => { if (state.isDeleting || 0 === state.selectedItems.size) { return; } const count = state.selectedItems.size; const confirmed = confirm(t("playlistDeleteConfirm", { count })); if (!confirmed) { return; } state.isDeleting = !0; const itemsRoot = state.itemsContainer || state.playlistPanel; if (!itemsRoot || !state.itemSelector) { state.isDeleting = !1; return; } const allItems = Array.from(itemsRoot.querySelectorAll(state.itemSelector)); const toDelete = allItems.filter((item, idx) => { const videoId = item.getAttribute("video-id") || `item-${idx}`; return state.selectedItems.has(videoId); }); let successCount = 0; let failCount = 0; for (const item of toDelete) { const result = await removeItemViaMenu(item); result ? successCount++ : failCount++; await new Promise(r => setTimeout(r, config.deleteDelay)); } state.isDeleting = !1; state.selectedItems.clear(); setTimeout(() => { collectOriginalItems(); state.deleteMode && addCheckboxesToItems(); updateDeleteBarState(); }, 500); const msg = failCount > 0 ? t("playlistDeletePartial", { success: successCount, fail: failCount }) : t("playlistDeleteSuccess", { count: successCount }); window.YouTubeUtils?.logger?.debug?.(`[Playlist Search] ${msg}`); }, "deleteSelectedItems"); const addDeleteUI = searchContainer => { if (!searchContainer || searchContainer.querySelector(".ytplus-playlist-delete-toggle")) { return; } addDeleteStyles(); const toggleBtn = document.createElement("button"); toggleBtn.type = "button"; toggleBtn.className = "ytplus-playlist-delete-toggle"; toggleBtn.setAttribute("aria-pressed", "false"); toggleBtn.setAttribute("aria-label", t("playlistDeleteMode")); toggleBtn.title = t("playlistDeleteMode"); toggleBtn.innerHTML = _createHTML('\n <svg width="18" height="18" viewBox="0 0 24 24" fill="none"\n stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <polyline points="3 6 5 6 21 6"/>\n <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>\n <line x1="10" y1="11" x2="10" y2="17"/>\n <line x1="14" y1="11" x2="14" y2="17"/>\n </svg>\n '); toggleBtn.style.cssText = "\n background: transparent;\n border: 1px solid var(--yt-spec-10-percent-layer);\n border-radius: 50%;\n width: 36px;\n height: 36px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: var(--yt-spec-text-secondary);\n transition: all 0.2s;\n vertical-align: middle;\n margin-left: 6px;\n flex-shrink: 0;\n "; toggleBtn.addEventListener("click", toggleDeleteMode); const inputWrapper = document.createElement("div"); inputWrapper.style.cssText = "display:flex;align-items:center;gap:6px;"; const searchInput = searchContainer.querySelector(".ytplus-playlist-search-input"); if (searchInput) { searchInput.style.width = ""; searchInput.style.flex = "1"; searchInput.parentNode.insertBefore(inputWrapper, searchInput); inputWrapper.appendChild(searchInput); inputWrapper.appendChild(toggleBtn); } const deleteBar = document.createElement("div"); deleteBar.className = "ytplus-playlist-delete-bar"; deleteBar.style.cssText = "\n display: none;\n padding: 6px 0 0;\n gap: 8px;\n align-items: center;\n flex-wrap: wrap;\n "; deleteBar.style.display = "none"; const countSpan = document.createElement("span"); countSpan.className = "ytplus-playlist-selected-count"; countSpan.style.cssText = "\n font-size: 12px;\n color: var(--yt-spec-text-secondary);\n margin-right: auto;\n "; countSpan.textContent = t("playlistSelectedCount", { count: 0 }); const createBtn = (label, cls, onClick) => { const btn = document.createElement("button"); btn.type = "button"; btn.textContent = label; btn.className = cls; btn.style.cssText = "\n padding: 5px 12px;\n border-radius: 16px;\n border: 1px solid var(--yt-spec-10-percent-layer);\n cursor: pointer;\n font-size: 12px;\n font-weight: 500;\n background: var(--yt-spec-badge-chip-background);\n color: var(--yt-spec-text-primary);\n transition: all 0.2s;\n "; btn.addEventListener("click", onClick); return btn; }; const selectAllBtn = createBtn(t("selectAll"), "ytplus-playlist-select-all", selectAllItems); const clearAllBtn = createBtn(t("clearAll"), "ytplus-playlist-clear-all", clearAllItems); const deleteBtn = createBtn(t("deleteSelected"), "ytplus-playlist-delete-selected", deleteSelectedItems); deleteBtn.disabled = !0; deleteBtn.style.opacity = "0.5"; deleteBtn.style.background = "rgba(255,99,71,.12)"; deleteBtn.style.borderColor = "rgba(255,99,71,.25)"; deleteBtn.style.color = "#ff5c5c"; deleteBar.append(countSpan, selectAllBtn, clearAllBtn, deleteBtn); searchContainer.appendChild(deleteBar); }; const addDeleteStyles = () => { if (document.getElementById("ytplus-playlist-delete-styles")) { return; } const css = "\n .ytplus-playlist-delete-toggle.active {\n color: #ff5c5c !important;\n border-color: rgba(255,99,71,.4) !important;\n background: rgba(255,99,71,.1) !important;\n }\n .ytplus-playlist-delete-toggle:hover {\n color: var(--yt-spec-text-primary);\n border-color: var(--yt-spec-text-secondary);\n }\n .ytplus-playlist-delete-bar {\n display: flex;\n }\n .ytplus-playlist-delete-selected:not(:disabled):hover {\n background: rgba(255,99,71,.22) !important;\n }\n .ytplus-playlist-select-all:hover,\n .ytplus-playlist-clear-all:hover {\n background: var(--yt-spec-10-percent-layer) !important;\n }\n .ytplus-playlist-item-checkbox {\n opacity: 0.85;\n transition: opacity 0.15s;\n }\n .ytplus-playlist-item-checkbox:hover {\n opacity: 1;\n }\n /* Checkbox base styles inherited from basic.js .ytp-plus-settings-checkbox — no need to duplicate */\n "; try { if (window.YouTubeUtils?.StyleManager) { window.YouTubeUtils.StyleManager.add("ytplus-playlist-delete-styles", css); return; } } catch {} const style = document.createElement("style"); style.id = "ytplus-playlist-delete-styles"; style.textContent = css; (document.head || document.documentElement).appendChild(style); }; const cleanup = () => { if (state.deleteMode) { removeCheckboxesFromItems(); state.deleteMode = !1; } state.isDeleting = !1; state.selectedItems.clear(); const searchUI = qs(".ytplus-playlist-search"); searchUI && searchUI.remove(); if (state.mutationObserver) { state.mutationObserver.disconnect(); state.mutationObserver = null; } if (state.rafId) { cancelAnimationFrame(state.rafId); state.rafId = null; } state.itemsCache.clear(); state.searchInput = null; state.originalItems = []; state.currentPlaylistId = null; state.itemsContainer = null; state.itemSelector = null; state.itemTagName = null; state.playlistPanel = null; state.isPlaylistPage = !1; }; const handleNavigation = debounce(() => { if (!featureEnabled) { cleanup(); return; } if (!shouldRunOnThisPage()) { cleanup(); return; } const newPlaylistId = getCurrentPlaylistId(); if (newPlaylistId !== state.currentPlaylistId || !qs(".ytplus-playlist-search")) { cleanup(); newPlaylistId && setTimeout(addSearchUI, 300); } }, 250); let initialized = !1; const ensureInit = () => { if (initialized || !featureEnabled || !isRelevantRoute()) { return; } initialized = !0; const run = () => { (() => { try { const globalSettings = localStorage.getItem(window.YouTubeUtils?.SETTINGS_KEY || "youtube_plus_settings"); if (globalSettings) { const parsedGlobal = JSON.parse(globalSettings); "boolean" == typeof parsedGlobal.enablePlaylistSearch && (config.enabled = parsedGlobal.enablePlaylistSearch); } const saved = localStorage.getItem(config.storageKey); if (saved) { const parsed = JSON.parse(saved); window.YouTubeUtils && window.YouTubeUtils.safeMerge ? window.YouTubeUtils.safeMerge(config, parsed) : "boolean" == typeof parsed.enabled && (config.enabled = parsed.enabled); } } catch (error) { console.warn("[Playlist Search] Failed to load settings:", error); } })(); featureEnabled && !1 !== config.enabled && addSearchUI(); }; "function" == typeof requestIdleCallback ? requestIdleCallback(run, { timeout: 1500 }) : setTimeout(run, 0); }; const handleNavigate = () => { if (isRelevantRoute()) { ensureInit(); handleNavigation(); } else { cleanup(); } }; (cb => { "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", cb, { once: !0 }) : cb(); })(ensureInit); if (window.YouTubeUtils?.cleanupManager?.registerListener) { YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", handleNavigate, { passive: !0 }); YouTubeUtils.cleanupManager.registerListener(window, "beforeunload", cleanup, { passive: !0 }); } else { document.addEventListener("yt-navigate-finish", handleNavigate); window.addEventListener("beforeunload", cleanup); } window.addEventListener("youtube-plus-settings-updated", e => { try { const nextEnabled = !1 !== e?.detail?.enablePlaylistSearch; if (nextEnabled === featureEnabled) { return; } setFeatureEnabled(nextEnabled); } catch { setFeatureEnabled(loadFeatureEnabled()); } }); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const qs = sel => window.YouTubeUtils?.$(sel) || document.querySelector(sel); const qsAll = sel => window.YouTubeUtils?.$$(sel) || Array.from(document.querySelectorAll(sel)); const t = window.YouTubeUtils?.t || (key => key || ""); window; function loadEnableThumbnail() { return window.YouTubeUtils?.loadFeatureEnabled?.("enableThumbnail") ?? !0; } let thumbnailFeatureEnabled = loadEnableThumbnail(); const isEnabled = () => thumbnailFeatureEnabled; let started = !1; let startScheduled = !1; let mutationObserver = null; let urlChangeCleanup = null; let thumbnailStylesInjected = !1; function parseAndValidateUrl(url) { try { const parsedUrl = new URL(url); return (function hasValidProtocol(parsedUrl) { if ("https:" !== parsedUrl.protocol) { console.warn("[YouTube+][Thumbnail]", "Only HTTPS URLs are allowed"); return !1; } return !0; })(parsedUrl) && (function hasValidDomain(parsedUrl) { const {hostname} = parsedUrl; if (!hostname.endsWith("ytimg.com") && !hostname.endsWith("youtube.com")) { console.warn("[YouTube+][Thumbnail]", "Only YouTube image domains are allowed"); return !1; } return !0; })(parsedUrl) ? parsedUrl : null; } catch (error) { console.error("[YouTube+][Thumbnail]", "Invalid URL:", error); return null; } } function cleanupImageElement(img) { try { img.onload = null; img.onerror = null; img.src = ""; img.parentNode && img.parentNode.removeChild(img); } catch {} } async function checkImageExists(url) { try { if (!(function isValidUrlString(url) { if (!url || "string" != typeof url) { console.warn("[YouTube+][Thumbnail]", "Invalid URL provided"); return !1; } return !0; })(url)) { return !1; } const parsedUrl = parseAndValidateUrl(url); if (!parsedUrl) { return !1; } const headResult = await (async function checkViaHeadRequest(url) { const controller = new AbortController; const timeoutId = setTimeout(() => controller.abort(), 5e3); try { const response = await fetch(url, { method: "HEAD", signal: controller.signal }).catch(() => null); clearTimeout(timeoutId); return !response || response.ok; } catch { clearTimeout(timeoutId); return null; } })(url); return null !== headResult ? headResult : await (function checkViaImageLoad(url) { return new Promise(resolve => { const img = document.createElement("img"); img.style.display = "none"; const timeout = setTimeout(() => { cleanupImageElement(img); resolve(!1); }, 3e3); window.YouTubeUtils?.cleanupManager?.registerTimeout && window.YouTubeUtils.cleanupManager.registerTimeout(timeout); img.onload = () => { clearTimeout(timeout); cleanupImageElement(img); resolve(!0); }; img.onerror = () => { clearTimeout(timeout); cleanupImageElement(img); resolve(!1); }; document.body.appendChild(img); img.src = url; }); })(url); } catch (error) { console.error("[YouTube+][Thumbnail]", "Error checking image:", error); return !1; } } function replaceWithSpinner(overlayElement, originalSvg) { const spinner = (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"; return spinner; })(); overlayElement.replaceChild(spinner, originalSvg); return spinner; } async function openThumbnail(videoId, isShorts, overlayElement) { try { if (!(function isValidVideoId(videoId) { return videoId && "string" == typeof videoId && /^[a-zA-Z0-9_-]{11}$/.test(videoId); })(videoId)) { console.error("[YouTube+][Thumbnail]", "Invalid video ID:", videoId); return; } if (!(function isValidOverlayElement(overlayElement) { return overlayElement && overlayElement instanceof HTMLElement; })(overlayElement)) { console.error("[YouTube+][Thumbnail]", "Invalid overlay element"); return; } const originalSvg = overlayElement.querySelector("svg"); if (!originalSvg) { console.warn("[YouTube+][Thumbnail]", "No SVG found in overlay element"); return; } const spinner = replaceWithSpinner(overlayElement, originalSvg); try { await (async function loadAndShowThumbnail(videoId, isShorts) { const urls = isShorts ? (function getShortsThumbnailUrls(videoId) { return { primary: `https://i.ytimg.com/vi/${videoId}/oardefault.jpg`, fallback: `https://i.ytimg.com/vi/${videoId}/oar2.jpg` }; })(videoId) : (function getVideoThumbnailUrls(videoId) { return { primary: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`, fallback: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg` }; })(videoId); const isPrimaryAvailable = await checkImageExists(urls.primary); showImageModal(isPrimaryAvailable ? urls.primary : urls.fallback); })(videoId, isShorts); } finally { !(function restoreOriginalSvg(overlayElement, spinner, originalSvg) { try { spinner && spinner.parentNode && overlayElement.replaceChild(originalSvg, spinner); } catch (restoreError) { console.error("[YouTube+][Thumbnail]", "Error restoring original SVG:", restoreError); spinner && spinner.parentNode && spinner.parentNode.removeChild(spinner); } })(overlayElement, spinner, originalSvg); } } catch (error) { console.error("[YouTube+][Thumbnail]", "Error opening thumbnail:", error); } } function createDownloadButton(img) { const downloadBtn = document.createElement("button"); downloadBtn.className = "thumbnail-modal-download thumbnail-modal-action-btn"; downloadBtn.innerHTML = _createHTML('\n <svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>\n <polyline points="7 10 12 15 17 10"/>\n <line x1="12" y1="15" x2="12" y2="3"/>\n </svg>\n '); downloadBtn.title = t("download"); downloadBtn.setAttribute("aria-label", t("download")); downloadBtn.addEventListener("click", async e => { e.preventDefault(); e.stopPropagation(); try { await (async function downloadImageAsBlob(imgSrc) { const response = await fetch(imgSrc); if (!response.ok) { throw new Error("Network response was not ok"); } const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; try { const urlObj = new URL(imgSrc); const segments = urlObj.pathname.split("/"); a.download = segments[segments.length - 1] || "thumbnail.jpg"; } catch { a.download = "thumbnail.jpg"; } document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(blobUrl), 1500); })(img.src); } catch { window.open(img.src, "_blank"); } }); return downloadBtn; } function showImageModal(url) { try { if (!isEnabled()) { return; } if (!(function validateModalUrl(url) { if (!url || "string" != typeof url) { console.error("[YouTube+][Thumbnail]", "Invalid URL provided to modal"); return !1; } try { const parsedUrl = new URL(url); if ("https:" !== parsedUrl.protocol) { console.error("[YouTube+][Thumbnail]", "Only HTTPS URLs are allowed"); return !1; } const allowedDomains = [ "ytimg.com", "youtube.com", "ggpht.com", "googleusercontent.com" ]; if (!allowedDomains.some(d => parsedUrl.hostname.endsWith(d))) { console.error("[YouTube+][Thumbnail]", "Image domain not allowed:", parsedUrl.hostname); return !1; } return !0; } catch (urlError) { console.error("[YouTube+][Thumbnail]", "Invalid URL format:", urlError); return !1; } })(url)) { return; } qsAll(".thumbnail-modal-overlay").forEach(m => m.remove()); const overlay = document.createElement("div"); overlay.className = "thumbnail-modal-overlay"; overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-label", "Thumbnail preview"); const content = document.createElement("div"); content.className = "thumbnail-modal-content"; const img = (function createModalImage(url) { const img = document.createElement("img"); img.className = "thumbnail-modal-img"; img.src = url; img.alt = t("thumbnailPreview"); img.title = ""; img.style.cursor = "pointer"; img.addEventListener("click", () => window.open(img.src, "_blank")); return img; })(url); const optionsDiv = document.createElement("div"); optionsDiv.className = "thumbnail-modal-options"; const closeBtn = (function createCloseButton(overlay) { const closeBtn = document.createElement("button"); closeBtn.className = "thumbnail-modal-close thumbnail-modal-action-btn"; closeBtn.innerHTML = _createHTML('\n <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\n </svg>\n '); closeBtn.title = t("close"); closeBtn.setAttribute("aria-label", t("close")); closeBtn.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); overlay.remove(); }); return closeBtn; })(overlay); const newTabBtn = (function createNewTabButton(img) { const newTabBtn = document.createElement("button"); newTabBtn.className = "thumbnail-modal-open thumbnail-modal-action-btn"; newTabBtn.innerHTML = _createHTML('\n <svg fill="currentColor" viewBox="0 0 24 24" width="18" height="18" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\n <g id="SVGRepo_iconCarrier"><path d="M14.293,9.707a1,1,0,0,1,0-1.414L18.586,4H16a1,1,0,0,1,0-2h5a1,1,0,0,1,1,1V8a1,1,0,0,1-2,0V5.414L15.707,9.707a1,1,0,0,1-1.414,0ZM3,22H8a1,1,0,0,0,0-2H5.414l4.293-4.293a1,1,0,0,0-1.414-1.414L4,18.586V16a1,1,0,0,0-2,0v5A1,1,0,0,0,3,22Z"></path></g>\n </svg>\n '); newTabBtn.title = t("clickToOpen"); newTabBtn.setAttribute("aria-label", t("clickToOpen")); newTabBtn.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); window.open(img.src, "_blank"); }); return newTabBtn; })(img); const downloadBtn = createDownloadButton(img); content.appendChild(img); content.appendChild(optionsDiv); const wrapper = document.createElement("div"); wrapper.className = "thumbnail-modal-wrapper"; const actionsDiv = document.createElement("div"); actionsDiv.className = "thumbnail-modal-actions"; actionsDiv.appendChild(closeBtn); actionsDiv.appendChild(newTabBtn); actionsDiv.appendChild(downloadBtn); wrapper.appendChild(content); wrapper.appendChild(actionsDiv); overlay.appendChild(wrapper); overlay.addEventListener("click", ({target}) => { target === overlay && overlay.remove(); }); !(function setupModalKeyboard(overlay) { window.addEventListener("keydown", function escHandler(e) { if ("Escape" === e.key) { overlay.remove(); window.removeEventListener("keydown", escHandler, !0); } }, !0); })(overlay); !(function setupImageErrorHandler(img, content) { img.addEventListener("error", () => { const err = document.createElement("div"); err.textContent = t("thumbnailLoadFailed"); err.style.color = "white"; content.appendChild(err); }); })(img, content); document.body.appendChild(overlay); requestAnimationFrame(() => { const focusTarget = overlay.querySelector('button, [tabindex="0"]'); focusTarget && focusTarget.focus(); }); if (window.YouTubePlusModalHandlers && window.YouTubePlusModalHandlers.createFocusTrap) { const removeTrap = window.YouTubePlusModalHandlers.createFocusTrap(overlay); const obs = new MutationObserver(() => { if (!overlay.isConnected) { removeTrap(); obs.disconnect(); } }); obs.observe(document.body, { childList: !0 }); } } catch (error) { console.error("[YouTube+][Thumbnail]", "Error showing modal:", error); } } let thumbnailPreviewCurrentVideoId = ""; let thumbnailPreviewClosed = !1; let thumbnailInsertionAttempts = 0; function attemptInsertion() { const player = (function findPlayerElement() { return qs("#movie_player") || qs("ytd-player"); })(); if (!player) { thumbnailInsertionAttempts++; thumbnailInsertionAttempts < 10 ? setTimeout(attemptInsertion, 500) : thumbnailInsertionAttempts = 0; return; } let overlay = player.querySelector("#thumbnailPreview-player-overlay"); if (!overlay) { overlay = (function createPlayerThumbnailOverlay(videoId, player) { const overlay = createThumbnailOverlay(videoId, player); overlay.id = "thumbnailPreview-player-overlay"; overlay.dataset.videoId = videoId; overlay.style.cssText = "\n position: absolute;\n top: 10%;\n right: 8px;\n width: 36px;\n height: 36px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n cursor: pointer;\n z-index: 1001;\n transition: all 0.15s ease;\n opacity: 0;\n "; return overlay; })(thumbnailPreviewCurrentVideoId, player); overlay.tabIndex = 0; overlay.setAttribute("role", "button"); overlay.setAttribute("aria-label", "Show thumbnail preview"); overlay.onmouseenter = () => { try { overlay.style.opacity = "0.5"; } catch {} }; overlay.onmouseleave = () => { try { overlay.style.opacity = "0"; } catch {} }; overlay.onfocus = () => { try { overlay.style.opacity = "0.5"; } catch {} }; overlay.onblur = () => { try { overlay.style.opacity = "0"; } catch {} }; overlay.addEventListener("keydown", e => { const ke = e; if (ke && ("Enter" === ke.key || " " === ke.key)) { ke.preventDefault(); overlay.click(); } }); const playerAny = player; "static" === getComputedStyle(playerAny).position && (playerAny.style.position = "relative"); playerAny.appendChild(overlay); return; } if (overlay.dataset.videoId !== thumbnailPreviewCurrentVideoId) { overlay.remove(); attemptInsertion(); } thumbnailInsertionAttempts = 0; } function addOrUpdateThumbnailImage() { if (!isEnabled()) { return; } if (!(function isWatchPage() { const url = new URL(window.location.href); return "/watch" === url.pathname && url.searchParams.has("v"); })()) { return; } const newVideoId = (function getCurrentVideoId() { return new URLSearchParams(window.location.search).get("v"); })(); if (newVideoId !== thumbnailPreviewCurrentVideoId) { thumbnailPreviewClosed = !1; !(function removeOldOverlay() { const oldOverlay = qs("#thumbnailPreview-player-overlay"); oldOverlay && oldOverlay.remove(); })(); } if (!(function shouldSkipThumbnailUpdate(newVideoId) { return !newVideoId || newVideoId === thumbnailPreviewCurrentVideoId || thumbnailPreviewClosed; })(newVideoId)) { thumbnailPreviewCurrentVideoId = newVideoId; attemptInsertion(); } } function createThumbnailOverlay(videoId, container) { const overlay = document.createElement("div"); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "16"); svg.setAttribute("height", "16"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("fill", "none"); svg.setAttribute("stroke", "white"); svg.setAttribute("stroke-width", "2"); svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round"); svg.style.transition = "stroke 0.2s ease"; const mainRect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); mainRect.setAttribute("width", "18"); mainRect.setAttribute("height", "18"); mainRect.setAttribute("x", "3"); mainRect.setAttribute("y", "3"); mainRect.setAttribute("rx", "2"); mainRect.setAttribute("ry", "2"); svg.appendChild(mainRect); const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", "9"); circle.setAttribute("cy", "9"); circle.setAttribute("r", "2"); svg.appendChild(circle); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"); svg.appendChild(path); overlay.appendChild(svg); overlay.style.cssText = "\n position: absolute;\n bottom: 8px;\n left: 8px;\n background: rgba(0, 0, 0, 0.3);\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n cursor: pointer;\n z-index: 1000;\n opacity: 0;\n transition: all 0.2s ease;\n "; overlay.onmouseenter = () => { overlay.style.background = "rgba(0, 0, 0, 0.7)"; }; overlay.onmouseleave = () => { overlay.style.background = "rgba(0, 0, 0, 0.3)"; }; overlay.onclick = async e => { e.preventDefault(); e.stopPropagation(); const isShorts = container.closest("ytm-shorts-lockup-view-model") || container.closest(".shortsLockupViewModelHost") || container.closest('[class*="shortsLockupViewModelHost"]') || container.querySelector('a[href*="/shorts/"]'); await openThumbnail(videoId, !!isShorts, overlay); }; return overlay; } function extractVideoInfo(container) { const img = container.querySelector('img[src*="ytimg.com"]'); if (!img?.src) { return { videoId: null, thumbnailContainer: null }; } const videoId = (function extractVideoId(thumbnailSrc) { try { if (!thumbnailSrc || "string" != typeof thumbnailSrc) { return null; } const match = thumbnailSrc.match(/\/vi\/([^\/]+)\//); const videoId = match ? match[1] : null; if (videoId && !/^[a-zA-Z0-9_-]{11}$/.test(videoId)) { console.warn("[YouTube+][Thumbnail]", "Invalid video ID format:", videoId); return null; } return videoId; } catch (error) { console.error("[YouTube+][Thumbnail]", "Error extracting video ID:", error); return null; } })(img.src); const thumbnailContainer = (function findThumbnailContainerFromImage(img) { return img.closest("yt-thumbnail-view-model") || img.parentElement; })(img); return { videoId, thumbnailContainer }; } function extractShortsInfo(container) { const link = container.querySelector('a[href*="/shorts/"]'); if (!link?.href) { return { videoId: null, thumbnailContainer: null }; } const videoId = (function extractShortsId(href) { try { if (!href || "string" != typeof href) { return null; } const match = href.match(/\/shorts\/([^\/\?]+)/); const shortsId = match ? match[1] : null; if (shortsId && !/^[a-zA-Z0-9_-]{11}$/.test(shortsId)) { console.warn("[YouTube+][Thumbnail]", "Invalid shorts ID format:", shortsId); return null; } return shortsId; } catch (error) { console.error("[YouTube+][Thumbnail]", "Error extracting shorts ID:", error); return null; } })(link.href); const shortsImg = container.querySelector('img[src*="ytimg.com"]'); const thumbnailContainer = (function findShortsThumbnailContainer(shortsImg) { return shortsImg ? shortsImg.closest(".ytCoreImageHost") || shortsImg.closest('[class*="ThumbnailContainer"]') || shortsImg.closest('[class*="ImageHost"]') || shortsImg.parentElement : null; })(shortsImg); return { videoId, thumbnailContainer }; } function addThumbnailOverlay(container) { if (!isEnabled()) { return; } if (container.querySelector(".thumb-overlay")) { return; } let {videoId, thumbnailContainer} = extractVideoInfo(container); videoId || ({videoId, thumbnailContainer} = extractShortsInfo(container)); if (!videoId || !thumbnailContainer) { return; } !(function ensureRelativePosition(thumbnailContainer) { "static" === getComputedStyle(thumbnailContainer).position && (thumbnailContainer.style.position = "relative"); })(thumbnailContainer); const overlay = createThumbnailOverlay(videoId, container); overlay.className = "thumb-overlay"; thumbnailContainer.appendChild(overlay); !(function setupOverlayHoverEffects(thumbnailContainer, overlay) { thumbnailContainer.onmouseenter = () => { overlay.style.opacity = "1"; }; thumbnailContainer.onmouseleave = () => { overlay.style.opacity = "0"; }; })(thumbnailContainer, overlay); } function addAvatarOverlay(img) { if (!isEnabled()) { return; } const container = img.parentElement; if (!container) { return; } if (img.closest(".avatar-btn, #avatar-btn") || container.closest(".avatar-btn, #avatar-btn") || img.closest("button") || container.closest("button") || img.closest(".thumbnail-modal-wrapper") || container.closest(".thumbnail-modal-wrapper")) { return; } if (img.closest("ytm-shorts-lockup-view-model") || container.closest("ytm-shorts-lockup-view-model") || img.closest(".shortsLockupViewModelHost") || container.closest(".shortsLockupViewModelHost") || img.closest('[class*="shortsLockupViewModelHost"]') || container.closest('[class*="shortsLockupViewModelHost"]') || img.closest('[class*="shorts"]') || container.closest('[class*="shorts"]')) { return; } if (container.querySelector(".avatar-overlay")) { return; } "static" === getComputedStyle(container).position && (container.style.position = "relative"); const overlay = (function createAvatarOverlay() { const overlay = document.createElement("div"); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "16"); svg.setAttribute("height", "16"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("fill", "none"); svg.setAttribute("stroke", "white"); svg.setAttribute("stroke-width", "2"); svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round"); svg.style.transition = "stroke 0.2s ease"; const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", "12"); circle.setAttribute("cy", "8"); circle.setAttribute("r", "5"); svg.appendChild(circle); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", "M20 21a8 8 0 0 0-16 0"); svg.appendChild(path); overlay.appendChild(svg); overlay.style.cssText = "\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background: rgba(0, 0, 0, 0.7);\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n cursor: pointer;\n z-index: 1000;\n opacity: 0;\n transition: all 0.2s ease;\n "; overlay.onmouseenter = () => { overlay.style.background = "rgba(0, 0, 0, 0.9)"; }; overlay.onmouseleave = () => { overlay.style.background = "rgba(0, 0, 0, 0.7)"; }; return overlay; })(); 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 addBannerOverlay(img) { if (!isEnabled()) { return; } const container = img.parentElement; if (container.querySelector(".banner-overlay")) { return; } "static" === getComputedStyle(container).position && (container.style.position = "relative"); const overlay = (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 = "\n position: absolute;\n bottom: 8px;\n left: 8px;\n background: rgba(0, 0, 0, 0.7);\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n cursor: pointer;\n z-index: 1000;\n opacity: 0;\n transition: all 0.2s ease;\n "; overlay.onmouseenter = () => { overlay.style.background = "rgba(0, 0, 0, 0.9)"; }; overlay.onmouseleave = () => { overlay.style.background = "rgba(0, 0, 0, 0.7)"; }; return overlay; })(); 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 processAll() { if (isEnabled()) { !(function processThumbnails() { const n1 = qsAll("yt-thumbnail-view-model"); for (let i = 0; i < n1.length; i++) { addThumbnailOverlay(n1[i]); } const n2 = qsAll(".ytd-thumbnail"); for (let i = 0; i < n2.length; i++) { addThumbnailOverlay(n2[i]); } const n3 = qsAll("ytm-shorts-lockup-view-model"); for (let i = 0; i < n3.length; i++) { addThumbnailOverlay(n3[i]); } const n4 = qsAll(".shortsLockupViewModelHost"); for (let i = 0; i < n4.length; i++) { addThumbnailOverlay(n4[i]); } const n5 = qsAll('[class*="shortsLockupViewModelHost"]'); for (let i = 0; i < n5.length; i++) { addThumbnailOverlay(n5[i]); } })(); !(function processAvatars() { [ "yt-avatar-shape img", "#avatar img", "ytd-channel-avatar-editor img", '.ytd-video-owner-renderer img[src*="yt"]', 'img[src*="yt3.ggpht.com"]', 'img[src*="yt4.ggpht.com"]' ].forEach(selector => { qsAll(selector).forEach(img => { if (!img.src) { return; } if (!img.src.includes("yt")) { return; } if (img.closest(".avatar-overlay")) { return; } const isAvatar = img.naturalWidth > 0 && img.naturalWidth === img.naturalHeight; (isAvatar || img.src.includes("ggpht.com")) && addAvatarOverlay(img); }); }); })(); !(function processBanners() { [ "yt-image-banner-view-model img", 'ytd-c4-tabbed-header-renderer img[src*="yt"]', '#channel-header img[src*="banner"]', 'img[src*="banner"]' ].forEach(selector => { qsAll(selector).forEach(img => { if (!img.src) { return; } if (img.closest(".banner-overlay")) { return; } const isBanner = (img.src.includes("banner") || img.src.includes("yt")) && img.naturalWidth > 2 * img.naturalHeight; (isBanner || img.src.includes("banner")) && addBannerOverlay(img); }); }); })(); addOrUpdateThumbnailImage(); } } let processAllTimerId = null; let lastProcessAllTime = 0; function scheduleProcessAll(minDelay = 0) { if (processAllTimerId) { return; } const now = Date.now(); const dueIn = Math.max(minDelay, Math.max(0, 350 - (now - lastProcessAllTime))); processAllTimerId = setTimeout(() => { processAllTimerId = null; lastProcessAllTime = Date.now(); try { if (!isEnabled()) { return; } processAll(); } catch (e) { console.error("[YouTube+][Thumbnail]", "processAll failed:", e); } }, dueIn); } function stop() { if (started) { started = !1; try { if (processAllTimerId) { clearTimeout(processAllTimerId); processAllTimerId = null; } } catch {} !(function teardownMutationObserver() { if (mutationObserver) { try { mutationObserver.disconnect(); } catch {} mutationObserver = null; } })(); if (urlChangeCleanup) { try { urlChangeCleanup(); } catch {} urlChangeCleanup = null; } !(function removeInjectedUi() { try { qsAll(".thumbnail-modal-overlay").forEach(m => m.remove()); } catch {} try { qsAll(".thumb-overlay, .avatar-overlay, .banner-overlay").forEach(el => el.remove()); } catch {} try { const playerOverlay = qs("#thumbnailPreview-player-overlay"); playerOverlay && playerOverlay.remove(); } catch {} })(); !(function removeThumbnailStyles() { try { window.YouTubeUtils?.StyleManager?.remove && window.YouTubeUtils.StyleManager.remove("thumbnail-viewer-styles"); } catch {} const el = document.getElementById("ytplus-thumbnail-styles"); if (el) { try { el.remove(); } catch {} } thumbnailStylesInjected = !1; })(); } } function start() { if (!started && isEnabled()) { started = !0; !(function ensureThumbnailStyles() { if (!thumbnailStylesInjected) { try { const css = "\n :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); }\n 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); }\n html:not([dark]) { --thumbnail-btn-bg: var(--thumbnail-btn-bg-light); --thumbnail-btn-bg: var(--thumbnail-btn-bg-light); --thumbnail-btn-hover-bg: var(--thumbnail-btn-hover-bg-light); --thumbnail-btn-color: var(--thumbnail-btn-color-light); --thumbnail-modal-bg: var(--thumbnail-modal-bg-light); --thumbnail-modal-title: var(--thumbnail-modal-title-light); --thumbnail-modal-btn-bg: var(--thumbnail-modal-btn-bg-light); --thumbnail-modal-btn-hover-bg: var(--thumbnail-modal-btn-hover-bg-light); --thumbnail-modal-btn-color: var(--thumbnail-modal-btn-color-light); --thumbnail-modal-btn-hover-color: var(--thumbnail-modal-btn-hover-color-light); }\n .thumbnail-overlay-container { position: absolute; bottom: 8px; left: 8px; z-index: 9999; opacity: 0; transition: opacity 0.2s ease; }\n .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); }\n .thumbnail-overlay-button:hover { background: var(--thumbnail-btn-hover-bg); }\n .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); }\n .thumbnail-dropdown.show { display: flex !important; }\n .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; }\n .thumbnail-dropdown-item:hover { background: rgba(255,255,255,0.06); }\n .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); }\n .thumbnailPreview-container { position: relative; }\n .thumbnailPreview-container:hover .thumbnailPreview-button { opacity: 1; }\n .thumbnail-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.55); z-index: 100000; display: flex; align-items: center; justify-content: center; animation: fadeInModal 0.22s cubic-bezier(.4,0,.2,1); backdrop-filter: blur(8px) saturate(140%); -webkit-backdrop-filter: blur(8px) saturate(140%); }\n .thumbnail-modal-content { background: var(--thumbnail-modal-bg); border-radius: 20px; box-shadow: 0 12px 40px rgba(0,0,0,0.45); max-width: 78vw; max-height: 90vh; overflow: auto; position: relative; display: flex; flex-direction: column; align-items: center; animation: scaleInModal 0.22s cubic-bezier(.4,0,.2,1); border: 1.5px solid var(--thumbnail-glass-border); backdrop-filter: blur(14px) saturate(150%); -webkit-backdrop-filter: blur(14px) saturate(150%);}\n /* Wrapper to place content and action buttons side-by-side */\n .thumbnail-modal-wrapper { display: flex; align-items: flex-start; gap: 12px; }\n .thumbnail-modal-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }\n .thumbnail-modal-action-btn { width: 40px; height: 40px; border-radius: 50%; background: var(--thumbnail-modal-btn-bg); border: 1px solid rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,0.2); transition: transform 0.12s ease, background 0.12s ease; color: var(--thumbnail-modal-btn-color); }\n .thumbnail-modal-action-btn:hover { transform: translateY(-2px); }\n .thumbnail-modal-close { }\n .thumbnail-modal-open { }\n .thumbnail-modal-img { max-width: 72vw; max-height: 70vh; box-shadow: var(--thumbnail-glass-shadow); background: #222; border: 1px solid var(--thumbnail-glass-border); }\n .thumbnail-modal-options { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }\n .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); }\n .thumbnail-modal-option-btn:hover { background: var(--thumbnail-modal-btn-hover-bg); color: var(--thumbnail-modal-btn-hover-color); }\n .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); }\n /* fadeInModal, scaleInModal defined in shared-keyframes (basic.js) */\n "; if (window.YouTubeUtils && YouTubeUtils.StyleManager && "function" == typeof YouTubeUtils.StyleManager.add) { YouTubeUtils.StyleManager.add("thumbnail-viewer-styles", css); } else { const s = document.createElement("style"); s.id = "ytplus-thumbnail-styles"; s.textContent = css; (document.head || document.documentElement).appendChild(s); } thumbnailStylesInjected = !0; } catch { if (!document.getElementById("ytplus-thumbnail-styles")) { const s = document.createElement("style"); s.id = "ytplus-thumbnail-styles"; s.textContent = ".thumbnail-modal-img{max-width:72vw;max-height:70vh;}"; (document.head || document.documentElement).appendChild(s); } thumbnailStylesInjected = !0; } } })(); urlChangeCleanup || (urlChangeCleanup = (function setupUrlChangeDetection() { let currentUrl = location.href; const onNavChange = () => { setTimeout(() => { if (isEnabled() && location.href !== currentUrl) { currentUrl = location.href; scheduleProcessAll(250); } }, 100); }; const ytNavigateHandler = () => { if (isEnabled()) { location.href !== currentUrl && (currentUrl = location.href); scheduleProcessAll(120); } }; window.addEventListener("ytp-history-navigate", onNavChange); window.addEventListener("popstate", onNavChange); window.addEventListener("yt-navigate-finish", ytNavigateHandler); return () => { try { window.removeEventListener("ytp-history-navigate", onNavChange); window.removeEventListener("popstate", onNavChange); window.removeEventListener("yt-navigate-finish", ytNavigateHandler); } catch {} }; })()); !(function setupMutationObserver() { if (mutationObserver) { return; } mutationObserver = new MutationObserver(() => { scheduleProcessAll(120); }); const startObserving = () => { if (!mutationObserver) { return; } const target = document.querySelector("#content") || document.querySelector("#page-manager") || document.body; mutationObserver.observe(target, { childList: !0, subtree: target !== document.body }); window.YouTubeUtils?.cleanupManager?.registerObserver && window.YouTubeUtils.cleanupManager.registerObserver(mutationObserver); }; document.body ? startObserving() : document.addEventListener("DOMContentLoaded", startObserving); })(); "function" == typeof requestIdleCallback ? requestIdleCallback(() => scheduleProcessAll(0), { timeout: 2e3 }) : scheduleProcessAll(400); setTimeout(() => scheduleProcessAll(0), 900); setTimeout(() => scheduleProcessAll(0), 1800); } } function startMaybe() { if (started || startScheduled) { return; } if (!isEnabled()) { return; } startScheduled = !0; const run = () => { startScheduled = !1; start(); }; "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", () => setTimeout(run, 100), { once: !0 }) : setTimeout(run, 100); } function setEnabled(nextEnabled) { thumbnailFeatureEnabled = !1 !== nextEnabled; thumbnailFeatureEnabled ? startMaybe() : stop(); } startMaybe(); window.addEventListener("youtube-plus-settings-updated", e => { try { const enabledFromEvent = e?.detail?.enableThumbnail; setEnabled(!1 !== enabledFromEvent); } catch { setEnabled(loadEnableThumbnail()); } }); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const t = window.YouTubeUtils?.t || (key => key || ""); const config = { enabled: !0, get shortcuts() { return { seekBackward: { key: "ArrowLeft", get description() { return t("seekBackward"); } }, seekForward: { key: "ArrowRight", get description() { return t("seekForward"); } }, volumeUp: { key: "+", get description() { return t("volumeUp"); } }, volumeDown: { key: "-", get description() { return t("volumeDown"); } }, mute: { key: "m", get description() { return t("muteUnmute"); } }, toggleCaptions: { key: "c", get description() { return t("toggleCaptions"); } }, showHelp: { key: "?", get description() { return t("showHideHelp"); }, editable: !1 } }; }, storageKey: "youtube_shorts_keyboard_settings" }; const state = { helpVisible: !1, lastAction: null, actionTimeout: null, editingShortcut: null, cachedVideo: null, lastVideoCheck: 0, initialized: !1, routeObserver: null }; const getCurrentVideo = (() => { const selectors = [ "ytd-reel-video-renderer[is-active] video", "#shorts-player video", "video" ]; return () => { const now = Date.now(); if (state.cachedVideo?.isConnected && now - state.lastVideoCheck < 100) { return state.cachedVideo; } for (const selector of selectors) { const video = YouTubeUtils.querySelector(selector); if (video) { state.cachedVideo = video; state.lastVideoCheck = now; return video; } } state.cachedVideo = null; return null; }; })(); const utils = { isInShortsPage: () => location.pathname.startsWith("/shorts/"), isInputFocused: () => { const el = document.activeElement; return el?.matches?.('input, textarea, [contenteditable="true"]') || el?.isContentEditable; }, loadSettings: () => { try { const saved = localStorage.getItem(config.storageKey); if (!saved) { return; } const parsed = JSON.parse(saved); if ("object" != typeof parsed || null === parsed) { console.warn("[YouTube+][Shorts]", "Invalid settings format"); return; } "boolean" == typeof parsed.enabled && (config.enabled = parsed.enabled); if (parsed.shortcuts && "object" == typeof parsed.shortcuts) { const defaultShortcuts = utils.getDefaultShortcuts(); for (const [action, shortcut] of Object.entries(parsed.shortcuts)) { if (!defaultShortcuts[action]) { continue; } if (!shortcut || "object" != typeof shortcut) { continue; } const {key: sKey, editable: sEditable} = shortcut; "string" == typeof sKey && sKey.length > 0 && sKey.length <= 20 && (config.shortcuts[action] = { key: sKey, description: defaultShortcuts[action].description, editable: !1 !== sEditable }); } } } catch (error) { console.error("[YouTube+][Shorts]", "Error loading settings:", error); } }, saveSettings: () => { try { const settingsToSave = { enabled: config.enabled, shortcuts: config.shortcuts }; localStorage.setItem(config.storageKey, JSON.stringify(settingsToSave)); } catch (error) { console.error("[YouTube+][Shorts]", "Error saving settings:", error); } }, getDefaultShortcuts: () => ({ seekBackward: { key: "ArrowLeft", get description() { return t("seekBackward"); } }, seekForward: { key: "ArrowRight", get description() { return t("seekForward"); } }, volumeUp: { key: "+", get description() { return t("volumeUp"); } }, volumeDown: { key: "-", get description() { return t("volumeDown"); } }, mute: { key: "m", get description() { return t("muteUnmute"); } }, toggleCaptions: { key: "c", get description() { return t("toggleCaptions"); } }, showHelp: { key: "?", get description() { return t("showHideHelp"); }, editable: !1 } }) }; const feedback = (() => { let element = null; return { show: text => { state.lastAction = text; clearTimeout(state.actionTimeout); const el = (() => { if (element) { return element; } element = document.createElement("div"); element.id = "shorts-keyboard-feedback"; element.style.cssText = "\n position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);\n background:var(--shorts-feedback-bg,rgba(255,255,255,.1));\n backdrop-filter:blur(16px) saturate(150%);\n border:1px solid var(--shorts-feedback-border,rgba(255,255,255,.15));\n border-radius:20px;\n color:var(--shorts-feedback-color,#fff);\n padding:18px 32px;font-size:20px;font-weight:700;\n z-index:10000;opacity:0;visibility:hidden;pointer-events:none;\n transition:all .3s cubic-bezier(.4,0,.2,1);text-align:center;\n box-shadow:0 8px 32px rgba(0,0,0,.4);\n background: rgba(155, 155, 155, 0.15);\n border: 1px solid rgba(255,255,255,0.2);\n box-shadow: 0 8px 32px 0 rgba(31,38,135,0.37);\n backdrop-filter: blur(12px) saturate(180%);\n -webkit-backdrop-filter: blur(12px) saturate(180%);\n "; document.body.appendChild(element); return element; })(); el.textContent = text; requestAnimationFrame(() => { el.style.opacity = "1"; el.style.visibility = "visible"; el.style.transform = "translate(-50%, -50%) scale(1.05)"; }); state.actionTimeout = setTimeout(() => { el.style.opacity = "0"; el.style.visibility = "hidden"; el.style.transform = "translate(-50%, -50%) scale(0.95)"; }, 1500); } }; })(); const actions = { seekBackward: () => { const video = getCurrentVideo(); if (video) { video.currentTime = Math.max(0, video.currentTime - 5); feedback.show("-5s"); } }, seekForward: () => { const video = getCurrentVideo(); if (video) { video.currentTime = Math.min(video.duration || Infinity, video.currentTime + 5); feedback.show("+5s"); } }, toggleCaptions: () => { try { const container = document.querySelector("ytd-shorts-player-controls, ytd-reel-video-renderer, #shorts-player") || document; const buttons = container.querySelectorAll("button[aria-label]"); for (const b of buttons) { const aria = (b.getAttribute("aria-label") || "").toLowerCase(); if ((aria.includes("subtit") || aria.includes("caption") || aria.includes("субтит") || aria.includes("субтитр") || aria.includes("cc")) && null !== b.offsetParent) { b.click(); break; } } } catch {} const video = getCurrentVideo(); if (video && video.textTracks && video.textTracks.length) { const tracks = Array.from(video.textTracks).filter(tr => "subtitles" === tr.kind || "captions" === tr.kind || !tr.kind); if (tracks.length) { const anyShowing = tracks.some(tr => "showing" === tr.mode); tracks.forEach(tr => { tr.mode = anyShowing ? "hidden" : "showing"; }); feedback.show(t(anyShowing ? "captionsOff" : "captionsOn")); return; } } feedback.show(t("captionsUnavailable")); }, volumeUp: () => { const video = getCurrentVideo(); if (video) { video.volume = Math.min(1, video.volume + .1); feedback.show(`${Math.round(100 * video.volume)}%`); } }, volumeDown: () => { const video = getCurrentVideo(); if (video) { video.volume = Math.max(0, video.volume - .1); feedback.show(`${Math.round(100 * video.volume)}%`); } }, mute: () => { const video = getCurrentVideo(); try { const container = document.querySelector("ytd-shorts-player-controls, ytd-reel-video-renderer, #shorts-player") || document; const buttons = container.querySelectorAll("button[aria-label]"); for (const b of buttons) { const aria = (b.getAttribute("aria-label") || "").toLowerCase(); if ((aria.includes("mute") || aria.includes("unmute") || aria.includes("sound") || aria.includes("volume") || aria.includes("звук") || aria.includes("громк")) && null !== b.offsetParent) { b.click(); setTimeout(() => { const v = getCurrentVideo(); v && feedback.show(v.muted ? "🔇" : "🔊"); }, 60); return; } } } catch {} if (video) { video.muted = !video.muted; feedback.show(video.muted ? "🔇" : "🔊"); } }, showHelp: () => helpPanel.toggle() }; const helpPanel = (() => { let panel = null; return { show: () => { const p = (() => { 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 = _createHTML(`\n <div class="help-header">\n <h3>${t("keyboardShortcuts")}</h3>\n <button class="ytp-plus-settings-close help-close" type="button" aria-label="${t("closeButton")}">\n <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\n </svg>\n </button>\n </div>\n <div class="help-content">\n ${Object.entries(config.shortcuts).map(([action, shortcut]) => `<div class="help-item">\n <kbd data-action="${action}" ${!1 === shortcut.editable ? 'class="non-editable"' : ""}>${" " === shortcut.key ? "Space" : shortcut.key}</kbd>\n <span>${shortcut.description}</span>\n </div>`).join("")}\n </div>\n <div class="help-footer">\n <button class="ytp-plus-button ytp-plus-button-primary reset-all-shortcuts">${t("resetAll")}</button>\n </div>\n `); panel.querySelector(".help-close").onclick = () => helpPanel.hide(); panel.querySelector(".reset-all-shortcuts").onclick = () => { if (confirm(t("resetAllConfirm"))) { config.shortcuts = utils.getDefaultShortcuts(); utils.saveSettings(); feedback.show(t("shortcutsReset")); render(); } }; panel.querySelectorAll("kbd[data-action]:not(.non-editable)").forEach(kbd => { kbd.onclick = () => editShortcut(kbd.dataset.action, config.shortcuts[kbd.dataset.action].key); }); }; render(); document.body.appendChild(panel); return panel; })(); p.classList.add("visible"); state.helpVisible = !0; p.focus(); }, hide: () => { if (panel) { panel.classList.remove("visible"); state.helpVisible = !1; } }, toggle: () => state.helpVisible ? helpPanel.hide() : helpPanel.show(), refresh: () => { if (panel) { panel.remove(); panel = null; } } }; })(); const editShortcut = (actionKey, currentKey) => { const dialog = document.createElement("div"); dialog.className = "glass-modal shortcut-edit-dialog"; dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-modal", "true"); dialog.innerHTML = _createHTML(`\n <div class="glass-panel shortcut-edit-content">\n <h4>${t("editShortcut")}: ${config.shortcuts[actionKey].description}</h4>\n <p>${t("pressAnyKey")}</p>\n <div class="current-shortcut">${t("current")}: <kbd>${" " === currentKey ? "Space" : currentKey}</kbd></div>\n <button class="ytp-plus-button ytp-plus-button-primary shortcut-cancel" type="button">${t("cancel")}</button>\n </div>\n `); document.body.appendChild(dialog); state.editingShortcut = actionKey; const handleKey = e => { e.preventDefault(); e.stopPropagation(); if ("Escape" === e.key) { return cleanup(); } const conflict = Object.keys(config.shortcuts).find(key => key !== actionKey && config.shortcuts[key].key === e.key); if (conflict) { feedback.show(t("keyAlreadyUsed", { key: e.key })); } else { config.shortcuts[actionKey].key = e.key; utils.saveSettings(); feedback.show(t("shortcutUpdated")); helpPanel.refresh(); cleanup(); } }; const cleanup = () => { document.removeEventListener("keydown", handleKey, !0); dialog.remove(); state.editingShortcut = null; }; dialog.querySelector(".shortcut-cancel").onclick = cleanup; dialog.onclick = ({target}) => { target === dialog && cleanup(); }; YouTubeUtils.cleanupManager.registerListener(document, "keydown", handleKey, !0); }; const handleKeydown = e => { if (!config.enabled || !utils.isInShortsPage() || utils.isInputFocused() || state.editingShortcut) { return; } let {key} = e; "NumpadAdd" === e.code ? key = "+" : "NumpadSubtract" === e.code && (key = "-"); const action = Object.keys(config.shortcuts).find(k => config.shortcuts[k].key === key); if (action && actions[action]) { e.preventDefault(); e.stopPropagation(); actions[action](); } }; const isOnShortsPage = () => location.pathname.startsWith("/shorts/"); const init = () => { if (!isOnShortsPage()) { return; } if (state.initialized) { return; } state.initialized = !0; utils.loadSettings(); (() => { if (document.getElementById("shorts-keyboard-styles")) { return; } YouTubeUtils.StyleManager.add("shorts-keyboard-styles", '\n :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;}\n 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;}\n 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;}\n .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);}\n .shorts-help-panel.visible{opacity:1;visibility:visible;transform:translate(-50%,-50%) scale(1);}\n .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);}\n html:not([dark]) .help-header{background:rgba(0,0,0,.04);border-bottom:1px solid rgba(0,0,0,.08);}\n .help-header h3{margin:0;font-size:20px;font-weight:700;}\n .help-close{display:flex;align-items:center;justify-content:center;padding:4px;}\n .help-content{padding:18px 24px;max-height:400px;overflow-y:auto;}\n .help-item{display:flex;align-items:center;margin-bottom:14px;gap:18px;}\n .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;}\n html:not([dark]) .help-item kbd{background:rgba(0,0,0,.06);color:#222;border:1.5px solid rgba(0,0,0,.08);}\n .help-item kbd:hover{background:rgba(255,255,255,.22);transform:scale(1.07);}\n .help-item kbd:after{content:"✎";position:absolute;top:-7px;right:-7px;font-size:11px;opacity:0;transition:opacity .2s;}\n .help-item kbd:hover:after{opacity:.7;}\n .help-item kbd.non-editable{cursor:default;opacity:.7;}\n .help-item kbd.non-editable:hover{background:rgba(255,255,255,.15);transform:none;}\n .help-item kbd.non-editable:after{display:none;}\n .help-item span{font-size:15px;color:rgba(255,255,255,.92);}\n html:not([dark]) .help-item span{color:#222;}\n .help-footer{padding:16px 24px 20px;border-top:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);text-align:center;}\n html:not([dark]) .help-footer{background:rgba(0,0,0,.04);border-top:1px solid rgba(0,0,0,.08);}\n .reset-all-shortcuts{display:inline-flex;align-items:center;justify-content:center;gap:var(--yt-space-sm);}\n .shortcut-edit-dialog{z-index:10002;}\n .shortcut-edit-content{padding:28px 32px;min-width:320px;text-align:center;display:flex;flex-direction:column;gap:var(--yt-space-md);color:inherit;}\n html:not([dark]) .shortcut-edit-content{color:#222;}\n .shortcut-edit-content h4{margin:0 0 14px;font-size:17px;font-weight:700;}\n .shortcut-edit-content p{margin:0 0 18px;font-size:15px;color:rgba(255,255,255,.85);}\n html:not([dark]) .shortcut-edit-content p{color:#222;}\n .current-shortcut{margin:18px 0;font-size:15px;}\n .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);}\n html:not([dark]) .current-shortcut kbd{background:rgba(0,0,0,.06);color:#222;border:1.5px solid rgba(0,0,0,.08);}\n .shortcut-cancel{display:inline-flex;align-items:center;justify-content:center;gap:var(--yt-space-sm);}\n @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}}\n #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%);}\n 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));}\n '); })(); YouTubeUtils.cleanupManager.registerListener(document, "keydown", handleKeydown, !0); YouTubeUtils.cleanupManager.registerListener(document, "click", ({target}) => { state.helpVisible && target?.closest && !target.closest("#shorts-keyboard-help") && helpPanel.hide(); }); YouTubeUtils.cleanupManager.registerListener(document, "keydown", e => { if ("Escape" === e.key && state.helpVisible) { e.preventDefault(); helpPanel.hide(); } }); }; const observeRoute = () => { let lastPath = location.pathname; let isCurrentlyOnShorts = isOnShortsPage(); state.routeObserver = new MutationObserver(() => { const currentPath = location.pathname; if (currentPath === lastPath) { return; } lastPath = currentPath; const nowOnShorts = isOnShortsPage(); if (nowOnShorts !== isCurrentlyOnShorts) { isCurrentlyOnShorts = nowOnShorts; !nowOnShorts && state.initialized ? (() => { if (state.initialized) { state.helpVisible && helpPanel.hide(); if (state.actionTimeout) { clearTimeout(state.actionTimeout); state.actionTimeout = null; } state.cachedVideo = null; state.initialized = !1; } })() : nowOnShorts && !state.initialized && init(); } }); if (document.body) { state.routeObserver.observe(document.body, { childList: !0, subtree: !1 }); YouTubeUtils.cleanupManager?.registerObserver && YouTubeUtils.cleanupManager.registerObserver(state.routeObserver); YouTubeUtils.ObserverRegistry?.track && YouTubeUtils.ObserverRegistry.track(); } }; if ("loading" === document.readyState) { document.addEventListener("DOMContentLoaded", () => { init(); observeRoute(); }); } else { init(); observeRoute(); } isOnShortsPage() && !localStorage.getItem("shorts_keyboard_help_shown") && setTimeout(() => { if (isOnShortsPage()) { feedback.show("Press ? for shortcuts"); localStorage.setItem("shorts_keyboard_help_shown", "true"); } }, 2e3); })(); !(function() { "use strict"; if (window.__ytpVideoStatsModuleInit) { return; } const initVideoStats = () => { if (window.__ytpVideoStatsModuleInit) { return; } window.__ytpVideoStatsModuleInit = !0; const _createHTML = window._ytplusCreateHTML || (s => s); const {$, $$, byId} = window.YouTubeUtils || {}; if (window.YouTubeUtils?.isStudioPage?.()) { return; } let statsInitialized = !1; const t = window.YouTubeUtils?.t || (key => key || ""); const STATS_ICON_ID = "ytp-stats-universal-icon"; const STATS_ICON_SELECTOR = `#${STATS_ICON_ID}, .videoStats[data-ytp-stats-icon="true"], .videoStats`; let statsButtonEnabled = "false" !== localStorage.getItem("youtube_stats_button_enabled"); let previousUrl = location.href; let isChecking = !1; let experimentalNavListenerKey = null; let channelFeatures = { hasStreams: !1, hasShorts: !1 }; const rateLimiter = { requests: new Map, maxRequests: 10, maxKeys: 100, timeWindow: 6e4, canRequest: key => { const now = Date.now(); const requests = rateLimiter.requests.get(key) || []; const recentRequests = requests.filter(time => now - time < rateLimiter.timeWindow); if (recentRequests.length >= rateLimiter.maxRequests) { console.warn(`[YouTube+][Stats] Rate limit exceeded for ${key}. Max ${rateLimiter.maxRequests} requests per minute.`); return !1; } recentRequests.push(now); rateLimiter.requests.set(key, recentRequests); if (rateLimiter.requests.size > rateLimiter.maxKeys) { const firstKey = rateLimiter.requests.keys().next().value; rateLimiter.requests.delete(firstKey); } return !0; }, clear: () => { rateLimiter.requests.clear(); } }; function isValidVideoId(id) { return id && /^[a-zA-Z0-9_-]{11}$/.test(id); } function getCurrentVideoUrl() { try { const url = window.location.href; if (!url.includes("youtube.com")) { return null; } const fromParams = (function getVideoIdFromParams() { const urlParams = new URLSearchParams(window.location.search); const videoId = urlParams.get("v"); return isValidVideoId(videoId) ? `https://www.youtube.com/watch?v=${videoId}` : null; })(); return fromParams || (function getVideoIdFromShorts(url) { const shortsMatch = url.match(/\/shorts\/([^?]+)/); return shortsMatch && isValidVideoId(shortsMatch[1]) ? `https://www.youtube.com/shorts/${shortsMatch[1]}` : null; })(url); } catch (error) { YouTubeUtils?.logError?.("Stats", "Failed to get video URL", error); return null; } } function getChannelIdentifier() { try { const url = window.location.href; let identifier = ""; url.includes("/channel/") ? identifier = url.split("/channel/")[1].split("/")[0] : url.includes("/@") && (identifier = url.split("/@")[1].split("/")[0]); return identifier && /^[a-zA-Z0-9_-]+$/.test(identifier) ? identifier : ""; } catch (error) { YouTubeUtils?.logError?.("Stats", "Failed to get channel identifier", error); return ""; } } function getTabUrl(tab) { return tab?.tabRenderer?.endpoint?.commandMetadata?.webCommandMetadata?.url || null; } function tabMatches(url, pattern) { return "string" == typeof url && pattern.test(url); } function hasBothContentTypes(hasStreams, hasShorts) { return hasStreams && hasShorts; } function updateContentTypeFlags(tabUrl, flags) { !flags.hasStreams && (function isStreamsTab(tabUrl) { return tabMatches(tabUrl, /\/streams$/); })(tabUrl) && (flags.hasStreams = !0); !flags.hasShorts && (function isShortsTab(tabUrl) { return tabMatches(tabUrl, /\/shorts$/); })(tabUrl) && (flags.hasShorts = !0); } async function checkChannelTabs(url) { if (!isChecking && (function validateYouTubeUrl(url) { if (!url || "string" != typeof url) { return !1; } try { const parsedUrl = new URL(url); if ("www.youtube.com" !== parsedUrl.hostname && "youtube.com" !== parsedUrl.hostname) { console.warn("[YouTube+][Stats] Invalid domain for channel check"); return !1; } return !0; } catch (error) { YouTubeUtils?.logError?.("Stats", "Invalid URL for channel check", error); return !1; } })(url) && rateLimiter.canRequest("checkChannelTabs")) { isChecking = !0; try { const html = await (async function fetchChannelHtml(url) { try { const parsed = new URL(url); const hostname = parsed.hostname.toLowerCase(); if ("www.youtube.com" !== hostname && "youtube.com" !== hostname && "m.youtube.com" !== hostname) { console.warn("[YouTube+][Stats] Blocked fetch to non-YouTube URL:", hostname); return null; } } catch { return null; } const controller = new AbortController; const timeoutId = setTimeout(() => controller.abort(), 1e4); try { const response = await fetch(url, { credentials: "same-origin", signal: controller.signal, headers: { Accept: "text/html" } }); clearTimeout(timeoutId); if (!response.ok) { console.warn(`[YouTube+][Stats] HTTP ${response.status} when checking channel tabs`); return null; } const html = await response.text(); if (html.length > 5e6) { console.warn("[YouTube+][Stats] Response too large, skipping parse"); return null; } return html; } catch (error) { "AbortError" === error.name && console.warn("[YouTube+][Stats] Channel check timed out"); throw error; } })(url); if (!html) { isChecking = !1; return; } const data = (function extractYouTubeData(html) { const match = html.match(/var ytInitialData = (.+?);<\/script>/); if (!match || !match[1]) { return null; } try { return JSON.parse(match[1]); } catch (parseError) { YouTubeUtils?.logError?.("Stats", "Failed to parse ytInitialData", parseError); return null; } })(html); if (!data) { isChecking = !1; return; } channelFeatures = (function analyzeChannelTabs(data) { const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || []; const flags = { hasStreams: !1, hasShorts: !1 }; for (const tab of tabs) { const tabUrl = getTabUrl(tab); if (tabUrl) { updateContentTypeFlags(tabUrl, flags); if (hasBothContentTypes(flags.hasStreams, flags.hasShorts)) { break; } } } return flags; })(data); !(function refreshStatsMenu() { const existingMenu = $(".stats-menu-container"); if (existingMenu) { existingMenu.remove(); createStatsMenu(); } })(); } catch (error) { YouTubeUtils?.logError?.("Stats", "Failed to check channel tabs", error); } finally { isChecking = !1; } } } function isChannelPage(url) { try { return url && "string" == typeof url && url.includes("youtube.com/") && (url.includes("/channel/") || url.includes("/@")) && !url.includes("/video/") && !url.includes("/watch"); } catch { return !1; } } const checkUrlChange = YouTubeUtils?.debounce?.(() => { try { const currentUrl = location.href; if (currentUrl !== previousUrl) { previousUrl = currentUrl; isChannelPage(currentUrl) && setTimeout(() => checkChannelTabs(currentUrl), 500); } } catch (error) { YouTubeUtils?.logError?.("Stats", "URL change check failed", error); } }, 300) || function() { try { const currentUrl = location.href; if (currentUrl !== previousUrl) { previousUrl = currentUrl; isChannelPage(currentUrl) && setTimeout(() => checkChannelTabs(currentUrl), 500); } } catch (error) { console.error("[YouTube+][Stats] URL change check failed:", error); } }; function insertUniversalIcon() { if (!statsButtonEnabled) { return; } let masthead = $("ytd-masthead.style-scope"); masthead || (masthead = $("ytd-masthead")); if (!masthead) { return; } let endElem = $("#end.style-scope.ytd-masthead", masthead); endElem || (endElem = $("#end", masthead)); const existingIcons = Array.from(document.querySelectorAll(STATS_ICON_SELECTOR)); let statsIcon = document.getElementById(STATS_ICON_ID); !statsIcon && existingIcons.length > 0 && (statsIcon = existingIcons[0]); existingIcons.length > 1 && existingIcons.forEach(icon => { if (icon !== statsIcon) { try { icon.remove(); } catch {} } }); if (statsIcon) { statsIcon.id = STATS_ICON_ID; statsIcon.classList.add("videoStats"); statsIcon.dataset.ytpStatsIcon = "true"; } else { statsIcon = (function createStatsIcon() { const icon = document.createElement("div"); icon.className = "videoStats"; icon.id = STATS_ICON_ID; icon.dataset.ytpStatsIcon = "true"; const SVG_NS = window.YouTubePlusConstants?.SVG_NS || "http://www.w3.org/2000/svg"; const svg = document.createElementNS(SVG_NS, "svg"); svg.setAttribute("viewBox", "0 0 512 512"); const path = document.createElementNS(SVG_NS, "path"); path.setAttribute("d", "M500 89c13.8-11 16-31.2 5-45s-31.2-16-45-5L319.4 151.5 211.2 70.4c-11.7-8.8-27.8-8.5-39.2 .6L12 199c-13.8 11-16 31.2-5 45s31.2 16 45 5L192.6 136.5l108.2 81.1c11.7 8.8 27.8 8.5 39.2-.6L500 89zM160 256l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32zM32 352l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32zm288-64c-17.7 0-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32zm96-32l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32z"); svg.appendChild(path); icon.appendChild(svg); icon.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); const videoUrl = getCurrentVideoUrl(); if (videoUrl) { const urlParams = new URLSearchParams(new URL(videoUrl).search); const videoId = urlParams.get("v") || videoUrl.match(/\/shorts\/([^?]+)/)?.[1]; videoId && openStatsModal("video", videoId); } }); return icon; })(); } endElem ? statsIcon.parentNode === endElem && endElem.firstChild === statsIcon || endElem.insertBefore(statsIcon, endElem.firstChild) : statsIcon.parentNode !== masthead && masthead.appendChild(statsIcon); } function createButton(text, svgPath, viewBox, className, onClick) { const buttonViewModel = document.createElement("button-view-model"); buttonViewModel.className = `yt-spec-button-view-model ${className}-view-model`; const button = document.createElement("button"); button.className = `yt-spec-button-shape-next yt-spec-button-shape-next--outline yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--enable-backdrop-filter-experiment ${className}-button`; button.setAttribute("aria-disabled", "false"); button.setAttribute("aria-label", text); button.style.display = "flex"; button.style.alignItems = "center"; button.style.justifyContent = "center"; button.style.gap = "8px"; button.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); onClick(); }); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", viewBox); svg.style.width = "20px"; svg.style.height = "20px"; svg.style.fill = "currentColor"; const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", svgPath); svg.appendChild(path); const buttonText = document.createElement("div"); buttonText.className = `yt-spec-button-shape-next__button-text-content ${className}-text`; buttonText.textContent = text; buttonText.style.display = "flex"; buttonText.style.alignItems = "center"; const touchFeedback = document.createElement("yt-touch-feedback-shape"); touchFeedback.style.borderRadius = "inherit"; const touchFeedbackDiv = document.createElement("div"); touchFeedbackDiv.className = "yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"; touchFeedbackDiv.setAttribute("aria-hidden", "true"); const strokeDiv = document.createElement("div"); strokeDiv.className = "yt-spec-touch-feedback-shape__stroke"; const fillDiv = document.createElement("div"); fillDiv.className = "yt-spec-touch-feedback-shape__fill"; touchFeedbackDiv.appendChild(strokeDiv); touchFeedbackDiv.appendChild(fillDiv); touchFeedback.appendChild(touchFeedbackDiv); button.appendChild(svg); button.appendChild(buttonText); button.appendChild(touchFeedback); buttonViewModel.appendChild(button); return buttonViewModel; } const INNERTUBE_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; const INNERTUBE_CLIENT_VERSION_FALLBACK = "2.20250312.01.00"; function getInnerTubeClientVersion() { try { if (void 0 !== window.ytcfg && "function" == typeof window.ytcfg.get) { const version = window.ytcfg.get("INNERTUBE_CLIENT_VERSION"); if (version && "string" == typeof version) { return version; } } if (window.ytcfg?.data_?.INNERTUBE_CLIENT_VERSION) { return window.ytcfg.data_.INNERTUBE_CLIENT_VERSION; } if (window.yt?.config_?.INNERTUBE_CLIENT_VERSION) { return window.yt.config_.INNERTUBE_CLIENT_VERSION; } } catch {} return INNERTUBE_CLIENT_VERSION_FALLBACK; } function createInnerTubeRequestBody(videoId) { return { context: { client: { clientName: "WEB", clientVersion: getInnerTubeClientVersion(), hl: "en", gl: "US" } }, videoId }; } function extractThumbnailUrl(details) { const thumbnails = details.thumbnail?.thumbnails; return thumbnails?.[thumbnails.length - 1]?.url || null; } async function fetchVideoStatsInnerTube(videoId) { if (!videoId) { return null; } try { const url = `https://www.youtube.com/youtubei/v1/player?key=${INNERTUBE_API_KEY}&prettyPrint=false`; const response = await fetch(url, (function createInnerTubeFetchOptions(videoId) { return { method: "POST", headers: { "Content-Type": "application/json", "X-YouTube-Client-Name": "1", "X-YouTube-Client-Version": getInnerTubeClientVersion() }, body: JSON.stringify(createInnerTubeRequestBody(videoId)) }; })(videoId)); if (!response.ok) { console.warn("[YouTube+][Stats] InnerTube API failed:", response.status); return null; } const data = await response.json(); const stats = (function parseVideoStatsFromResponse(data) { const details = data.videoDetails || {}; const microformat = data.microformat?.playerMicroformatRenderer || {}; const ownerProfileUrl = microformat.ownerProfileUrl || microformat.ownerUrls?.[0] || ""; const handleMatch = ownerProfileUrl.match(/\/@([\w.-]+)/); const authorHandle = handleMatch ? `@${handleMatch[1]}` : null; return { videoId: details.videoId, title: details.title, author: details.author || null, authorHandle, views: details.viewCount ? parseInt(details.viewCount, 10) : null, likes: null, thumbnail: extractThumbnailUrl(details), duration: details.lengthSeconds, country: null, monetized: void 0 !== microformat.isFamilySafe, channelId: details.channelId }; })(data); if (stats.channelId) { stats.country = await (async function fetchChannelCountryFromInnerTube(channelId) { if (!channelId) { return null; } try { const url = `https://www.youtube.com/youtubei/v1/browse?key=${INNERTUBE_API_KEY}&prettyPrint=false`; const body = { browseId: channelId, context: { client: { clientName: "WEB", clientVersion: getInnerTubeClientVersion(), hl: "en", gl: "US" } } }; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "X-YouTube-Client-Name": "1", "X-YouTube-Client-Version": getInnerTubeClientVersion() }, body: JSON.stringify(body) }); if (!response.ok) { return null; } const data = await response.json(); const country = data?.header?.c4TabbedHeaderRenderer?.country || data?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows?.[0]?.metadataParts?.find?.(p => 2 === p?.text?.content?.length)?.text?.content || (() => { const mutations = data?.frameworkUpdates?.entityBatchUpdate?.mutations || []; for (const m of mutations) { const c = m?.payload?.channelHeaderMetadataEntityViewModel?.country || m?.payload?.channelBasicInfoEntityViewModel?.country; if (c) { return c; } } return null; })(); return country || null; } catch { return null; } })(stats.channelId); stats.country || (stats.country = await (async function fetchChannelCountryFromTubeInsights(channelId) { if (!channelId) { return null; } try { const response = await fetch(`https://tubeinsights.exyezed.cc/api/channels/${channelId}`); if (!response.ok) { return null; } const data = await response.json(); const country = data?.items?.[0]?.snippet?.country; return "string" == typeof country && country.trim() ? country.trim().toUpperCase() : null; } catch { return null; } })(stats.channelId)); } return stats; } catch (error) { console.error("[YouTube+][Stats] InnerTube fetch error:", error); return null; } } async function fetchStats(type, id) { if (!id) { return { ok: !1, status: 0, data: null }; } try { if ("video" === type) { const videoData = await fetchVideoStatsInnerTube(id); if (!videoData) { return { ok: !1, status: 404, data: null }; } const dislikeData = await (async function fetchDislikesData(videoId) { if (!videoId) { return null; } try { const response = await fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${videoId}`); if (!response.ok) { return null; } const data = await response.json(); return { likes: data.likes || null, dislikes: data.dislikes || null, rating: data.rating || null }; } catch (error) { console.error("[YouTube+][Stats] Failed to fetch dislikes:", error); return null; } })(id); if (dislikeData) { videoData.likes = dislikeData.likes; videoData.dislikes = dislikeData.dislikes; videoData.rating = dislikeData.rating; } return { ok: !0, status: 200, data: videoData }; } const endpoint = `https://api.livecounts.io/youtube-live-subscriber-counter/stats/${id}`; const response = await fetch(endpoint, { method: "GET", headers: { Accept: "application/json" } }); if (!response.ok) { console.warn(`[YouTube+][Stats] Failed to fetch ${type} stats:`, response.status); return { ok: !1, status: response.status, data: null, url: endpoint }; } const data = await response.json(); return { ok: !0, status: response.status, data, url: endpoint }; } catch (error) { YouTubeUtils?.logError?.("Stats", `Failed to fetch ${type} stats`, error); return { ok: !1, status: 0, data: null }; } } function getPageVideoStats() { try { const helpers = window.YouTubeStatsHelpers || {}; const fallbackHelpers = { extractViews() { try { const el = $("yt-view-count-renderer, #count .view-count"); const text = el && el.textContent ? el.textContent.trim() : ""; const match = text.replace(/[^0-9,\.]/g, "").replace(/,/g, ""); return match ? { views: Number(match) || null } : {}; } catch { return {}; } }, extractLikes() { try { const btn = $("ytd-toggle-button-renderer[is-icon-button] yt-formatted-string") || $("#top-level-buttons-computed ytd-toggle-button-renderer:first-child yt-formatted-string"); const text = btn && btn.textContent ? btn.textContent.trim() : ""; const match = text.replace(/[^0-9,\.]/g, "").replace(/,/g, ""); return match ? { likes: Number(match) || null } : {}; } catch { return {}; } }, extractDislikes: () => ({}), extractComments() { try { const el = $("#count > ytd-comment-thread-renderer, ytd-comments-header-renderer #count"); const text = el && el.textContent ? el.textContent.trim() : ""; const match = text.replace(/[^0-9,\.]/g, "").replace(/,/g, ""); return match ? { comments: Number(match) || null } : {}; } catch { return {}; } }, extractSubscribers() { try { const el = $("#owner-sub-count, #subscriber-count"); const text = el && el.textContent ? el.textContent.trim() : ""; return text ? { subscribers: text } : {}; } catch { return {}; } }, extractThumbnail() { try { const meta = $('link[rel="image_src"]') || $('meta[property="og:image"]'); const url = meta && (meta.href || meta.content) ? meta.href || meta.content : null; return url ? { thumbnail: url } : {}; } catch { return {}; } }, extractTitle() { try { const el = $("h1.title yt-formatted-string") || $("h1"); const text = el && el.textContent ? el.textContent.trim() : ""; return text ? { title: text } : {}; } catch { return {}; } }, extractAuthor() { try { const handleEl = $("ytd-video-owner-renderer #channel-handle") || $("ytd-video-owner-renderer yt-formatted-string.ytd-channel-name a") || $("#owner ytd-channel-name a") || $("ytd-video-owner-renderer #owner-name a"); const handleText = handleEl?.textContent?.trim() || ""; const handle = handleText.startsWith("@") ? handleText : null; const nameEl = $("ytd-video-owner-renderer #channel-name") || $("ytd-video-owner-renderer #owner-name"); const authorName = nameEl?.textContent?.trim() || null; return handle || authorName ? { authorHandle: handle, author: authorName } : {}; } catch { return {}; } } }; const use = helpers && helpers.extractViews ? helpers : fallbackHelpers; const result = Object.assign({}, use.extractViews?.() || {}, use.extractLikes?.() || {}, use.extractDislikes?.() || {}, use.extractComments?.() || {}, use.extractSubscribers?.() || {}, use.extractThumbnail?.() || {}, use.extractTitle?.() || {}, use.extractAuthor?.() || {}); return Object.keys(result).length > 0 ? result : null; } catch (e) { YouTubeUtils?.logError?.("Stats", "Failed to read page stats", e); return null; } } function buildValueOnlyCard(value, iconOrClass = "", options = { showValue: !0, showIcon: !0 }) { const {showValue, showIcon} = options; if (!showValue && !showIcon) { return ""; } let displayVal = ""; showValue && (displayVal = value ?? t("unknown")); let iconContent = ""; let extraClass = ""; showIcon && (iconOrClass && "string" == typeof iconOrClass && iconOrClass.indexOf("<") >= 0 ? iconContent = iconOrClass : iconOrClass && "string" == typeof iconOrClass && (extraClass = ` ${iconOrClass}`)); return `\n <div class="stats-card">\n <div class="stats-icon${extraClass}">${iconContent}</div>\n <div class="stats-info">\n ${showValue ? `<div class="stats-value">${displayVal}</div>` : ""}\n </div>\n </div>\n `; } function buildStatCards(pageStats) { const cardConfigs = [ { value: pageStats.views, key: "views", icon: "stats-icon-views", svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>' }, { value: pageStats.likes, key: "likes", icon: "stats-icon-likes", svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>' }, { value: pageStats.dislikes, key: "dislikes", icon: "stats-icon-dislikes", svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>' }, { value: pageStats.comments, key: "comments", icon: "stats-icon-comments", svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>' } ]; return cardConfigs.map(config => (function buildPageStatCard(value, labelKey, iconClass, iconSvg) { return null == value ? "" : `\n <div class="stats-card">\n <div class="stats-icon ${iconClass}">\n ${iconSvg}\n </div>\n <div class="stats-info">\n <div class="stats-label">${t(labelKey)}</div>\n <div class="stats-value">${formatNumber(value)}</div>\n <div class="stats-exact">${(value || 0).toLocaleString()}</div>\n </div>\n </div>\n `; })(config.value, config.key, config.icon, config.svg)).filter(card => card); } function getThumbnailUrl(id, pageStats) { if (pageStats && pageStats.thumbnail) { try { const parsed = new URL(pageStats.thumbnail); const h = parsed.hostname; if ("https:" === parsed.protocol && ("ytimg.com" === h || h.endsWith(".ytimg.com") || "ggpht.com" === h || h.endsWith(".ggpht.com") || "googleusercontent.com" === h || h.endsWith(".googleusercontent.com") || "youtube.com" === h || h.endsWith(".youtube.com"))) { return pageStats.thumbnail; } } catch {} } return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : ""; } function buildThumbnailLayout(titleHtml, thumbUrl, gridHtml, extras) { const extraCards = (function buildExtraCards(extras) { const monetizationText = extras.monetization || t("unknown"); const countryText = extras.country || t("unknown"); const durationText = extras.duration || t("unknown"); const extraMonCard = buildValueOnlyCard(monetizationText, "stats-icon-subscribers", { showValue: !1, showIcon: !0 }); const extraCountryCard = buildValueOnlyCard(countryText, "stats-icon-views", { showValue: !1, showIcon: !0 }); const extraDurationCard = buildValueOnlyCard(durationText, "stats-icon-videos", { showValue: !0, showIcon: !1 }); return `${extraMonCard}${extraCountryCard}${extraDurationCard}`; })(extras); const leftHtml = `<div class="stats-thumb-left"><img class="stats-thumb-img" src="${thumbUrl}" alt="thumbnail"><div class="stats-thumb-extras">${extraCards}</div></div>`; return `${titleHtml}<div class="stats-thumb-row">${leftHtml}${gridHtml}</div>`; } function formatNumber(num) { if (!num || isNaN(num)) { return "0"; } const absNum = Math.abs(num); return absNum >= 1e9 ? `${(num / 1e9).toFixed(1)}B` : absNum >= 1e6 ? `${(num / 1e6).toFixed(1)}M` : absNum >= 1e3 ? `${(num / 1e3).toFixed(1)}K` : num.toLocaleString(); } function makeStatsCard(labelKey, value, exact, iconClass, iconSvg) { const display = null == value ? t("unknown") : formatNumber(value); let exactText = "—"; if (null != exact) { const numExact = Number(exact); exactText = isNaN(numExact) ? String(exact) : Math.floor(numExact).toLocaleString(); } return `\n <div class="stats-card">\n <div class="stats-icon ${iconClass}">\n ${iconSvg}\n </div>\n <div class="stats-info">\n <div class="stats-label">${t(labelKey)}</div>\n <div class="stats-value">${display}</div>\n <div class="stats-exact">${exactText}</div>\n </div>\n </div>\n `; } function getFirstAvailableField(stats, ...fields) { for (const field of fields) { if (null != stats?.[field]) { return stats[field]; } } return null; } function getThumbnailUrl(stats, id) { const raw = stats?.thumbnail; if (raw) { try { const parsed = new URL(raw); const h = parsed.hostname; if ("https:" === parsed.protocol && ("ytimg.com" === h || h.endsWith(".ytimg.com") || "ggpht.com" === h || h.endsWith(".ggpht.com") || "googleusercontent.com" === h || h.endsWith(".googleusercontent.com") || "youtube.com" === h || h.endsWith(".youtube.com"))) { return raw; } } catch {} } return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : ""; } function getVideoExtras(apiStats, pageStats) { const helpers = window.YouTubeStatsHelpers || {}; const duration = apiStats?.duration ?? pageStats?.duration ?? helpers.getDurationFromSources?.(apiStats, pageStats) ?? null; const country = apiStats?.country ?? pageStats?.country ?? helpers.getCountryFromSources?.(apiStats, pageStats) ?? null; let monetization = null; monetization = null != apiStats?.monetized ? t(!0 === apiStats.monetized ? "yes" : "no") : null != pageStats?.monetized ? t(!0 === pageStats.monetized ? "yes" : "no") : helpers.getMonetizationFromSources?.(apiStats, pageStats, t) ?? null; return { duration, country, monetization }; } function createStatsModalStructure(overlay) { const container = document.createElement("div"); container.className = "stats-modal-container"; container.setAttribute("role", "dialog"); container.setAttribute("aria-modal", "true"); container.setAttribute("aria-label", t("videoStats") || "Video Statistics"); const content = document.createElement("div"); content.className = "stats-modal-content"; const body = document.createElement("div"); body.className = "stats-modal-body"; body.appendChild((function createLoadingSpinner() { const loader = document.createElement("div"); loader.className = "stats-loader"; loader.innerHTML = _createHTML(`\n <svg class="stats-spinner" viewBox="0 0 50 50">\n <circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="4"></circle>\n </svg>\n <p>${t("loadingStats")}</p>\n `); return loader; })()); content.appendChild(body); const wrapper = document.createElement("div"); wrapper.className = "thumbnail-modal-wrapper"; const actionsDiv = document.createElement("div"); actionsDiv.className = "thumbnail-modal-actions"; actionsDiv.appendChild((function createStatsModalCloseButton(overlay) { const closeBtn = document.createElement("button"); closeBtn.className = "thumbnail-modal-close thumbnail-modal-action-btn"; closeBtn.innerHTML = _createHTML('\n <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\n </svg>\n '); closeBtn.title = t("close"); closeBtn.setAttribute("aria-label", t("close")); closeBtn.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); overlay.remove(); }); return closeBtn; })(overlay)); wrapper.appendChild(content); wrapper.appendChild(actionsDiv); container.appendChild(wrapper); return { body, container }; } function handleFailedFetch(body, result, id) { const pageStats = getPageVideoStats(); pageStats ? (function renderPageFallback(container, pageStats, id) { const cards = buildStatCards(pageStats); const gridHtml = `<div class="stats-grid">${cards.join("")}</div>`; const title = pageStats && pageStats.title || document.title || ""; const escapeHtml = window.YouTubeSecurityUtils?.escapeHtml || (s => { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }); const safeTitle = escapeHtml(title); const titleHtml = safeTitle ? `<div class="stats-thumb-title-centered">${safeTitle}</div>` : ""; const thumbUrl = getThumbnailUrl(id, pageStats); const extras = getVideoExtras(null, pageStats); container.innerHTML = _createHTML(thumbUrl ? buildThumbnailLayout(titleHtml, thumbUrl, gridHtml, extras) : `${titleHtml}${gridHtml}`); })(body, pageStats, id) : (function renderErrorMessage(body, result) { const statusText = result?.status ? ` (${result.status})` : ""; const endpointHint = result?.url ? `<div style="margin-top:8px;font-size:12px;opacity:0.8;word-break:break-all">${result.url}</div>` : ""; body.innerHTML = _createHTML(`\n <div class="stats-error">\n <svg class="stats-error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <circle cx="12" cy="12" r="10"></circle>\n <line x1="12" y1="8" x2="12" y2="12"></line>\n <line x1="12" y1="16" x2="12.01" y2="16"></line>\n </svg>\n <p>${t("failedToLoadStats")}${statusText}</p>\n ${endpointHint}\n </div>\n `); })(body, result); } function displayStatsBasedOnType(body, type, stats, id) { if ("video" === type) { try { const pageStats = getPageVideoStats(); const merged = (function mergeVideoStats(apiStats, pageStats) { if (!pageStats) { return apiStats || {}; } const getValue = (...fields) => { for (const field of fields) { if (null != apiStats?.[field]) { return apiStats[field]; } } for (const field of fields) { if (null != pageStats?.[field]) { return pageStats[field]; } } return null; }; return { ...apiStats, views: getValue("views", "viewCount"), likes: getValue("likes", "likeCount"), dislikes: getValue("dislikes"), comments: getValue("comments", "commentCount"), thumbnail: getValue("thumbnail"), title: getValue("title"), liveViewer: getValue("liveViewer"), duration: getValue("duration"), country: getValue("country"), monetized: getValue("monetized", "isMonetized", "monetization"), author: getValue("author"), authorHandle: getValue("authorHandle") }; })(stats, pageStats); displayVideoStats(body, merged, id); } catch { displayVideoStats(body, stats, id); } } else { !(function displayChannelStats(container, stats) { const {liveSubscriber, liveViews, liveVideos} = stats; container.innerHTML = _createHTML(`\n <div class="stats-grid">\n <div class="stats-card">\n <div class="stats-icon stats-icon-subscribers">\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>\n <circle cx="9" cy="7" r="4"></circle>\n <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>\n <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>\n </svg>\n </div>\n <div class="stats-info">\n <div class="stats-label">${t("subscribers")}</div>\n <div class="stats-value">${formatNumber(liveSubscriber)}</div>\n <div class="stats-exact">${(liveSubscriber || 0).toLocaleString()}</div>\n </div>\n </div>\n\n <div class="stats-card">\n <div class="stats-icon stats-icon-views">\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>\n <circle cx="12" cy="12" r="3"></circle>\n </svg>\n </div>\n <div class="stats-info">\n <div class="stats-label">${t("totalViews")}</div>\n <div class="stats-value">${formatNumber(liveViews)}</div>\n <div class="stats-exact">${(liveViews || 0).toLocaleString()}</div>\n </div>\n </div>\n\n <div class="stats-card">\n <div class="stats-icon stats-icon-videos">\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <polygon points="23 7 16 12 23 17 23 7"></polygon>\n <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>\n </svg>\n </div>\n <div class="stats-info">\n <div class="stats-label">${t("totalVideos")}</div>\n <div class="stats-value">${formatNumber(liveVideos)}</div>\n <div class="stats-exact">${(liveVideos || 0).toLocaleString()}</div>\n </div>\n </div>\n </div>\n `); })(body, stats); } } async function openStatsModal(type, id) { if (!type || !id) { console.error("[YouTube+][Stats] Invalid parameters for modal"); return; } const existingOverlays = $$(".stats-modal-overlay"); for (let i = 0; i < existingOverlays.length; i++) { try { existingOverlays[i].remove(); } catch {} } const overlay = document.createElement("div"); overlay.className = "stats-modal-overlay"; const {body, container} = createStatsModalStructure(overlay); overlay.appendChild(container); !(function setupModalEventHandlers(overlay) { const previouslyFocused = document.activeElement; overlay.addEventListener("click", ({target}) => { if (target === overlay) { overlay.remove(); try { previouslyFocused && previouslyFocused.focus(); } catch {} } }); window.addEventListener("keydown", function escHandler(e) { if ("Escape" === e.key) { overlay.remove(); window.removeEventListener("keydown", escHandler, !0); try { previouslyFocused && previouslyFocused.focus(); } catch {} } }, !0); requestAnimationFrame(() => { const focusTarget = overlay.querySelector('button, [tabindex="0"]'); focusTarget && focusTarget.focus(); }); if (window.YouTubePlusModalHandlers && window.YouTubePlusModalHandlers.createFocusTrap) { const removeTrap = window.YouTubePlusModalHandlers.createFocusTrap(overlay); const obs = new MutationObserver(() => { if (!overlay.isConnected) { removeTrap(); obs.disconnect(); } }); obs.observe(overlay.parentNode || document.body, { childList: !0 }); } })(overlay); document.body.appendChild(overlay); const result = await fetchStats(type, id); result?.ok ? displayStatsBasedOnType(body, type, result.data, id) : handleFailedFetch(body, result, id); } function createMonetizationCard(extras, stats) { const monetizationValue = extras.monetization || t("unknown"); const isMonetized = extras.monetization === t("yes") || !0 === stats?.monetized; const monIcon = isMonetized ? '<svg viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg>' : '<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>'; return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-subscribers">${monIcon}</div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t("monetization")}</div><div class="stats-value" style="font-size:16px;">${monetizationValue}</div></div></div>`; } function createCountryCard(extras) { const escapeHtml = window.YouTubeSecurityUtils?.escapeHtml || (s => { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }); const countryValue = escapeHtml(extras.country || t("unknown")); const rawCode = extras.country && extras.country !== t("unknown") ? extras.country.toUpperCase() : ""; const countryCode = /^[A-Z]{2}$/.test(rawCode) ? rawCode : ""; if (countryCode) { const flagUrl = `https://cdn.jsdelivr.net/gh/lipis/[email protected]/flags/4x3/${countryCode.toLowerCase()}.svg`; return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-views" data-fallback-icon="globe"><img class="country-flag" src="${flagUrl}" alt="${countryCode}" width="32" height="24" style="border-radius:4px;"/></div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t("country")}</div><div class="stats-value" style="font-size:16px;">${countryCode}</div></div></div>`; } return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-views"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg></div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t("country")}</div><div class="stats-value" style="font-size:16px;">${countryValue}</div></div></div>`; } function createDurationCard(extras) { const raw = extras?.duration ?? null; const formatted = (function formatDuration(value) { if (null == value) { return null; } function pad(n) { return String(n).padStart(2, "0"); } function secToHms(sec) { sec = Math.max(0, Math.floor(Number(sec) || 0)); const h = Math.floor(sec / 3600); const m = Math.floor(sec % 3600 / 60); const s = sec % 60; return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`; } if ("number" == typeof value && Number.isFinite(value)) { return secToHms(value); } if ("string" == typeof value) { const s = value.trim(); if (/^\d+$/.test(s)) { return secToHms(Number(s)); } const iso = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i.exec(s); if (iso) { const h = parseInt(iso[1] || "0", 10); const m = parseInt(iso[2] || "0", 10); const sec = parseInt(iso[3] || "0", 10); const total = 3600 * h + 60 * m + sec; return secToHms(total); } if (/^\d+:\d{1,2}(:\d{1,2})?$/.test(s)) { const parts = s.split(":").map(p => p.replace(/^0+(\d)/, "$1")); if (2 === parts.length) { const [mm, ss] = parts; return `${Number(mm)}:${pad(Number(ss))}`; } if (3 === parts.length) { const [hh, mm, ss] = parts; return `${Number(hh)}:${pad(Number(mm))}:${pad(Number(ss))}`; } } return s || null; } return null; })(raw); const durationValue = formatted || (raw ? String(raw) : null) || t("unknown"); return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-videos"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t("duration")}</div><div class="stats-value" style="font-size:16px;">${durationValue}</div></div></div>`; } function displayVideoStats(container, stats, id) { const fields = (function extractVideoFields(stats, id) { return { views: getFirstAvailableField(stats, "liveViews", "views", "viewCount"), likes: getFirstAvailableField(stats, "liveLikes", "likes", "likeCount"), dislikes: getFirstAvailableField(stats, "dislikes", "liveDislikes", "dislikeCount"), comments: getFirstAvailableField(stats, "liveComments", "comments", "commentCount"), liveViewer: getFirstAvailableField(stats, "liveViewer", "live_viewers"), title: stats?.title || document.title || "", thumbUrl: getThumbnailUrl(stats, id), country: getFirstAvailableField(stats, "country"), monetized: stats?.monetized ?? null, duration: getFirstAvailableField(stats, "duration"), author: getFirstAvailableField(stats, "author"), authorHandle: getFirstAvailableField(stats, "authorHandle") }; })(stats, id); const {liveViewer, title, thumbUrl} = fields; const escapeHtml = window.YouTubeSecurityUtils?.escapeHtml || (s => { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }); const safeTitle = escapeHtml(title); const titleHtml = safeTitle ? `<div class="stats-thumb-title-centered">${safeTitle}</div>` : ""; const defs = (function getVideoStatDefinitions(fields) { const {views, likes, dislikes, comments} = fields; return [ { label: "views", value: views, exact: views, iconClass: "stats-icon-views", iconSvg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>' }, { label: "likes", value: likes, exact: likes, iconClass: "stats-icon-likes", iconSvg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>' }, { label: "dislikes", value: dislikes, exact: dislikes, iconClass: "stats-icon-dislikes", iconSvg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>' }, { label: "comments", value: comments, exact: comments, iconClass: "stats-icon-comments", iconSvg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>' } ]; })(fields); const viewsDef = defs.find(d => "views" === d.label); const likesDef = defs.find(d => "likes" === d.label); const dislikesDef = defs.find(d => "dislikes" === d.label); const commentsDef = defs.find(d => "comments" === d.label); const viewsHtml = viewsDef ? makeStatsCard(viewsDef.label, viewsDef.value, viewsDef.exact, viewsDef.iconClass, viewsDef.iconSvg) : ""; const likesHtml = likesDef ? makeStatsCard(likesDef.label, likesDef.value, likesDef.exact, likesDef.iconClass, likesDef.iconSvg) : ""; const dislikesHtml = dislikesDef ? makeStatsCard(dislikesDef.label, dislikesDef.value, dislikesDef.exact, dislikesDef.iconClass, dislikesDef.iconSvg) : ""; const commentsHtml = commentsDef ? makeStatsCard(commentsDef.label, commentsDef.value, commentsDef.exact, commentsDef.iconClass, commentsDef.iconSvg) : ""; const pairHtml = likesHtml || dislikesHtml ? `<div class="stats-card-pair">${likesHtml}${dislikesHtml}</div>` : ""; const {author, authorHandle} = fields; const safeAuthor = author ? escapeHtml(String(author)) : ""; const safeHandle = authorHandle ? escapeHtml(String(authorHandle)) : ""; const authorBigHtml = safeHandle || safeAuthor ? `<div class="stats-author-big">${safeHandle ? `<a class="stats-author-handle-big" href="https://www.youtube.com/${encodeURIComponent(authorHandle)}" target="_blank" rel="noopener noreferrer">${safeHandle}</a>` : `<span class="stats-author-name-big">${safeAuthor}</span>`}</div>` : ""; const parts = [ viewsHtml, pairHtml, commentsHtml ].filter(Boolean); const liveViewerCard = (function createLiveViewerCard(liveViewer) { return null == liveViewer ? "" : makeStatsCard("liveViewers", liveViewer, liveViewer, "stats-icon-viewers", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>'); })(liveViewer); liveViewerCard && parts.push(liveViewerCard); const gridHtml = `<div class="stats-grid">${parts.join("")}</div>`; const sideColumnHtml = `<div class="stats-side-column">${gridHtml}${authorBigHtml}</div>`; if (thumbUrl) { const extras = getVideoExtras(stats, null); const metaCardsHtml = (function buildMetaCardsHtml(stats, extras) { const cards = [ createMonetizationCard(extras, stats), createCountryCard(extras), createDurationCard(extras) ]; return cards.filter(Boolean).join(""); })(stats, extras); const metaExtrasHtml = metaCardsHtml ? `<div class="stats-thumb-extras" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;">${metaCardsHtml}</div>` : ""; const leftHtml = `<div class="stats-thumb-left"><img class="stats-thumb-img" src="${thumbUrl}" alt="thumbnail">${metaExtrasHtml}</div>`; container.innerHTML = _createHTML(`${titleHtml}<div class="stats-thumb-row">${leftHtml}${sideColumnHtml}</div>`); } else { container.innerHTML = _createHTML(`${titleHtml}${sideColumnHtml}`); } !(function setupFlagImageErrorHandlers(container) { const flagImages = $$(".country-flag", container); const globeIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>'; flagImages.forEach(img => { img.addEventListener("error", function() { const iconContainer = this.parentElement; if (iconContainer && "globe" === iconContainer.dataset.fallbackIcon) { this.style.display = "none"; iconContainer.innerHTML = _createHTML(globeIcon); } }, { once: !0 }); }); })(container); } function createStatsMenu() { if (!statsButtonEnabled) { return; } if ($(".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", t("stats")); mainButton.style.display = "flex"; mainButton.style.alignItems = "center"; mainButton.style.justifyContent = "center"; mainButton.style.gap = "8px"; const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 512 512"); svg.style.width = "20px"; svg.style.height = "20px"; svg.style.fill = "currentColor"; const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", "M500 89c13.8-11 16-31.2 5-45s-31.2-16-45-5L319.4 151.5 211.2 70.4c-11.7-8.8-27.8-8.5-39.2 .6L12 199c-13.8 11-16 31.2-5 45s31.2 16 45 5L192.6 136.5l108.2 81.1c11.7 8.8 27.8 8.5 39.2-.6L500 89zM160 256l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32zM32 352l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32zm288-64c-17.7 0-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32zm96-32l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32z"); svg.appendChild(path); const buttonText = document.createElement("div"); buttonText.className = "yt-spec-button-shape-next__button-text-content main-stats-text"; buttonText.textContent = t("stats"); buttonText.style.display = "flex"; buttonText.style.alignItems = "center"; const touchFeedback = document.createElement("yt-touch-feedback-shape"); touchFeedback.style.borderRadius = "inherit"; const touchFeedbackDiv = document.createElement("div"); touchFeedbackDiv.className = "yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"; touchFeedbackDiv.setAttribute("aria-hidden", "true"); const strokeDiv = document.createElement("div"); strokeDiv.className = "yt-spec-touch-feedback-shape__stroke"; const fillDiv = document.createElement("div"); fillDiv.className = "yt-spec-touch-feedback-shape__fill"; touchFeedbackDiv.appendChild(strokeDiv); touchFeedbackDiv.appendChild(fillDiv); touchFeedback.appendChild(touchFeedbackDiv); mainButton.appendChild(svg); mainButton.appendChild(buttonText); mainButton.appendChild(touchFeedback); mainButtonViewModel.appendChild(mainButton); containerDiv.appendChild(mainButtonViewModel); const horizontalMenu = document.createElement("div"); horizontalMenu.className = "stats-horizontal-menu"; const channelButtonContainer = document.createElement("div"); channelButtonContainer.className = "stats-menu-button channel-stats-container"; const channelButton = createButton(t("channel"), "M64 48c-8.8 0-16 7.2-16 16l0 288c0 8.8 7.2 16 16 16l512 0c8.8 0 16-7.2 16-16l0-288c0-8.8-7.2-16-16-16L64 48zM0 64C0 28.7 28.7 0 64 0L576 0c35.3 0 64 28.7 64 64l0 288c0 35.3-28.7 64-64 64L64 416c-35.3 0-64-28.7-64-64L0 64zM120 464l400 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-400 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z", "0 0 640 512", "channel-stats", () => { const channelId = getChannelIdentifier(); channelId && openStatsModal("channel", channelId); }); channelButtonContainer.appendChild(channelButton); horizontalMenu.appendChild(channelButtonContainer); if (channelFeatures.hasStreams) { const liveButtonContainer = document.createElement("div"); liveButtonContainer.className = "stats-menu-button live-stats-container"; const liveButton = createButton(t("live"), "M99.8 69.4c10.2 8.4 11.6 23.6 3.2 33.8C68.6 144.7 48 197.9 48 256s20.6 111.3 55 152.8c8.4 10.2 7 25.3-3.2 33.8s-25.3 7-33.8-3.2C24.8 389.6 0 325.7 0 256S24.8 122.4 66 72.6c8.4-10.2 23.6-11.6 33.8-3.2zm376.5 0c10.2-8.4 25.3-7 33.8 3.2c41.2 49.8 66 113.8 66 183.4s-24.8 133.6-66 183.4c-8.4 10.2-23.6 11.6-33.8 3.2s-11.6-23.6-3.2-33.8c34.3-41.5 55-94.7 55-152.8s-20.6-111.3-55-152.8c-8.4-10.2-7-25.3 3.2-33.8zM248 256a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zm-61.1-78.5C170 199.2 160 226.4 160 256s10 56.8 26.9 78.5c8.1 10.5 6.3 25.5-4.2 33.7s-25.5 6.3-33.7-4.2c-23.2-29.8-37-67.3-37-108s13.8-78.2 37-108c8.1-10.5 23.2-12.3 33.7-4.2s12.3 23.2 4.2 33.7zM427 148c23.2 29.8 37 67.3 37 108s-13.8 78.2-37 108c-8.1 10.5-23.2 12.3-33.7 4.2s-12.3-23.2-4.2-33.7C406 312.8 416 285.6 416 256s-10-56.8-26.9-78.5c-8.1-10.5-6.3-25.5 4.2-33.7s25.5-6.3 33.7 4.2z", "0 0 576 512", "live-stats", () => { const channelId = getChannelIdentifier(); channelId && openStatsModal("channel", channelId); }); liveButtonContainer.appendChild(liveButton); horizontalMenu.appendChild(liveButtonContainer); } if (channelFeatures.hasShorts) { const shortsButtonContainer = document.createElement("div"); shortsButtonContainer.className = "stats-menu-button shorts-stats-container"; const shortsButton = createButton(t("shorts"), "M80 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16l224 0c8.8 0 16-7.2 16-16l0-384c0-8.8-7.2-16-16-16L80 48zM16 64C16 28.7 44.7 0 80 0L304 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L80 512c-35.3 0-64-28.7-64-64L16 64zM160 400l64 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-64 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z", "0 0 384 512", "shorts-stats", () => { const channelId = getChannelIdentifier(); channelId && openStatsModal("channel", channelId); }); shortsButtonContainer.appendChild(shortsButton); horizontalMenu.appendChild(shortsButtonContainer); } containerDiv.appendChild(horizontalMenu); const joinButton = $(".yt-flexible-actions-view-model-wiz__action:not(.stats-menu-container)"); if (joinButton) { joinButton.parentNode.appendChild(containerDiv); } else { const buttonContainer = $("#subscribe-button + #buttons"); buttonContainer && buttonContainer.appendChild(containerDiv); } return containerDiv; } function checkAndAddMenu() { if (!statsButtonEnabled) { return; } const joinButton = $(".yt-flexible-actions-view-model-wiz__action:not(.stats-menu-container)"); const statsMenu = $(".stats-menu-container"); joinButton && !statsMenu && createStatsMenu(); } function checkAndInsertIcon() { statsButtonEnabled && insertUniversalIcon(); } let ensureSettingsScheduler = null; function ensureSettingsUI() { ensureSettingsScheduler && ensureSettingsScheduler.stop(); ensureSettingsScheduler = (function createSafeRetryScheduler(opts) { const factory = window.YouTubeUtils?.createRetryScheduler; if ("function" == typeof factory) { try { return factory(opts); } catch (error) { YouTubeUtils?.logError?.("Stats", "Retry scheduler factory failed", error); } } const {check, maxAttempts = 20, interval = 100} = opts || {}; let attempts = 0; let timerId = null; let stopped = !1; const tick = () => { if (!stopped) { attempts += 1; try { if ("function" == typeof check && check()) { stopped = !0; return; } } catch (error) { YouTubeUtils?.logError?.("Stats", "Fallback retry check failed", error); } attempts >= maxAttempts ? stopped = !0 : timerId = setTimeout(tick, interval); } }; timerId = setTimeout(tick, 0); return { stop() { stopped = !0; timerId && clearTimeout(timerId); timerId = null; } }; })({ check: () => (function addSettingsUI() { const section = $('.ytp-plus-settings-section[data-section="experimental"]'); if (!section) { return !1; } const existingItem = $(".stats-button-settings-item", section); if (existingItem) { const label = existingItem.querySelector(".ytp-plus-settings-item-label"); const description = existingItem.querySelector(".ytp-plus-settings-item-description"); label && (label.textContent = t("statisticsButton")); description && (description.textContent = t("statisticsButtonDescription")); return !0; } const item = document.createElement("div"); item.className = "ytp-plus-settings-item stats-button-settings-item"; item.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label">${t("statisticsButton")}</label>\n <div class="ytp-plus-settings-item-description">${t("statisticsButtonDescription")}</div>\n </div>\n <input type="checkbox" class="ytp-plus-settings-checkbox" ${statsButtonEnabled ? "checked" : ""}>\n `); section.appendChild(item); item.querySelector("input")?.addEventListener("change", e => { const {target} = e; const input = target; statsButtonEnabled = input.checked; localStorage.setItem("youtube_stats_button_enabled", statsButtonEnabled ? "true" : "false"); Array.from(document.querySelectorAll(`${STATS_ICON_SELECTOR}, .stats-menu-container`)).forEach(el => el.remove()); if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } }); return !0; })(), maxAttempts: 20, interval: 100 }); } YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "youtube-plus-settings-modal-opened", () => { ensureSettingsUI(); }) : document.addEventListener("youtube-plus-settings-modal-opened", () => { ensureSettingsUI(); }); const handleExperimentalNavClick = e => { const {target} = e; const el = target; const navItem = el?.closest?.(".ytp-plus-settings-nav-item"); "experimental" === navItem?.dataset?.section && ensureSettingsUI(); }; YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "youtube-plus-language-changed", () => { ensureSettingsUI(); }) : document.addEventListener("youtube-plus-language-changed", () => { ensureSettingsUI(); }); if (!experimentalNavListenerKey) { if (YouTubeUtils?.cleanupManager?.registerListener) { experimentalNavListenerKey = YouTubeUtils.cleanupManager.registerListener(document, "click", handleExperimentalNavClick, !0); } else { document.addEventListener("click", handleExperimentalNavClick, !0); experimentalNavListenerKey = "native-click-listener"; } } function init() { !(function addStyles() { byId("youtube-enhancer-styles") || YouTubeUtils.StyleManager.add("youtube-enhancer-styles", "\n .videoStats{width:36px;height:36px;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;margin-left:8px;margin-right:8px;background:transparent;backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);border:none;transition:transform .18s ease,background .18s}\n html[dark] .videoStats{background:transparent;border:none}html:not([dark]) .videoStats{background:transparent;border:none}.videoStats:hover{transform:translateY(-2px)}.videoStats svg{width:18px;height:18px;fill:var(--yt-spec-text-primary,#030303)}html[dark] .videoStats svg{fill:#fff}html:not([dark]) .videoStats svg{fill:#222}\n .shortsStats{display:flex;align-items:center;justify-content:center;margin-top:16px;margin-bottom:16px;width:48px;height:48px;border-radius:50%;cursor:pointer;background:rgba(255,255,255,0.12);box-shadow:0 12px 30px rgba(0,0,0,0.32);backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);border:1.25px solid rgba(255,255,255,0.12);transition:transform .22s ease}html[dark] .shortsStats{background:rgba(24,24,24,0.68);border:1.25px solid rgba(255,255,255,0.08)}html:not([dark]) .shortsStats{background:rgba(255,255,255,0.12);border:1.25px solid rgba(0,0,0,0.06)}\n .shortsStats:hover{transform:translateY(-3px)}.shortsStats svg{width:24px;height:24px;fill:#222}html[dark] .shortsStats svg{fill:#fff}html:not([dark]) .shortsStats svg{fill:#222}\n .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}\n /* Modal overlay and container with glassmorphism */\n .stats-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:linear-gradient(rgba(0,0,0,0.45),rgba(0,0,0,0.55));z-index:99999;display:flex;align-items:center;justify-content:center;animation:fadeInModal .18s;backdrop-filter:blur(20px) saturate(170%);-webkit-backdrop-filter:blur(20px) saturate(170%)}\n .stats-modal-container{max-width:1100px;max-height:calc(100vh - 32px);display:flex;flex-direction:column}\n .stats-modal-content{background:rgba(24,24,24,0.92);border-radius:20px;box-shadow:0 18px 40px rgba(0,0,0,0.45);overflow:hidden;display:flex;flex-direction:column;animation:scaleInModal .18s;border:1.5px solid rgba(255,255,255,0.08);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%)}\n /* Fix custom element display for Chrome */\n button-view-model{display:inline-flex;align-items:center;justify-content:center;}\n button-view-model.yt-spec-button-view-model{vertical-align:top;}\n html[dark] .stats-modal-content{background:rgba(24, 24, 24, 0.25)}\n html:not([dark]) .stats-modal-content{background:rgba(255,255,255,0.95);color:#222;border:1.25px solid rgba(0,0,0,0.06)}\n .stats-modal-close{background:transparent;border:none;color:#fff;font-size:36px;line-height:1;width:36px;height:36px;cursor:pointer;transition:transform .15s ease,color .15s;display:flex;align-items:center;justify-content:center;border-radius:8px;padding:0}\n .stats-modal-close:hover{color:#ff6b6b;transform:scale(1.1)}\n html:not([dark]) .stats-modal-close{color:#666}\n html:not([dark]) .stats-modal-close:hover{color:#ff6b6b} \n /* Modal body */\n .stats-modal-body{padding:16px;overflow:visible;flex:1;display:flex;flex-direction:column}\n /* Thumbnail preview */\n .stats-thumb-title-centered{font-size:16px;font-weight:600;color:#fff;margin:0 0 15px 0;text-align:center}\n html:not([dark]) .stats-thumb-title-centered{color:#111}\n .stats-thumb-row{display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap}\n .stats-thumb-img{width:36vw;max-width:420px;height:auto;object-fit:cover;border-radius:8px;flex-shrink:0;border:1px solid rgba(255,255,255,0.06);max-height:44vh}\n html:not([dark]) .stats-thumb-img{border:1px solid rgba(0,0,0,0.06)}\n /* ensure the grid takes remaining horizontal space */\n .stats-thumb-row .stats-grid{flex:1;min-width:0}\n .stats-side-column{flex:1;min-width:280px;display:flex;flex-direction:column}\n .stats-thumb-left{display:flex;flex-direction:column;align-items:center;gap:8px}\n .stats-thumb-left .stats-thumb-sub{font-size:13px;color:rgba(255,255,255,0.65)}\n html:not([dark]) .stats-thumb-left .stats-thumb-sub{color:rgba(0,0,0,0.6)}\n /* extras row under thumbnail: inline, single line */\n .stats-thumb-extras{display:flex;flex-direction:row;gap:10px;align-items:center;margin-top:8px}\n .stats-thumb-extras .stats-card{padding:8px 10px}\n .stats-thumb-meta{display:flex;flex-direction:column;justify-content:center}\n .stats-thumb-sub{font-size:13px;color:rgba(255,255,255,0.65)}\n html:not([dark]) .stats-thumb-sub{color:rgba(0,0,0,0.6)} \n /* Loading state */\n .stats-loader{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;color:#fff}\n html:not([dark]) .stats-loader{color:#666}\n .stats-spinner{width:60px;height:60px;animation:spin 1s linear infinite;margin-bottom:16px}\n .stats-spinner circle{stroke-dasharray:80;stroke-dashoffset:60;animation:dash 1.5s ease-in-out infinite} \n /* Error state */\n .stats-error{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;color:#ff6b6b;text-align:center}\n .stats-error-icon{width:60px;height:60px;margin-bottom:16px;stroke:#ff6b6b} \n /* Stats grid */\n .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px} \n /* Stats card */\n .stats-card{background:rgba(255,255,255,0.05);border-radius:12px;padding:12px;display:flex;align-items:center;gap:12px;border:1px solid rgba(255,255,255,0.08);transition:transform .18s ease,box-shadow .18s ease}\n html:not([dark]) .stats-card{background:rgba(0,0,0,0.03);border:1px solid rgba(0,0,0,0.1)}\n .stats-card:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,0.3)} \n /* Stats icon */\n .stats-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0}\n .stats-icon svg{width:24px;height:24px}\n .stats-icon-views{background:rgba(59,130,246,0.15);color:#3b82f6}\n .stats-icon-likes{background:rgba(34,197,94,0.15);color:#22c55e}\n .stats-icon-dislikes{background:rgba(239,68,68,0.15);color:#ef4444}\n .stats-icon-comments{background:rgba(168,85,247,0.15);color:#a855f7}\n .stats-icon-viewers{background:rgba(234,179,8,0.15);color:#eab308}\n .stats-icon-subscribers{background:rgba(236,72,153,0.15);color:#ec4899}\n .stats-icon-videos{background:rgba(14,165,233,0.15);color:#0ea5e9}\n /* Pair likes/dislikes into a single grid cell */\n .stats-card-pair{display:flex;gap:8px;align-items:stretch}\n .stats-card-pair .stats-card{flex:1;margin:0}\n @media(max-width:480px){.stats-card-pair{flex-direction:column}} \n /* Stats info */\n .stats-info{flex:1;min-width:0}\n .stats-label{font-size:13px;color:rgba(255,255,255,0.72);margin-bottom:4px;font-weight:500}\n html:not([dark]) .stats-label{color:rgba(0,0,0,0.6)}\n .stats-value{font-size:20px;font-weight:700;color:#fff;line-height:1.2;margin-bottom:2px}\n html:not([dark]) .stats-value{color:#111}\n .stats-exact{font-size:13px;color:rgba(255,255,255,0.5);font-weight:400}\n html:not([dark]) .stats-exact{color:rgba(0,0,0,0.5)} \n /* Animations — shared keyframes (fadeInModal, scaleInModal, spin, dash) defined in basic.js */\n /* Responsive */\n @media(max-width:768px){.stats-modal-container{width:95vw}.stats-grid{grid-template-columns:1fr}.stats-card{padding:16px}.stats-side-column{min-width:0;width:100%}}\n /* Centered large author handle (preferred) */\n .stats-author-big{display:block;text-align:center;margin-top:13px;padding-inline:8px}\n .stats-author-name-big{display:block;color:rgba(255,255,255,0.9);font-weight:600;font-size:16px}\n .stats-author-handle-big{display:inline-block;color:#ffffff;font-weight:700;font-size:20px;text-decoration:none;padding:6px 10px;border-radius:6px}\n .stats-author-handle-big:hover{color:#e6f0ff;text-decoration:underline}\n html:not([dark]) .stats-author-name-big{color:rgba(0,0,0,0.8)}\n html:not([dark]) .stats-author-handle-big{color:#0b61d6}\n html:not([dark]) .stats-author-handle-big:hover{color:#0647a6}\n "); })(); if (statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); } if (YouTubeUtils?.cleanupManager?.registerListener) { YouTubeUtils.cleanupManager.registerListener(window, "ytp-history-navigate", checkUrlChange); YouTubeUtils.cleanupManager.registerListener(window, "popstate", checkUrlChange); } else { window.addEventListener("ytp-history-navigate", checkUrlChange); window.addEventListener("popstate", checkUrlChange); } isChannelPage(location.href) && checkChannelTabs(location.href); } const scheduleInit = () => { if (statsInitialized || !(() => { try { const path = location.pathname || ""; return !("/watch" !== path && !path.startsWith("/shorts")) || isChannelPage(location.href); } catch { return !1; } })()) { return; } statsInitialized = !0; const run = () => { try { init(); } catch (e) { statsInitialized = !1; throw e; } }; "function" == typeof requestIdleCallback ? requestIdleCallback(run, { timeout: 2e3 }) : setTimeout(run, 0); }; (cb => { "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", cb, { once: !0 }) : cb(); })(scheduleInit); const handleNavigate = () => { scheduleInit(); if (statsInitialized && statsButtonEnabled) { checkAndInsertIcon(); checkAndAddMenu(); isChannelPage(location.href) && checkChannelTabs(location.href); } }; window.YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", handleNavigate, { passive: !0 }) : window.addEventListener("yt-navigate-finish", handleNavigate); const handleAction = event => { scheduleInit(); if (!statsInitialized || !statsButtonEnabled) { return; } const ev = event; if (ev.detail && "yt-reload-continuation-items-command" === ev.detail.actionName) { checkAndInsertIcon(); checkAndAddMenu(); } }; window.YouTubeUtils?.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "yt-action", handleAction, { passive: !0 }) : document.addEventListener("yt-action", handleAction); }; window.YouTubePlusLazyLoader ? window.YouTubePlusLazyLoader.register("video-stats", initVideoStats, { priority: 2 }) : initVideoStats(); })(); !(function() { "use strict"; if (window.__ytpChannelStatsModuleInit) { return; } const initChannelStats = () => { if (window.__ytpChannelStatsModuleInit) { return; } window.__ytpChannelStatsModuleInit = !0; const _createHTML = window._ytplusCreateHTML || (s => s); const {$, byId} = window.YouTubeUtils || {}; if (window.YouTubeUtils?.isStudioPage?.()) { return; } const t = window.YouTubeUtils?.t || (key => key || ""); 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: 5e3, DEFAULT_OVERLAY_OPACITY: .75, MAX_RETRIES: 3, CACHE_DURATION: 3e5, DEBOUNCE_DELAY: 100, STORAGE_KEY: "youtube_channel_stats_settings" }; const state = { overlay: null, isUpdating: !1, intervalId: null, currentChannelName: null, currentChannelId: null, enabled: "false" !== localStorage.getItem(CONFIG.STORAGE_KEY), updateInterval: parseInt(localStorage.getItem("youtubeEnhancerInterval"), 10) || CONFIG.DEFAULT_UPDATE_INTERVAL, overlayOpacity: parseFloat(localStorage.getItem("youtubeEnhancerOpacity")) || CONFIG.DEFAULT_OVERLAY_OPACITY, lastSuccessfulStats: new Map, previousStats: new Map, channelIdCache: new Map, lastChannelIdWarnAt: 0, previousUrl: location.href, isChecking: !1, documentListenerKeys: new Set }; const boundedCacheSet = (map, key, value) => { if (map.size >= 50) { const firstKey = map.keys().next().value; map.delete(firstKey); } map.set(key, value); }; const utils = { log: (message, ...args) => { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[YouTube+][Stats]", message, ...args); }, warn: (message, ...args) => { console.warn("[YouTube+][Stats]", message, ...args); }, error: (message, ...args) => { console.error("[YouTube+][Stats]", message, ...args); }, debounce: window.YouTubeUtils?.debounce || ((func, wait) => { let timeout; return function executedFunction(...args) { clearTimeout(timeout); timeout = setTimeout(() => { clearTimeout(timeout); func(...args); }, wait); }; }) }; const {OPTIONS} = CONFIG; const {FONT_LINK} = CONFIG; const {STATS_API_URL} = CONFIG; async function getChannelInfo(url) { const data = await (async function fetchChannel(url) { if (state.isChecking) { return null; } state.isChecking = !0; 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 = !1; } })(url); if (!data) { return null; } try { const channelName = data?.metadata?.channelMetadataRenderer?.title || t("unknown"); const channelId = data?.metadata?.channelMetadataRenderer?.externalId || null; return { channelName, channelId }; } catch { return null; } } function isChannelPageUrl(url) { return url.includes("youtube.com/") && (url.includes("/channel/") || url.includes("/@")) && !url.includes("/video/") && !url.includes("/watch"); } function checkUrlChange() { const currentUrl = location.href; if (currentUrl !== state.previousUrl) { state.previousUrl = currentUrl; isChannelPageUrl(currentUrl) && setTimeout(() => getChannelInfo(currentUrl), 500); } } const _cm2 = window.YouTubeUtils?.cleanupManager; if (_cm2?.registerListener) { _cm2.registerListener(window, "yt-navigate-finish", checkUrlChange, { passive: !0 }); _cm2.registerListener(window, "popstate", checkUrlChange, { passive: !0 }); } else { window.addEventListener("yt-navigate-finish", checkUrlChange, { passive: !0 }); window.addEventListener("popstate", checkUrlChange, { passive: !0 }); } function init() { try { utils.log("Initializing YouTube Enhancer v1.6"); !(function loadFonts() { const fontLink = document.createElement("link"); fontLink.rel = "stylesheet"; fontLink.href = FONT_LINK; (document.head || document.documentElement).appendChild(fontLink); })(); !(function initializeLocalStorage() { OPTIONS.forEach(option => { null === localStorage.getItem(`show-${option}`) && localStorage.setItem(`show-${option}`, "true"); }); })(); !(function addStyles() { const styles = '\n .channel-banner-overlay{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:12px;z-index:9;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)} \n .settings-button{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;cursor:pointer;z-index:11;transition:all .2s ease;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.4);backdrop-filter:blur(4px);border:1px solid rgba(255,255,255,0.1);opacity:0.7}\n .channel-banner-overlay:hover .settings-button{opacity:1}\n .settings-button:hover{transform:rotate(30deg) scale(1.1);opacity:1;background:rgba(0,0,0,0.6);border-color:rgba(255,255,255,0.3)}\n .settings-button svg{width:18px;height:18px;fill:white;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.5))} \n .settings-menu{position:absolute;top:52px;right:12px;background:rgba(28,28,28,0.75);padding:16px;border-radius:16px;z-index:12;display:flex;flex-direction:column;gap:12px;backdrop-filter:blur(16px) saturate(180%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 8px 32px rgba(0,0,0,0.6);min-width:320px;opacity:0;visibility:hidden;transform:translateY(-10px) scale(0.98);transition:all 0.2s cubic-bezier(0.2,0,0.2,1);pointer-events:none}\n .settings-menu.show{opacity:1;visibility:visible;transform:translateY(0) scale(1);pointer-events:auto} \n .settings-menu .ytp-plus-settings-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-radius:8px;background:rgba(255,255,255,0.02);}\n .settings-menu .ytp-plus-settings-item + .ytp-plus-settings-item{margin-top:6px}\n .settings-menu .ytp-plus-settings-item .ytp-plus-settings-item-label{color:#eee;font-size:14px;font-weight:500}\n .settings-menu label{color:#eee!important;font-size:14px!important;font-weight:500!important;margin-bottom:6px!important} \n .settings-menu input[type="range"]{-webkit-appearance:none;width:100%!important;height:4px;background:rgba(255,255,255,0.2)!important;border-radius:2px;margin:12px 0 4px 0!important;cursor:pointer}\n .settings-menu input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;height:16px;width:16px;border-radius:50%;background:#3ea6ff;margin-top:-6px;box-shadow:0 2px 4px rgba(0,0,0,0.3);border:2px solid #fff;transition:transform .1s;cursor:pointer}\n .settings-menu input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)} \n .settings-menu select{width:100%!important;background:rgba(255,255,255,0.1)!important;border:1px solid rgba(255,255,255,0.1)!important;color:#fff!important;padding:8px 12px!important;border-radius:6px!important;font-size:13px!important;margin-bottom:12px!important;cursor:pointer;outline:none}\n .settings-menu select:hover{background:rgba(255,255,255,0.15)!important}\n .settings-menu select option{background:#333;color:#fff} \n /* Don\'t override the shared settings checkbox styling; only target non-shared inputs */\n .settings-menu input[type="checkbox"]:not(.ytp-plus-settings-checkbox){appearance:none;width:18px!important;height:18px!important;border:2px solid rgba(255,255,255,0.4)!important;border-radius:4px!important;background:transparent!important;cursor:pointer;position:relative;margin-right:12px!important;vertical-align:middle;transition:all .2s}\n .settings-menu input[type="checkbox"]:not(.ytp-plus-settings-checkbox):checked{background:#3ea6ff!important;border-color:#3ea6ff!important}\n .settings-menu input[type="checkbox"]:not(.ytp-plus-settings-checkbox):checked::after{content:\'\';position:absolute;left:5px;top:1px;width:4px;height:10px;border:solid white;border-width:0 2px 2px 0;transform:rotate(45deg)} \n .stat-container{display:flex;flex-direction:column;align-items:center;justify-content:center;visibility:hidden;width:33%;height:100%;padding:0 1rem;text-shadow:0 2px 4px rgba(0,0,0,0.3)}\n .number-container{display:flex;align-items:center;justify-content:center;font-weight:700;min-height:3rem}\n .label-container{display:flex;align-items:center;margin-top:.5rem;font-size:1.2rem;opacity:.9}\n .label-container svg{width:1.5rem;height:1.5rem;margin-right:.5rem;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.3))}\n .difference{font-size:1.8rem;height:2rem;margin-bottom:.5rem;transition:opacity .3s}\n .spinner-container{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center}\n .spinner-container .stats-spinner{width:60px;height:60px;animation:spin 1s linear infinite}\n .spinner-container .stats-spinner circle{stroke-dasharray:80;stroke-dashoffset:60;animation:dash 1.5s ease-in-out infinite}\n /* @keyframes spin already defined in video stats CSS above */\n @media(max-width:768px){.channel-banner-overlay{flex-direction:column;padding:8px;min-height:160px}.settings-menu{width:280px!important;right:4px!important;top:48px!important}}\n .setting-group{margin-bottom:12px}\n .setting-group:last-child{margin-bottom:0}\n .setting-value{color:#bbb;font-size:12px;margin-top:4px}\n '; YouTubeUtils.StyleManager.add("channel-stats-overlay", styles); })(); if (state.enabled) { observePageChanges(); addNavigationListener(); isChannelPageUrl(location.href) && getChannelInfo(location.href); } utils.log("YouTube Enhancer initialized successfully"); } catch (error) { utils.error("Failed to initialize YouTube Enhancer:", error); } } 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 = (function createDisplaySection() { const displaySection = document.createElement("div"); displaySection.style.flex = "1"; const displayLabel = document.createElement("label"); displayLabel.textContent = t("displayOptions"); displayLabel.style.marginBottom = "10px"; displayLabel.style.display = "block"; displayLabel.style.fontSize = "16px"; displayLabel.style.fontWeight = "bold"; displaySection.appendChild(displayLabel); displaySection.addEventListener("change", e => { const checkbox = e.target; if ("checkbox" === checkbox.type && checkbox.id.startsWith("show-")) { const option = checkbox.id.replace("show-", ""); localStorage.setItem(`show-${option}`, String(checkbox.checked)); updateDisplayState(); } }); OPTIONS.forEach(option => { const item = document.createElement("div"); item.className = "ytp-plus-settings-item"; const left = document.createElement("div"); const label = document.createElement("label"); label.className = "ytp-plus-settings-item-label"; label.htmlFor = `show-${option}`; label.textContent = t(option); left.appendChild(label); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = `show-${option}`; checkbox.checked = "false" !== localStorage.getItem(`show-${option}`); checkbox.className = "ytp-plus-settings-checkbox"; item.appendChild(left); item.appendChild(checkbox); displaySection.appendChild(item); }); return displaySection; })(); const controlsSection = (function createControlsSection() { const controlsSection = document.createElement("div"); controlsSection.style.flex = "1"; controlsSection.addEventListener("input", e => { const target = e.target; if (target.classList.contains("font-size-slider")) { const input = target; const fontSizeValue = controlsSection.querySelector(".font-size-value"); fontSizeValue && (fontSizeValue.textContent = `${input.value}px`); localStorage.setItem("youtubeEnhancerFontSize", input.value); state.overlay && state.overlay.querySelectorAll(".subscribers-number,.views-number,.videos-number").forEach(el => { el.style.fontSize = `${input.value}px`; }); } if (target.classList.contains("interval-slider")) { const input = target; const newInterval = 1e3 * parseInt(input.value, 10); const intervalValue = controlsSection.querySelector(".interval-value"); intervalValue && (intervalValue.textContent = `${input.value}s`); state.updateInterval = newInterval; localStorage.setItem("youtubeEnhancerInterval", String(newInterval)); if (state.intervalId) { clearInterval(state.intervalId); state.intervalId = setInterval(() => { updateOverlayContent(state.overlay, state.currentChannelName); }, newInterval); YouTubeUtils.cleanupManager.registerInterval(state.intervalId); } } if (target.classList.contains("opacity-slider")) { const input = target; const newOpacity = parseInt(input.value, 10) / 100; const opacityValue = controlsSection.querySelector(".opacity-value"); opacityValue && (opacityValue.textContent = `${input.value}%`); state.overlayOpacity = newOpacity; localStorage.setItem("youtubeEnhancerOpacity", String(newOpacity)); state.overlay && (state.overlay.style.backgroundColor = `rgba(0, 0, 0, ${newOpacity})`); } }); const fontLabel = document.createElement("label"); fontLabel.textContent = t("fontFamily"); fontLabel.style.display = "block"; fontLabel.style.marginBottom = "5px"; fontLabel.style.fontSize = "16px"; fontLabel.style.fontWeight = "bold"; const 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"; const savedFontName = fonts.find(f => f.value === savedFont)?.name || "Rubik"; const fontSelect = document.createElement("select"); fontSelect.className = "font-family-select"; fontSelect.style.display = "none"; fonts.forEach(f => { const opt = document.createElement("option"); opt.value = f.value; opt.textContent = f.name; f.value === savedFont && (opt.selected = !0); fontSelect.appendChild(opt); }); const fontDropdown = document.createElement("div"); fontDropdown.className = "glass-dropdown"; fontDropdown.id = "stats-font-dropdown"; fontDropdown.tabIndex = 0; fontDropdown.setAttribute("role", "listbox"); fontDropdown.setAttribute("aria-expanded", "false"); fontDropdown.style.marginBottom = "12px"; fontDropdown.innerHTML = _createHTML(`\n <button class="glass-dropdown__toggle" type="button" aria-haspopup="listbox">\n <span class="glass-dropdown__label">${savedFontName}</span>\n <svg class="glass-dropdown__chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>\n </button>\n <ul class="glass-dropdown__list" role="presentation">\n ${fonts.map(f => { const sel = f.value === savedFont ? ' aria-selected="true"' : ""; return `<li class="glass-dropdown__item" data-value="${f.value}" role="option"${sel}>${f.name}</li>`; }).join("")}\n </ul>\n `); const initFontDropdown = () => { const toggle = fontDropdown.querySelector(".glass-dropdown__toggle"); const list = fontDropdown.querySelector(".glass-dropdown__list"); const label = fontDropdown.querySelector(".glass-dropdown__label"); const closeList = () => { fontDropdown.setAttribute("aria-expanded", "false"); list && (list.style.display = "none"); }; const openList = () => { fontDropdown.setAttribute("aria-expanded", "true"); list && (list.style.display = "block"); }; closeList(); toggle && toggle.addEventListener("click", e => { e.stopPropagation(); const expanded = "true" === fontDropdown.getAttribute("aria-expanded"); expanded ? closeList() : openList(); }); const _docClickHandler = e => { fontDropdown.contains(e.target) || closeList(); }; if (window.YouTubeUtils?.cleanupManager?.registerListener) { window.YouTubeUtils.cleanupManager.registerListener(document, "click", _docClickHandler); } else { document.addEventListener("click", _docClickHandler); state?.documentListenerKeys && state.documentListenerKeys.add("_docClickHandler"); } list && list.addEventListener("click", e => { const it = e.target.closest(".glass-dropdown__item"); if (!it) { return; } const val = it.dataset.value; fontDropdown.querySelectorAll(".glass-dropdown__item").forEach(i => i.removeAttribute("aria-selected")); it.setAttribute("aria-selected", "true"); label && (label.textContent = it.textContent); fontSelect.value = val; closeList(); localStorage.setItem("youtubeEnhancerFontFamily", val); state.overlay && state.overlay.querySelectorAll(".subscribers-number,.views-number,.videos-number").forEach(el => { el.style.fontFamily = val; }); }); }; ("function" == typeof queueMicrotask ? queueMicrotask : fn => Promise.resolve().then(fn))(initFontDropdown); const fontSizeLabel = document.createElement("label"); fontSizeLabel.textContent = t("fontSize"); fontSizeLabel.style.display = "block"; fontSizeLabel.style.marginBottom = "5px"; fontSizeLabel.style.fontSize = "16px"; fontSizeLabel.style.fontWeight = "bold"; const fontSizeSlider = document.createElement("input"); fontSizeSlider.type = "range"; fontSizeSlider.min = "16"; fontSizeSlider.max = "72"; fontSizeSlider.value = localStorage.getItem("youtubeEnhancerFontSize") || "24"; fontSizeSlider.step = "1"; fontSizeSlider.className = "font-size-slider"; const fontSizeValue = document.createElement("div"); fontSizeValue.className = "font-size-value"; fontSizeValue.textContent = `${fontSizeSlider.value}px`; fontSizeValue.style.fontSize = "14px"; fontSizeValue.style.marginBottom = "15px"; const intervalLabel = document.createElement("label"); intervalLabel.textContent = t("updateInterval"); intervalLabel.style.display = "block"; intervalLabel.style.marginBottom = "5px"; intervalLabel.style.fontSize = "16px"; intervalLabel.style.fontWeight = "bold"; const intervalSlider = document.createElement("input"); intervalSlider.type = "range"; intervalSlider.min = "2"; intervalSlider.max = "10"; intervalSlider.value = String(state.updateInterval / 1e3); 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"; const opacityLabel = document.createElement("label"); opacityLabel.textContent = t("backgroundOpacity"); opacityLabel.style.display = "block"; opacityLabel.style.marginBottom = "5px"; opacityLabel.style.fontSize = "16px"; opacityLabel.style.fontWeight = "bold"; const opacitySlider = document.createElement("input"); opacitySlider.type = "range"; opacitySlider.min = "50"; opacitySlider.max = "90"; opacitySlider.value = String(100 * state.overlayOpacity); opacitySlider.step = "5"; opacitySlider.className = "opacity-slider"; const opacityValue = document.createElement("div"); opacityValue.className = "opacity-value"; opacityValue.textContent = `${opacitySlider.value}%`; opacityValue.style.fontSize = "14px"; controlsSection.appendChild(fontLabel); controlsSection.appendChild(fontSelect); controlsSection.appendChild(fontDropdown); 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; })(); menu.appendChild(displaySection); menu.appendChild(controlsSection); return menu; } 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 = (function createNumberContainer() { const container = document.createElement("div"); Object.assign(container.style, { display: "flex", justifyContent: "center", alignItems: "center", letterSpacing: "0.025em" }); return container; })(); 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 = (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; })(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 setupSettingsButton() { const button = (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; })(); button.setAttribute("tabindex", "0"); button.setAttribute("aria-label", t("settingsAriaLabel")); button.setAttribute("role", "button"); return button; } function createOverlay(bannerElement) { clearExistingOverlay(); if (!bannerElement) { return null; } const overlay = (function createOverlayElement() { const overlay = document.createElement("div"); overlay.classList.add("channel-banner-overlay"); Object.assign(overlay.style, { position: "absolute", top: "0", left: "0", width: "100%", height: "100%", backgroundColor: `rgba(0, 0, 0, ${state.overlayOpacity})`, borderRadius: "15px", zIndex: "10", display: "flex", justifyContent: "space-around", alignItems: "center", color: "white", fontFamily: localStorage.getItem("youtubeEnhancerFontFamily") || "Rubik, sans-serif", fontSize: `${localStorage.getItem("youtubeEnhancerFontSize") || "24"}px`, boxSizing: "border-box", transition: "background-color 0.3s ease" }); return overlay; })(); !(function applyOverlayAccessibility(overlay) { overlay.setAttribute("role", "region"); overlay.setAttribute("aria-label", t("overlayAriaLabel")); overlay.setAttribute("tabindex", "-1"); })(overlay); !(function applyMobileResponsiveness(overlay) { if (window.innerWidth <= 768) { overlay.style.flexDirection = "column"; overlay.style.padding = "10px"; overlay.style.minHeight = "200px"; } })(overlay); const settingsButton = setupSettingsButton(); const settingsMenu = (function setupSettingsMenu() { const menu = createSettingsMenu(); menu.setAttribute("aria-label", t("settingsMenuAriaLabel")); menu.setAttribute("role", "dialog"); return menu; })(); overlay.appendChild(settingsButton); overlay.appendChild(settingsMenu); !(function attachMenuEventHandlers(settingsButton, settingsMenu) { const toggleMenu = show => { settingsMenu.classList.toggle("show", show); settingsButton.setAttribute("aria-expanded", show); show && settingsMenu.focus(); }; settingsButton.addEventListener("click", e => { e.stopPropagation(); toggleMenu(!settingsMenu.classList.contains("show")); }); settingsButton.addEventListener("keydown", e => { if ("Enter" === e.key || " " === e.key) { e.preventDefault(); toggleMenu(!settingsMenu.classList.contains("show")); } }); const clickKey = YouTubeUtils.cleanupManager.registerListener(document, "click", e => { const node = e.target; settingsMenu.contains(node) || settingsButton.contains(node) || toggleMenu(!1); }); const keyKey = YouTubeUtils.cleanupManager.registerListener(document, "keydown", e => { if ("Escape" === e.key && settingsMenu.classList.contains("show")) { toggleMenu(!1); settingsButton.focus(); } }); state.documentListenerKeys.add(clickKey); state.documentListenerKeys.add(keyKey); })(settingsButton, settingsMenu); const spinner = (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 50 50"); spinner.classList.add("stats-spinner"); const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", "25"); circle.setAttribute("cy", "25"); circle.setAttribute("r", "20"); circle.setAttribute("fill", "none"); circle.setAttribute("stroke", "currentColor"); circle.setAttribute("stroke-width", "4"); spinner.appendChild(circle); spinnerContainer.appendChild(spinner); return spinnerContainer; })(); overlay.appendChild(spinner); !(function addStatContainers(overlay) { const subscribersElement = createStatContainer("subscribers", "M144 160c-44.2 0-80-35.8-80-80S99.8 0 144 0s80 35.8 80 80s-35.8 80-80 80zm368 0c-44.2 0-80-35.8-80-80s35.8-80 80-80s80 35.8 80 80s-35.8 80-80 80zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM416 224c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z"); const viewsElement = createStatContainer("views", "M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"); const videosElement = createStatContainer("videos", "M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z"); overlay.appendChild(subscribersElement); overlay.appendChild(viewsElement); overlay.appendChild(videosElement); })(overlay); bannerElement.appendChild(overlay); updateDisplayState(); return overlay; } async function fetchChannelStats(channelId) { const helpers = "undefined" != typeof window && window.YouTubePlusChannelStatsHelpers ? window.YouTubePlusChannelStatsHelpers : null; if (!helpers) { utils.error("Channel stats helpers not loaded"); return { followerCount: 0, bottomOdos: [ 0, 0 ], error: !0, timestamp: Date.now() }; } try { const fetchFn = () => (function fetchWithGM(url, headers = {}) { const requestHeaders = { Accept: "application/json", ...headers }; const gm = window.GM_xmlhttpRequest; if ("function" == typeof gm) { return new Promise((resolve, reject) => { gm({ method: "GET", url, headers: requestHeaders, timeout: 1e4, 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; }); })(`${STATS_API_URL}${channelId}`, { origin: "https://livecounts.io", referer: "https://livecounts.io/" }); const stats = await helpers.fetchWithRetry(fetchFn, CONFIG.MAX_RETRIES, utils); if (stats) { helpers.cacheStats(state.lastSuccessfulStats, channelId, stats); return stats; } const cachedStats = helpers.getCachedStats(state.lastSuccessfulStats, channelId, CONFIG.CACHE_DURATION, utils); if (cachedStats) { return cachedStats; } const fallbackCount = helpers.extractSubscriberCountFromPage(); fallbackCount > 0 && utils.log("Extracted fallback subscriber count:", fallbackCount); return helpers.createFallbackStats(fallbackCount); } catch (error) { utils.error("Failed to fetch channel stats:", error); return helpers.createFallbackStats(0); } } function clearExistingOverlay() { const existingOverlay = $(".channel-banner-overlay"); if (existingOverlay) { try { existingOverlay.remove(); } catch { console.warn("[YouTube+] Failed to remove overlay"); } } if (state.intervalId) { try { clearInterval(state.intervalId); YouTubeUtils.cleanupManager.unregisterInterval(state.intervalId); } catch { console.warn("[YouTube+] Failed to clear interval"); } state.intervalId = null; } if (state.documentListenerKeys && state.documentListenerKeys.size) { state.documentListenerKeys.forEach(key => { try { YouTubeUtils.cleanupManager.unregisterListener(key); } catch { console.warn("[YouTube+] Failed to unregister listener"); } }); state.documentListenerKeys.clear(); } state.lastSuccessfulStats && state.lastSuccessfulStats.clear(); state.previousStats && state.previousStats.clear(); state.currentChannelId = null; state.isUpdating = !1; 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 updateDigits(container, newValue) { const newValueStr = newValue.toString(); const digitGroups = (function splitIntoDigitGroups(valueStr) { const digits = []; for (let i = valueStr.length - 1; i >= 0; i -= 3) { const start = Math.max(0, i - 2); digits.unshift(valueStr.slice(start, i + 1)); } return digits; })(newValueStr); !(function clearContainer(container) { for (;container.firstChild; ) { container.removeChild(container.firstChild); } })(container); !(function renderDigitGroups(container, digitGroups) { for (let i = 0; i < digitGroups.length; i++) { const group = digitGroups[i]; for (let j = 0; j < group.length; j++) { const digitElement = createDigitElement(); digitElement.textContent = group[j]; container.appendChild(digitElement); } i < digitGroups.length - 1 && container.appendChild(createCommaElement()); } })(container, digitGroups); !(function animateDigitChanges(container, digitGroups) { let elementIndex = 0; for (let i = 0; i < digitGroups.length; i++) { const group = digitGroups[i]; for (let j = 0; j < group.length; j++) { const digitElement = container.children[elementIndex]; const newDigit = parseInt(group[j], 10); const currentDigit = parseInt(digitElement.textContent || "0", 10); currentDigit !== newDigit && animateDigit(digitElement, currentDigit, newDigit); elementIndex++; } i < digitGroups.length - 1 && elementIndex++; } })(container, digitGroups); } function animateDigit(element, start, end) { const startTime = performance.now(); requestAnimationFrame(function update(currentTime) { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / 1e3, 1); const easeOutQuart = 1 - Math.pow(1 - progress, 4); const current = Math.round(start + (end - start) * easeOutQuart); element.textContent = current; progress < 1 && requestAnimationFrame(update); }); } function updateDisplayState() { const overlay = $(".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 = "false" !== localStorage.getItem(`show-${type}`); if (isVisible) { container.style.display = "flex"; visibleCount++; visibleContainers.push(container); } else { container.style.display = "none"; } }); visibleContainers.forEach(container => { container.style.width = ""; container.style.margin = ""; switch (visibleCount) { case 1: container.style.width = "100%"; break; case 2: container.style.width = "50%"; break; case 3: container.style.width = "33.33%"; break; default: container.style.display = "none"; } }); const fontSize = localStorage.getItem("youtubeEnhancerFontSize") || "24"; const fontFamily = localStorage.getItem("youtubeEnhancerFontFamily") || "Rubik, sans-serif"; overlay.querySelectorAll(".subscribers-number,.views-number,.videos-number").forEach(el => { el.style.fontSize = `${fontSize}px`; el.style.fontFamily = fontFamily; }); overlay.style.display = "flex"; } function updateStatElement(overlay, channelId, className, value, label) { const numberContainer = overlay.querySelector(`.${className}-number`); const differenceElement = overlay.querySelector(`.${className}-difference`); const labelElement = overlay.querySelector(`.${className}-label`); numberContainer && updateDigits(numberContainer, value); if (differenceElement && state.previousStats.has(channelId)) { const previousValue = (function getPreviousStatValue(channelId, className) { const prevStats = state.previousStats.get(channelId); if (!prevStats) { return null; } if ("subscribers" === className) { return prevStats.followerCount; } const index = "views" === className ? 0 : 1; return prevStats.bottomOdos[index]; })(channelId, className); null !== previousValue && (function updateDifferenceElement(element, currentValue, previousValue) { if (!previousValue) { return; } const difference = currentValue - previousValue; if (0 === difference) { element.textContent = ""; return; } const sign = difference > 0 ? "+" : ""; element.textContent = `${sign}${difference.toLocaleString()}`; element.style.color = difference > 0 ? "#1ed760" : "#f3727f"; setTimeout(() => { element.textContent = ""; }, 1e3); })(differenceElement, value, previousValue); } labelElement && (labelElement.textContent = label); } async function updateOverlayContent(overlay, channelName) { if ((function shouldUpdateOverlay(channelName) { return !state.isUpdating && channelName === state.currentChannelName; })(channelName) && overlay && overlay.isConnected && "hidden" !== document.visibilityState) { state.isUpdating = !0; try { const channelId = await (async function fetchChannelId(channelName) { const cacheKey = channelName || state.currentChannelName || window.location.pathname; if (cacheKey && state.channelIdCache.has(cacheKey)) { return state.channelIdCache.get(cacheKey); } if (state.currentChannelId) { return state.currentChannelId; } if ("string" == typeof channelName && /^UC[\w-]{22}$/.test(channelName)) { state.currentChannelId = channelName; cacheKey && boundedCacheSet(state.channelIdCache, cacheKey, channelName); return channelName; } const metaTag = $('meta[itemprop="channelId"]'); if (metaTag && metaTag.content) { state.currentChannelId = metaTag.content; cacheKey && boundedCacheSet(state.channelIdCache, cacheKey, metaTag.content); return metaTag.content; } const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/); if (urlMatch && urlMatch[1]) { state.currentChannelId = urlMatch[1]; cacheKey && boundedCacheSet(state.channelIdCache, cacheKey, urlMatch[1]); return urlMatch[1]; } const channelInfo = await getChannelInfo(window.location.href); if (channelInfo && channelInfo.channelId) { state.currentChannelId = channelInfo.channelId; cacheKey && boundedCacheSet(state.channelIdCache, cacheKey, channelInfo.channelId); return channelInfo.channelId; } return null; })(channelName); if (!channelId) { const now = Date.now(); if (now - state.lastChannelIdWarnAt > 15e3) { state.lastChannelIdWarnAt = now; utils.warn("Skipping overlay update: channel ID is not available yet"); } return; } state.currentChannelId = channelId; const stats = await fetchChannelStats(channelId); if (channelName !== state.currentChannelName) { return; } if (stats.error) { !(function handleStatsError(overlay, stats) { const containers = overlay.querySelectorAll('[class$="-number"]'); containers.forEach(container => { container.classList.contains("subscribers-number") && stats.followerCount > 0 ? updateDigits(container, stats.followerCount) : container.textContent = "---"; }); utils.warn("Using fallback stats due to API error"); })(overlay, stats); return; } !(function updateAllStatElements(overlay, channelId, stats) { updateStatElement(overlay, channelId, "subscribers", stats.followerCount, t("subscribers")); updateStatElement(overlay, channelId, "views", stats.bottomOdos[0], t("views")); updateStatElement(overlay, channelId, "videos", stats.bottomOdos[1], t("videos")); })(overlay, channelId, stats); if (!state.previousStats.has(channelId)) { !(function showContent(overlay) { const spinnerContainer = overlay.querySelector(".spinner-container"); 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"; }); })(overlay); utils.log("Displayed initial stats for channel:", channelName); } state.previousStats.set(channelId, stats); } catch (error) { utils.error("Failed to update overlay content:", error); !(function showOverlayError(overlay) { const containers = overlay.querySelectorAll('[class$="-number"]'); containers.forEach(container => { container.textContent = "---"; }); })(overlay); } finally { state.isUpdating = !1; } } } let ensureSettingsScheduler = null; function ensureSettingsUI() { ensureSettingsScheduler && ensureSettingsScheduler.stop(); ensureSettingsScheduler = (function createSafeRetryScheduler(opts) { const factory = window.YouTubeUtils?.createRetryScheduler; if ("function" == typeof factory) { try { return factory(opts); } catch (error) { utils.error("Retry scheduler factory failed:", error); } } const {check, maxAttempts = 20, interval = 100} = opts || {}; let attempts = 0; let timerId = null; let stopped = !1; const tick = () => { if (!stopped) { attempts += 1; try { if ("function" == typeof check && check()) { stopped = !0; return; } } catch (error) { utils.error("Fallback retry check failed:", error); } attempts >= maxAttempts ? stopped = !0 : timerId = setTimeout(tick, interval); } }; timerId = setTimeout(tick, 0); return { stop() { stopped = !0; timerId && clearTimeout(timerId); timerId = null; } }; })({ check: () => (function addSettingsUI() { const section = $('.ytp-plus-settings-section[data-section="experimental"]'); if (!section) { return !1; } const existingItem = section.querySelector(".count-settings-item"); if (existingItem) { const label = existingItem.querySelector(".ytp-plus-settings-item-label"); const description = existingItem.querySelector(".ytp-plus-settings-item-description"); label && (label.textContent = t("channelStatsTitle")); description && (description.textContent = t("channelStatsDescription")); return !0; } const item = document.createElement("div"); item.className = "ytp-plus-settings-item count-settings-item"; item.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label">${t("channelStatsTitle")}</label>\n <div class="ytp-plus-settings-item-description">${t("channelStatsDescription")}</div>\n </div>\n <input type="checkbox" class="ytp-plus-settings-checkbox" ${state.enabled ? "checked" : ""}>\n `); section.appendChild(item); item.querySelector("input")?.addEventListener("change", e => { const {target} = e; const input = target; state.enabled = input.checked; localStorage.setItem(CONFIG.STORAGE_KEY, state.enabled ? "true" : "false"); if (state.enabled) { observePageChanges(); addNavigationListener(); setTimeout(() => { const bannerElement = byId("page-header-banner-sizer"); bannerElement && isChannelPage() && addOverlay(bannerElement); }, 100); } else { clearExistingOverlay(); } }); return !0; })(), maxAttempts: 20, interval: 100 }); } if (_cm2?.registerListener) { _cm2.registerListener(document, "youtube-plus-settings-modal-opened", () => { ensureSettingsUI(); }); } else { const _handler = () => { ensureSettingsUI(); }; document.addEventListener("youtube-plus-settings-modal-opened", _handler); try { window.YouTubeUtils?.cleanupManager?.register?.(() => document.removeEventListener("youtube-plus-settings-modal-opened", _handler)); } catch {} } const experimentalNavClickHandler = e => { const {target} = e; const el = target; const navItem = el?.closest?.(".ytp-plus-settings-nav-item"); "experimental" === navItem?.dataset?.section && ensureSettingsUI(); }; if (_cm2?.registerListener) { _cm2.registerListener(document, "youtube-plus-language-changed", () => { ensureSettingsUI(); }); } else { const _langHandler = () => { ensureSettingsUI(); }; document.addEventListener("youtube-plus-language-changed", _langHandler); try { window.YouTubeUtils?.cleanupManager?.register?.(() => document.removeEventListener("youtube-plus-language-changed", _langHandler)); } catch {} } if (_cm2?.registerListener) { const listenerKey = _cm2.registerListener(document, "click", experimentalNavClickHandler, !0); state.documentListenerKeys.add(listenerKey); } else { document.addEventListener("click", experimentalNavClickHandler, !0); try { window.YouTubeUtils?.cleanupManager?.register?.(() => document.removeEventListener("click", experimentalNavClickHandler, !0)); } catch {} } function setupUpdateInterval(overlay, channelName) { state.intervalId && clearInterval(state.intervalId); const debouncedUpdate = (function createDebouncedUpdate(overlay, channelName) { let lastUpdateTime = 0; return () => { if (!overlay || !overlay.isConnected) { return; } if ("hidden" === document.visibilityState) { return; } const now = Date.now(); if (now - lastUpdateTime >= state.updateInterval - 100) { updateOverlayContent(overlay, channelName); lastUpdateTime = now; } }; })(overlay, channelName); state.intervalId = setInterval(() => { "hidden" !== document.visibilityState && debouncedUpdate(); }, state.updateInterval); YouTubeUtils.cleanupManager.registerInterval(state.intervalId); } function addOverlay(bannerElement) { const channelName = (function extractChannelName(pathname) { return pathname.startsWith("/@") ? pathname.split("/")[1].replace("@", "") : pathname.startsWith("/channel/") || pathname.startsWith("/c/") || pathname.startsWith("/user/") ? pathname.split("/")[2] : null; })(window.location.pathname); if (!(function shouldSkipOverlay(channelName) { return !channelName || channelName === state.currentChannelName && state.overlay; })(channelName)) { !(function ensureBannerPosition(bannerElement) { bannerElement && !bannerElement.style.position && (bannerElement.style.position = "relative"); })(bannerElement); state.currentChannelName = channelName; state.overlay = createOverlay(bannerElement); if (state.overlay) { !(function clearUpdateInterval() { if (state.intervalId) { clearInterval(state.intervalId); state.intervalId = null; } })(); setupUpdateInterval(state.overlay, channelName); updateOverlayContent(state.overlay, channelName); utils.log("Added overlay for channel:", channelName); } } } function isChannelPage() { return window.location.pathname.startsWith("/@") || window.location.pathname.startsWith("/channel/") || window.location.pathname.startsWith("/c/"); } function handleBannerUpdate() { const bannerElement = (function findBannerElement() { let bannerElement = byId("page-header-banner-sizer"); if (!bannerElement) { const alternatives = [ '[id*="banner"]', ".ytd-c4-tabbed-header-renderer", "#channel-header", ".channel-header" ]; for (const selector of alternatives) { bannerElement = $(selector); if (bannerElement) { break; } } } return bannerElement; })(); if (bannerElement && isChannelPage()) { !(function ensureBannerPositioning(bannerElement) { "relative" !== bannerElement.style.position && (bannerElement.style.position = "relative"); })(bannerElement); addOverlay(bannerElement); } else if (!isChannelPage()) { clearExistingOverlay(); state.currentChannelName = null; } } function observePageChanges() { if (!state.enabled) { return; } const debouncedBannerUpdate = YouTubeUtils.debounce ? YouTubeUtils.debounce(handleBannerUpdate, 150) : handleBannerUpdate; if (_cm2?.registerListener) { _cm2.registerListener(document, "yt-navigate-finish", debouncedBannerUpdate); _cm2.registerListener(document, "yt-page-data-updated", debouncedBannerUpdate); } else { document.addEventListener("yt-navigate-finish", debouncedBannerUpdate); document.addEventListener("yt-page-data-updated", debouncedBannerUpdate); } } function addNavigationListener() { if (!state.enabled) { return; } const _navHandler = () => { if (isChannelPage()) { const bannerElement = byId("page-header-banner-sizer"); if (bannerElement) { addOverlay(bannerElement); utils.log("Navigated to channel page"); } } else { clearExistingOverlay(); state.currentChannelName = null; utils.log("Navigated away from channel page"); } }; _cm2?.registerListener ? _cm2.registerListener(window, "yt-navigate-finish", _navHandler) : window.addEventListener("yt-navigate-finish", _navHandler); } function cleanup() { if (state.observers && Array.isArray(state.observers)) { state.observers.forEach(observer => { try { observer.disconnect(); } catch (e) { console.warn("[YouTube+] Failed to disconnect observer:", e); } }); state.observers = []; } clearExistingOverlay(); utils.log("Cleanup completed"); } _cm2?.registerListener ? _cm2.registerListener(window, "beforeunload", cleanup) : window.addEventListener("beforeunload", cleanup); "undefined" != typeof window && (window.YouTubeStats = { init, cleanup, version: "2.4.5" }); init(); }; window.YouTubePlusLazyLoader ? window.YouTubePlusLazyLoader.register("channel-stats", initChannelStats, { priority: 1 }) : initChannelStats(); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const t = window.YouTubeUtils?.t || (key => key || ""); const CONFIG = { selectors: { deleteButtons: 'div[class^="VfPpkd-Bz112c-"], button[aria-label*="Delete"], button[aria-label*="Удалить"], button[aria-label*="Remove"]', 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: !0, storageKey: "youtube_comment_manager_settings" }; const state = { observer: null, isProcessing: !1, settingsNavListenerKey: null, panelCollapsed: !1, initialized: !1 }; const COMMENT_HISTORY_URL = (() => { let lang = "en"; try { window.YouTubePlusI18n?.getLanguage ? lang = window.YouTubePlusI18n.getLanguage() : document.documentElement.lang && (lang = document.documentElement.lang.split("-")[0]); } catch {} return `https://myactivity.google.com/page?hl=${encodeURIComponent(lang)}&utm_medium=web&utm_source=youtube&page=youtube_comments`; })(); const settings_load = () => { try { const saved = localStorage.getItem(CONFIG.storageKey); saved && (CONFIG.enabled = JSON.parse(saved).enabled ?? !0); } catch {} }; const debounce = window.YouTubeUtils?.debounce || ((fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }); const $ = sel => window.YouTubeUtils?.$(sel) || document.querySelector(sel); const $$ = sel => window.YouTubeUtils?.$$(sel) || Array.from(document.querySelectorAll(sel)); const withErrorBoundary = (fn, context) => window.YouTubeErrorBoundary?.withErrorBoundary ? window.YouTubeErrorBoundary.withErrorBoundary(fn, "CommentManager") : (...args) => { try { return fn(...args); } catch (e) { ((context, error) => { const errorObj = error instanceof Error ? error : new Error(String(error)); window.YouTubeErrorBoundary ? window.YouTubeErrorBoundary.logError(errorObj, { context }) : console.error(`[YouTube+][CommentManager] ${context}:`, error); })(context, e); return null; } }; const addCheckboxes = withErrorBoundary(() => { if (!CONFIG.enabled || state.isProcessing) { return; } const deleteButtons = $$(CONFIG.selectors.deleteButtons); deleteButtons.forEach(button => { const parent = button.parentNode; if (button.closest(CONFIG.selectors.menuButton) || parent && parent.querySelector && parent.querySelector(`.${CONFIG.classes.checkbox}`)) { return; } const commentElement = button.closest('[class*="comment"]') || button.closest('[role="article"]') || parent; commentElement && commentElement instanceof Element && (commentElement.hasAttribute("data-comment-text") || commentElement.setAttribute("data-comment-text", (commentElement.textContent || "").toLowerCase())); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.className = `${CONFIG.classes.checkbox} ytp-plus-settings-checkbox`; checkbox.setAttribute("aria-label", t("selectComment")); checkbox.addEventListener("change", updateDeleteButtonState); checkbox.addEventListener("click", e => e.stopPropagation()); const dateElement = commentElement && commentElement.querySelector ? commentElement.querySelector('[class*="date"],[class*="time"],time,[title*="20"],[aria-label*="ago"]') : null; if (dateElement && dateElement instanceof Element) { dateElement.classList.add(CONFIG.classes.checkboxAnchor); checkbox.classList.add(CONFIG.classes.checkboxFloating); dateElement.appendChild(checkbox); } else { parent && parent.insertBefore && parent.insertBefore(checkbox, button); } }); }, "addCheckboxes"); const addControlButtons = withErrorBoundary(() => { if (!CONFIG.enabled || $(`.${CONFIG.classes.container}`)) { return; } const deleteButtons = $$(CONFIG.selectors.deleteButtons); if (!deleteButtons.length) { return; } const first = deleteButtons[0]; const container = first && first.parentNode && first.parentNode.parentNode; if (!(container && container instanceof Element)) { return; } const panel = document.createElement("div"); panel.className = `${CONFIG.classes.container} ${CONFIG.classes.panel} glass-panel`; panel.setAttribute("role", "region"); panel.setAttribute("aria-label", t("commentManagerControls")); const header = document.createElement("div"); header.className = CONFIG.classes.header; const title = document.createElement("div"); title.className = CONFIG.classes.title; title.textContent = t("commentManager"); const collapseButton = document.createElement("button"); collapseButton.className = `${CONFIG.classes.close} ytp-plus-settings-close`; collapseButton.setAttribute("type", "button"); collapseButton.setAttribute("aria-expanded", String(!state.panelCollapsed)); collapseButton.setAttribute("aria-label", t("togglePanel")); collapseButton.innerHTML = _createHTML('\n <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\n </svg>\n '); 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}`; options.id && (button.id = options.id); options.disabled && (button.disabled = !0); button.addEventListener("click", onClick); return button; }; const deleteAllButton = createActionButton(t("deleteSelected"), `${CONFIG.classes.buttonDanger} ${CONFIG.classes.deleteButton}`, deleteSelectedComments, { disabled: !0 }); const selectAllButton = createActionButton(t("selectAll"), CONFIG.classes.buttonPrimary, () => { $$(`.${CONFIG.classes.checkbox}`).forEach(cb => cb.checked = !0); updateDeleteButtonState(); }); const clearAllButton = createActionButton(t("clearAll"), CONFIG.classes.buttonSuccess, () => { $$(`.${CONFIG.classes.checkbox}`).forEach(cb => cb.checked = !1); updateDeleteButtonState(); }); actions.append(deleteAllButton, selectAllButton, clearAllButton); togglePanelState(state.panelCollapsed); panel.append(header, actions); const refNode = deleteButtons[0] && deleteButtons[0].parentNode; refNode && refNode.parentNode ? container.insertBefore(panel, refNode) : container.appendChild(panel); }, "addControlButtons"); const updateDeleteButtonState = withErrorBoundary(() => { const deleteAllButton = $(`.${CONFIG.classes.deleteButton}`); if (!deleteAllButton) { return; } const hasChecked = Array.from($$(`.${CONFIG.classes.checkbox}`)).some(cb => cb.checked); deleteAllButton.disabled = !hasChecked; deleteAllButton.style.opacity = hasChecked ? "1" : "0.6"; }, "updateDeleteButtonState"); const deleteSelectedComments = withErrorBoundary(() => { const checkedBoxes = Array.from($$(`.${CONFIG.classes.checkbox}`)).filter(cb => cb.checked); if (checkedBoxes.length && confirm(`Delete ${checkedBoxes.length} comment(s)?`)) { state.isProcessing = !0; checkedBoxes.forEach((checkbox, index) => { setTimeout(() => { const deleteButton = checkbox.nextElementSibling || checkbox.parentNode.querySelector(CONFIG.selectors.deleteButtons); deleteButton?.click(); }, index * CONFIG.deleteDelay); }); setTimeout(() => state.isProcessing = !1, checkedBoxes.length * CONFIG.deleteDelay + 1e3); } }, "deleteSelectedComments"); const cleanup = withErrorBoundary(() => { $$(`.${CONFIG.classes.checkbox}`).forEach(el => el.remove()); $(`.${CONFIG.classes.container}`)?.remove(); }, "cleanup"); const initializeScript = withErrorBoundary(() => { if (CONFIG.enabled) { addCheckboxes(); addControlButtons(); updateDeleteButtonState(); } else { cleanup(); } }, "initializeScript"); const addStyles = withErrorBoundary(() => { if ($("#comment-delete-styles")) { return; } const styles = `\n .${CONFIG.classes.checkboxAnchor}{position:relative;display:inline-flex;align-items:center;gap:8px;width:auto;}\n .${CONFIG.classes.checkboxFloating}{position:absolute;top:-4px;right:-32px;margin:0;}\n /* Panel styled to match shorts feedback: glassmorphism, rounded corners, soft shadow */\n .${CONFIG.classes.panel}{position:fixed;top:50%;right:24px;transform:translateY(-50%);display:flex;flex-direction:column;gap:14px;z-index:10000;padding:16px 18px;background:var(--yt-glass-bg);border:1.5px solid var(--yt-glass-border);border-radius:20px;box-shadow:0 12px 40px rgba(0,0,0,0.45);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);min-width:220px;max-width:300px;color:var(--yt-text-primary);transition:transform .22s cubic-bezier(.4,0,.2,1),opacity .22s,box-shadow .2s}\n html:not([dark]) .${CONFIG.classes.panel}{background:var(--yt-glass-bg);}\n .${CONFIG.classes.header}{display:flex;align-items:center;justify-content:space-between;gap:12px;}\n .${CONFIG.classes.panel}.is-collapsed{padding:14px 18px;}\n .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.title}{font-weight:500;opacity:.85;}\n .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.close}{transform:rotate(45deg);}\n .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.actions}{display:none!important;}\n .${CONFIG.classes.title}{font-size:15px;font-weight:600;letter-spacing:.3px;}\n .${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;}\n .${CONFIG.classes.close}:hover{transform:rotate(90deg) scale(1.05);color:var(--yt-accent);}\n .${CONFIG.classes.actions}{display:flex;flex-direction:column;gap:10px;}\n .${CONFIG.classes.actions}.is-hidden{display:none!important;}\n .${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;}\n .${CONFIG.classes.button}:disabled{opacity:.5;cursor:not-allowed;}\n .${CONFIG.classes.button}:not(:disabled):hover{transform:translateY(-1px);box-shadow:var(--yt-shadow);}\n .${CONFIG.classes.buttonDanger}{background:rgba(255,99,71,.12);border-color:rgba(255,99,71,.25);color:#ff5c5c;}\n .${CONFIG.classes.buttonPrimary}{background:rgba(33,150,243,.12);border-color:rgba(33,150,243,.25);color:#2196f3;}\n .${CONFIG.classes.buttonSuccess}{background:rgba(76,175,80,.12);border-color:rgba(76,175,80,.25);color:#4caf50;}\n .${CONFIG.classes.buttonDanger}:not(:disabled):hover{background:rgba(255,99,71,.22);}\n .${CONFIG.classes.buttonPrimary}:not(:disabled):hover{background:rgba(33,150,243,.22);}\n .${CONFIG.classes.buttonSuccess}:not(:disabled):hover{background:rgba(76,175,80,.22);}\n @media(max-width:1280px){\n .${CONFIG.classes.panel}{top:auto;bottom:24px;transform:none;right:16px;}\n }\n @media(max-width:768px){\n .${CONFIG.classes.panel}{position:fixed;left:16px;right:16px;bottom:16px;top:auto;transform:none;max-width:none;}\n .${CONFIG.classes.actions}{flex-direction:row;flex-wrap:wrap;}\n .${CONFIG.classes.button}{flex:1;min-width:140px;}\n }\n `; (cssText => { try { if (window.YouTubeUtils && YouTubeUtils.StyleManager) { YouTubeUtils.StyleManager.add("comment-delete-styles", cssText); return; } } catch {} try { if (document.getElementById("comment-delete-styles")) { return; } const style = document.createElement("style"); style.id = "comment-delete-styles"; style.textContent = cssText; (document.head || document.documentElement).appendChild(style); } catch {} })(styles); }, "addStyles"); const addCommentManagerSettings = withErrorBoundary(() => { const experimentalSection = $('.ytp-plus-settings-section[data-section="experimental"]'); if (!experimentalSection) { return; } const existing = $(".comment-manager-settings-item"); if (existing) { try { experimentalSection.appendChild(existing); } catch {} return; } const settingsItem = document.createElement("div"); settingsItem.className = "ytp-plus-settings-item comment-manager-settings-item"; settingsItem.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label">${t("commentManagement")}</label>\n <div class="ytp-plus-settings-item-description">${t("bulkDeleteDescription")}</div>\n </div>\n <button class="ytp-plus-button" id="open-comment-history-page" style="margin:0 0 0 30px;padding:12px 16px;font-size:13px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2)">\n <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="gray" stroke-width="2">\n <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>\n <polyline points="15,3 21,3 21,9"/>\n <line x1="10" y1="14" x2="21" y2="3"/>\n </svg>\n </button>\n `); experimentalSection.appendChild(settingsItem); $("#open-comment-history-page").addEventListener("click", () => { window.open(COMMENT_HISTORY_URL, "_blank"); }); }, "addCommentManagerSettings"); const ensureCommentManagerSettings = (attempt = 0) => { const experimentalVisible = $('.ytp-plus-settings-section[data-section="experimental"]:not(.hidden)'); if (experimentalVisible) { addCommentManagerSettings(); !$(".comment-manager-settings-item") && attempt < 20 && setTimeout(() => ensureCommentManagerSettings(attempt + 1), 80); } else { attempt < 20 && setTimeout(() => ensureCommentManagerSettings(attempt + 1), 80); } }; const init = withErrorBoundary(() => { if (state.initialized && state.observer) { return; } settings_load(); addStyles(); state.observer?.disconnect(); state.observer = new MutationObserver(debounce(initializeScript, CONFIG.debounceDelay)); (observer => { try { window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.registerObserver(observer); } catch {} })(state.observer); const observeTarget = () => { const target = document.querySelector("#comments") || document.querySelector("#content") || document.body; state.observer.observe(target, { childList: !0, subtree: !0 }); }; document.body ? observeTarget() : document.addEventListener("DOMContentLoaded", observeTarget); window.addEventListener("yt-navigate-finish", () => { state.observer.disconnect(); setTimeout(observeTarget, 200); }, { passive: !0 }); "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", initializeScript) : initializeScript(); document.addEventListener("youtube-plus-settings-modal-opened", () => { setTimeout(() => ensureCommentManagerSettings(), 100); }); const handleExperimentalNavClick = e => { const target = e.target; const navItem = target?.closest?.(".ytp-plus-settings-nav-item"); "experimental" === navItem?.dataset?.section && setTimeout(() => ensureCommentManagerSettings(), 50); }; state.settingsNavListenerKey || (state.settingsNavListenerKey = ((target, event, handler, options) => { try { if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { return YouTubeUtils.cleanupManager.registerListener(target, event, handler, options); } } catch {} try { target.addEventListener(event, handler, options); } catch {} return null; })(document, "click", handleExperimentalNavClick, { passive: !0, capture: !0 })); }, "init"); const isRelevantRoute = () => { if ((() => { try { const host = location.hostname || ""; if (!host.includes("myactivity.google.com")) { return !1; } const params = new URLSearchParams(location.search || ""); return "youtube_comments" === params.get("page"); } catch { return !1; } })()) { return !0; } const path = location.pathname; return "/watch" === path || path.startsWith("/shorts/") || path.startsWith("/@") || path.startsWith("/channel/"); }; const scheduleInit = () => { !state.initialized && isRelevantRoute() && requestIdleCallback(() => { if (!state.initialized && isRelevantRoute()) { init(); state.initialized = !0; } }, { timeout: 2e3 }); }; const navigationObserver = new MutationObserver(debounce(() => { !state.initialized && isRelevantRoute() && scheduleInit(); if (state.initialized) { navigationObserver.disconnect(); window.YouTubeUtils?.ObserverRegistry?.untrack && window.YouTubeUtils.ObserverRegistry.untrack(); } }, 300)); if (document.body) { navigationObserver.observe(document.body, { childList: !0, subtree: !1, attributes: !1 }); window.YouTubeUtils?.cleanupManager?.registerObserver && window.YouTubeUtils.cleanupManager.registerObserver(navigationObserver); window.YouTubeUtils?.ObserverRegistry?.track && window.YouTubeUtils.ObserverRegistry.track(); } scheduleInit(); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const Y = window.YouTubeUtils || {}; const t = Y.t || (key => key || ""); function mk(tag, props = {}, children = []) { const el = document.createElement(tag); Object.entries(props).forEach(([k, v]) => { "class" === k ? el.className = v : "html" === k ? el.innerHTML = "function" == typeof window._ytplusCreateHTML ? window._ytplusCreateHTML(v) : _createHTML(sanitizeHTML(v)) : k.startsWith("on") && "function" == typeof v ? el.addEventListener(k.substring(2).toLowerCase(), v) : el.setAttribute(k, String(v)); }); children.forEach(c => el.appendChild("string" == typeof c ? document.createTextNode(c) : c)); return el; } function sanitizeHTML(html) { if (Y?.sanitizeHTML && "function" == typeof Y.sanitizeHTML) { return Y.sanitizeHTML(html); } if ("string" != typeof html) { return ""; } const map = { "<": "<", ">": ">", "&": "&", '"': """, "'": "'", "/": "/", "`": "`", "=": "=" }; return html.replace(/[<>&"'\/`=]/g, char => map[char] || char); } function isValidEmail(email) { if (!email || "string" != typeof email) { return !1; } return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 254; } function validateTitle(title) { return title && "string" == typeof title ? sanitizeHTML(title.trim().substring(0, 200)) : ""; } function validateDescription(description) { return description && "string" == typeof description ? sanitizeHTML(description.trim().substring(0, 5e3)) : ""; } function getDebugInfo() { try { const debug = { version: window.YouTubePlusDebug?.version || "unknown", userAgent: navigator?.userAgent || "unknown", url: location?.href || "unknown", language: document.documentElement?.lang || navigator?.language || "unknown", settings: "object" == typeof Y?.SettingsManager ? Y.SettingsManager.load() : null }; return debug; } catch (err) { Y && "function" == typeof Y.logError && Y.logError("Report", "Failed to collect debug info", err); return { version: "unknown", userAgent: "unknown", url: "unknown", language: "unknown", settings: null, error: "Failed to collect debug info" }; } } function buildIssuePayload({type, title, description, email, includeDebug}) { const debug = includeDebug ? getDebugInfo() : null; const lines = []; const typeLabel = t("bug" === type ? "typeBug" : "feature" === type ? "typeFeature" : "typeOther"); lines.push(`**Type:** ${typeLabel}`); email && lines.push(`**Reporter email (optional):** ${email}`); lines.push("\n**Description:**\n"); lines.push(description || "(no description)"); if (debug) { lines.push("\n---\n**Debug info**\n"); lines.push("```json"); try { lines.push(JSON.stringify(debug, null, 2)); } catch (err) { Y && "function" == typeof Y.logError && Y.logError("Report", "Failed to stringify debug info", err); const minimalDebug = { version: debug.version || "unknown", userAgent: debug.userAgent || "unknown", url: debug.url || "unknown" }; try { lines.push(JSON.stringify(minimalDebug, null, 2)); } catch { lines.push('{ "error": "Failed to stringify debug info" }'); } } lines.push("```"); lines.push("\n_Please do not include sensitive personal data._"); } const body = lines.join("\n"); const issueTitle = `${"bug" === type ? "[Bug]" : "feature" === type ? "[Feature]" : "[Report]"} ${title || ""}`.trim(); return { title: issueTitle, body }; } try { window.youtubePlusReport = window.youtubePlusReport || {}; window.youtubePlusReport.render = function renderReportSection(modal) { if (!modal || !modal.querySelector) { return; } const section = modal.querySelector('.ytp-plus-settings-section[data-section="report"]'); if (!section) { return; } section.replaceChildren(); const form = mk("div", { style: "display:flex;flex-direction:column;gap:var(--yt-space-sm);margin-top:var(--yt-space-md);" }); const typeSelect = mk("select", { style: "display:none;" }, []); const typeOptions = [ { v: "bug", l: t("typeBug") }, { v: "feature", l: t("typeFeature") }, { v: "other", l: t("typeOther") } ]; typeOptions.forEach(opt => { const o = mk("option", { value: opt.v }, [ opt.l ]); typeSelect.appendChild(o); }); const typeDropdown = mk("div", { class: "glass-dropdown", id: "report-type-dropdown", tabindex: "0", role: "listbox", "aria-expanded": "false" }); const defaultLabel = typeOptions[0].l; const toggleBtn = mk("button", { class: "glass-dropdown__toggle", type: "button", "aria-haspopup": "listbox" }, [ mk("span", { class: "glass-dropdown__label" }, [ defaultLabel ]) ]); toggleBtn.appendChild(mk("svg", { class: "glass-dropdown__chev", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ mk("polyline", { points: "6 9 12 15 18 9" }, []) ])); const listEl = mk("ul", { class: "glass-dropdown__list", role: "presentation" }, []); typeOptions.forEach((opt, i) => { const li = mk("li", { class: "glass-dropdown__item", "data-value": opt.v, role: "option" }, [ opt.l ]); 0 === i && li.setAttribute("aria-selected", "true"); listEl.appendChild(li); }); typeDropdown.appendChild(toggleBtn); typeDropdown.appendChild(listEl); const inputStyle = "padding:var(--yt-space-sm);border-radius:var(--yt-radius-sm);background:var(--yt-input-bg);color:var(--yt-text-primary);border:1px solid var(--yt-glass-border);backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);font-size:14px;transition:var(--yt-transition);box-sizing:border-box;"; const titleInput = mk("input", { placeholder: t("shortTitle"), style: inputStyle }); const emailInput = mk("input", { placeholder: t("emailOptional"), type: "email", style: inputStyle }); const descInput = mk("textarea", { placeholder: t("descriptionPlaceholder"), rows: 6, style: inputStyle + "resize:vertical;font-family:inherit;" }); const debugCheckboxInput = mk("input", { type: "checkbox", class: "ytp-plus-settings-checkbox" }); const includeDebug = mk("label", { style: "font-size:13px;display:flex;gap:var(--yt-space-sm);align-items:center;color:var(--yt-text-primary);cursor:pointer;align-self:center;" }, [ debugCheckboxInput, " " + t("includeDebug") ]); const actions = mk("div", { style: "display:flex;gap:var(--yt-space-sm);margin-top:var(--yt-space-sm);flex-wrap:wrap;" }); const submitBtn = mk("button", { class: "glass-button" }, [ t("openGitHub") ]); const copyBtn = mk("button", { class: "glass-button" }, [ t("copyReport") ]); const emailBtn = mk("button", { class: "glass-button" }, [ t("prepareEmail") ]); actions.appendChild(submitBtn); actions.appendChild(copyBtn); actions.appendChild(emailBtn); form.appendChild(typeSelect); form.appendChild(typeDropdown); form.appendChild(titleInput); form.appendChild(emailInput); form.appendChild(descInput); form.appendChild(includeDebug); const debugPreview = mk("div", { class: "glass-card", style: "overflow:auto;max-height:240px;font-size:11px;display:none;margin-top:var(--yt-space-sm);padding:8px;box-sizing:border-box;" }, []); form.appendChild(debugPreview); form.appendChild(actions); const privacy = mk("div", { class: "ytp-plus-settings-item-description", style: "margin-top:var(--yt-space-sm);font-size:12px;color:var(--yt-text-secondary);" }, [ t("privacy") ]); section.appendChild(form); section.appendChild(privacy); !(function initReportTypeDropdown() { try { const hidden = typeSelect; const dropdown = typeDropdown; const toggle = dropdown.querySelector(".glass-dropdown__toggle"); const list = dropdown.querySelector(".glass-dropdown__list"); const label = dropdown.querySelector(".glass-dropdown__label"); let items = Array.from(list.querySelectorAll(".glass-dropdown__item")); let idx = items.findIndex(it => "true" === it.getAttribute("aria-selected")); idx < 0 && (idx = 0); const openList = () => { dropdown.setAttribute("aria-expanded", "true"); list.style.display = "block"; items = Array.from(list.querySelectorAll(".glass-dropdown__item")); }; const closeList = () => { dropdown.setAttribute("aria-expanded", "false"); list.style.display = "none"; }; const selectedItem = items[idx]; if (selectedItem) { hidden.value = selectedItem.dataset.value || ""; label.textContent = selectedItem.textContent || ""; } toggle.addEventListener("click", () => { const expanded = "true" === dropdown.getAttribute("aria-expanded"); expanded ? closeList() : openList(); }); const _rDocClick = e => { dropdown.contains(e.target) || closeList(); }; window.YouTubeUtils?.cleanupManager?.registerListener ? window.YouTubeUtils.cleanupManager.registerListener(document, "click", _rDocClick) : document.addEventListener("click", _rDocClick); list.addEventListener("click", e => { const it = e.target.closest(".glass-dropdown__item"); if (!it) { return; } const val = it.dataset.value; hidden.value = val; list.querySelectorAll(".glass-dropdown__item").forEach(li => li.removeAttribute("aria-selected")); it.setAttribute("aria-selected", "true"); label.textContent = it.textContent; hidden.dispatchEvent(new Event("change", { bubbles: !0 })); closeList(); }); dropdown.addEventListener("keydown", e => { const expanded = "true" === dropdown.getAttribute("aria-expanded"); if ("ArrowDown" === e.key) { e.preventDefault(); expanded || openList(); idx = Math.min(idx + 1, items.length - 1); items.forEach(it => it.removeAttribute("aria-selected")); items[idx].setAttribute("aria-selected", "true"); items[idx].scrollIntoView({ block: "nearest" }); } else if ("ArrowUp" === e.key) { e.preventDefault(); expanded || openList(); idx = Math.max(idx - 1, 0); items.forEach(it => it.removeAttribute("aria-selected")); items[idx].setAttribute("aria-selected", "true"); items[idx].scrollIntoView({ block: "nearest" }); } else if ("Enter" === e.key || " " === e.key) { e.preventDefault(); if (!expanded) { openList(); return; } const it = items[idx]; if (it) { hidden.value = it.dataset.value; hidden.dispatchEvent(new Event("change", { bubbles: !0 })); label.textContent = it.textContent; closeList(); } } else { "Escape" === e.key && closeList(); } }); } catch (err) { Y && "function" == typeof Y.logError && Y.logError("Report", "initReportTypeDropdown", err); } })(); debugCheckboxInput.addEventListener("change", function updateDebugPreview() { try { if (debugCheckboxInput.checked) { const d = getDebugInfo(); debugPreview.replaceChildren(); const header = mk("div", { style: "display:flex;flex-direction:column;gap:6px;margin-bottom:6px;" }, []); header.appendChild(mk("div", {}, [ "Version: ", mk("strong", {}, [ String(d.version || "unknown") ]) ])); header.appendChild(mk("div", {}, [ "User agent: ", mk("code", { style: "font-size:11px;color:var(--yt-text-secondary);" }, [ String(d.userAgent || "") ]) ])); const urlStr = String(d.url || "unknown"); let urlEl = mk("span", {}, [ urlStr ]); try { /^https?:\/\//i.test(urlStr) && (urlEl = mk("a", { href: urlStr, target: "_blank", rel: "noopener noreferrer", style: "color:var(--yt-accent);word-break:break-all;" }, [ urlStr ])); } catch (e) { Y && "function" == typeof Y.logError && Y.logError("Report", "URL link creation failed", e); urlEl = mk("span", {}, [ String(urlStr) ]); } header.appendChild(mk("div", {}, [ "URL: ", urlEl ])); header.appendChild(mk("div", {}, [ "Language: ", mk("code", {}, [ String(d.language || "") ]) ])); debugPreview.appendChild(header); if (d.settings) { const settingsDetails = mk("details", {}, [ mk("summary", {}, [ "Settings" ]) ]); settingsDetails.appendChild(mk("pre", { style: "white-space:pre-wrap;margin:6px 0 0 0;font-size:11px;" }, [ JSON.stringify(d.settings, null, 2) ])); debugPreview.appendChild(settingsDetails); } const fullDetails = mk("details", {}, [ mk("summary", {}, [ "Full debug JSON" ]) ]); fullDetails.appendChild(mk("pre", { style: "white-space:pre-wrap;margin:6px 0 0 0;font-size:11px;" }, [ JSON.stringify(d, null, 2) ])); debugPreview.appendChild(fullDetails); debugPreview.style.display = "block"; } else { debugPreview.replaceChildren(); debugPreview.style.display = "none"; } } catch (err) { Y && "function" == typeof Y.logError && Y.logError("Report", "updateDebugPreview failed", err); } }); function gather() { const type = typeSelect.value; const rawTitle = titleInput.value.trim(); const rawDescription = descInput.value.trim(); const rawEmail = emailInput.value.trim(); const includeDebugValue = includeDebug.querySelector("input").checked; const errors = []; rawTitle ? rawTitle.length < 5 && errors.push(t("titleMin")) : errors.push(t("titleRequired")); rawDescription ? rawDescription.length < 10 && errors.push(t("descMin")) : errors.push(t("descRequired")); rawEmail && !isValidEmail(rawEmail) && errors.push(t("invalidEmail")); return { type, title: validateTitle(rawTitle), description: validateDescription(rawDescription), email: rawEmail && isValidEmail(rawEmail) ? rawEmail : "", includeDebug: includeDebugValue, errors }; } submitBtn.addEventListener("click", e => { e.preventDefault(); if (!submitBtn.disabled) { try { const data = gather(); if (data.errors && data.errors.length > 0) { const errorMsg = t("fixErrorsPrefix") + data.errors.join("\n• "); Y.NotificationManager && "function" == typeof Y.NotificationManager.show ? Y.NotificationManager.show(errorMsg, { duration: 4e3, type: "error" }) : console.warn("[Report] Validation errors:", data.errors); return; } const originalText = submitBtn.textContent; submitBtn.disabled = !0; submitBtn.textContent = t("opening"); submitBtn.style.opacity = "0.6"; const payload = buildIssuePayload(data); !(function openGitHubIssue(payload) { try { const repoOwner = "diorhc"; const repo = "YTP"; const url = `https://github.com/${repoOwner}/${repo}/issues/new?title=${encodeURIComponent(payload.title)}&body=${encodeURIComponent(payload.body)}`; window.open(url, "_blank"); } catch (err) { Y && "function" == typeof Y.logError && Y.logError("Report", "Failed to open GitHub issue", err); throw err; } })(payload); Y.NotificationManager && "function" == typeof Y.NotificationManager.show && Y.NotificationManager.show(t("openingGithubNotification"), { duration: 2500 }); setTimeout(() => { submitBtn.disabled = !1; submitBtn.textContent = originalText; submitBtn.style.opacity = "1"; }, 2e3); } catch (err) { Y.logError && Y.logError("Report", "Failed to open GitHub issue", err); Y.NotificationManager && "function" == typeof Y.NotificationManager.show && Y.NotificationManager.show(t("failedOpenGithub"), { duration: 3e3, type: "error" }); submitBtn.disabled = !1; submitBtn.textContent = t("openGitHub"); submitBtn.style.opacity = "1"; } } }); copyBtn.addEventListener("click", e => { e.preventDefault(); if (!copyBtn.disabled) { try { const data = gather(); if (data.errors && data.errors.length > 0) { const errorMsg = t("fixErrorsPrefix") + data.errors.join("\n• "); Y.NotificationManager && "function" == typeof Y.NotificationManager.show ? Y.NotificationManager.show(errorMsg, { duration: 4e3, type: "error" }) : console.warn("[Report] Validation errors:", data.errors); return; } const originalText = copyBtn.textContent; copyBtn.disabled = !0; copyBtn.textContent = t("copying"); copyBtn.style.opacity = "0.6"; const payload = buildIssuePayload(data); const full = `Title: ${payload.title}\n\n${payload.body}`; (function copyToClipboard(text) { return navigator.clipboard && navigator.clipboard.writeText ? navigator.clipboard.writeText(text) : new Promise((resolve, reject) => { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.left = "-9999px"; ta.style.opacity = "0"; document.body.appendChild(ta); try { ta.select(); ta.setSelectionRange(0, text.length); const success = document.execCommand("copy"); document.body.removeChild(ta); success ? resolve() : reject(new Error("execCommand failed")); } catch (err) { document.body.removeChild(ta); reject(err); } }); })(full).then(() => { Y.NotificationManager && "function" == typeof Y.NotificationManager.show && Y.NotificationManager.show(t("reportCopied"), { duration: 2e3 }); copyBtn.textContent = t("copied"); copyBtn.style.opacity = "1"; setTimeout(() => { copyBtn.disabled = !1; copyBtn.textContent = originalText; }, 2e3); }).catch(err => { Y && "function" == typeof Y.logError && Y.logError("Report", "copy failed", err); Y && Y.NotificationManager && "function" == typeof Y.NotificationManager.show ? Y.NotificationManager.show(t("copyFailed"), { duration: 3e3, type: "error" }) : console.warn("Copy failed; please copy manually", err); copyBtn.disabled = !1; copyBtn.textContent = originalText; copyBtn.style.opacity = "1"; }); } catch (err) { Y.logError && Y.logError("Report", "Failed to copy report", err); copyBtn.disabled = !1; copyBtn.textContent = t("copyReport"); copyBtn.style.opacity = "1"; } } }); emailBtn.addEventListener("click", e => { e.preventDefault(); if (!emailBtn.disabled) { try { const data = gather(); if (data.errors && data.errors.length > 0) { const errorMsg = t("fixErrorsPrefix") + data.errors.join("\n• "); Y.NotificationManager && "function" == typeof Y.NotificationManager.show ? Y.NotificationManager.show(errorMsg, { duration: 4e3, type: "error" }) : console.warn("[Report] Validation errors:", data.errors); return; } const originalText = emailBtn.textContent; emailBtn.disabled = !0; emailBtn.textContent = t("opening"); emailBtn.style.opacity = "0.6"; const payload = buildIssuePayload(data); const subject = payload.title; const mailto = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(payload.body)}`; window.location.href = mailto; setTimeout(() => { emailBtn.disabled = !1; emailBtn.textContent = originalText; emailBtn.style.opacity = "1"; }, 2e3); } catch (err) { Y.logError && Y.logError("Report", "Failed to prepare email", err); emailBtn.disabled = !1; emailBtn.textContent = t("prepareEmail"); emailBtn.style.opacity = "1"; } } }); }; } catch (e) { Y.logError && Y.logError("Report", "Failed to attach report module to window", e); } })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const t = window.YouTubeUtils?.t || (key => key || ""); const getLanguage = () => { if (window.YouTubePlusI18n?.getLanguage) { return window.YouTubePlusI18n.getLanguage(); } if (window.YouTubeUtils?.getLanguage) { return window.YouTubeUtils.getLanguage(); } const lang = document.documentElement.lang || navigator.language || "en"; return lang.startsWith("ru") ? "ru" : "en"; }; const UPDATE_CONFIG = { enabled: !0, checkInterval: 864e5, updateUrl: "https://update.greasyfork.org/scripts/537017/YouTube%20%2B.meta.js", currentVersion: "2.4.5", storageKey: "youtube_plus_update_check", notificationDuration: 8e3, autoInstallUrl: "https://update.greasyfork.org/scripts/537017/YouTube%20%2B.user.js", autoInstallOnCheck: !1, showNotificationIcon: !1 }; const windowRef = "undefined" == typeof window ? null : window; const GM_namespace = windowRef?.GM || null; const GM_info_safe = windowRef?.GM_info || null; const GM_openInTab_safe = (() => { if (windowRef) { if ("function" == typeof windowRef.GM_openInTab) { return windowRef.GM_openInTab.bind(windowRef); } if (GM_namespace?.openInTab) { return GM_namespace.openInTab.bind(GM_namespace); } } return null; })(); GM_info_safe?.script?.version && (UPDATE_CONFIG.currentVersion = GM_info_safe.script.version); const updateState = { lastCheck: 0, lastVersion: UPDATE_CONFIG.currentVersion, updateAvailable: !1, checkInProgress: !1, updateDetails: null }; function pluralizeTime(n, unit) { const lang = getLanguage(); const num = Math.abs(Number(n)) || 0; if ("ru" === lang) { const forms = (function getRussianForms(unit) { return { day: [ "день", "дня", "дней" ], hour: [ "час", "часа", "часов" ], minute: [ "минута", "минуты", "минут" ] }[unit]; })(unit); const idx = (function getRussianPluralIndex(num) { const mod10 = num % 10; const mod100 = num % 100; return 1 === mod10 && 11 !== mod100 ? 0 : mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14) ? 1 : 2; })(num); return `${num} ${forms[idx]}`; } const enForms = (function getEnglishForms(unit) { return { day: [ "day", "days" ], hour: [ "hour", "hours" ], minute: [ "minute", "minutes" ] }[unit]; })(unit); return `${num} ${1 === num ? enForms[0] : enForms[1]}`; } const utils_loadSettings = () => { try { const saved = localStorage.getItem(UPDATE_CONFIG.storageKey); if (!saved) { return; } const parsed = JSON.parse(saved); if ("object" != typeof parsed || null === parsed) { console.error("[YouTube+][Update]", "Invalid settings structure"); return; } "number" == typeof parsed.lastCheck && parsed.lastCheck >= 0 && (updateState.lastCheck = parsed.lastCheck); if ("string" == typeof parsed.lastVersion) { const ver = parsed.lastVersion.replace(/^v/i, ""); /^\d+(?:\.\d+){0,2}$/.test(ver) && (updateState.lastVersion = ver); } "boolean" == typeof parsed.updateAvailable && (updateState.updateAvailable = parsed.updateAvailable); parsed.updateDetails && "object" == typeof parsed.updateDetails && "string" == typeof parsed.updateDetails.version && /^\d+\.\d+\.\d+/.test(parsed.updateDetails.version) && (updateState.updateDetails = parsed.updateDetails); } catch (e) { console.error("[YouTube+][Update]", "Failed to load update settings:", e); } }, utils_saveSettings = () => { try { const dataToSave = { lastCheck: updateState.lastCheck, lastVersion: updateState.lastVersion, updateAvailable: updateState.updateAvailable, updateDetails: updateState.updateDetails }; localStorage.setItem(UPDATE_CONFIG.storageKey, JSON.stringify(dataToSave)); } catch (e) { console.error("[YouTube+][Update]", "Failed to save update settings:", e); } }, utils_compareVersions = (v1, v2) => { if ("string" != typeof v1 || "string" != typeof v2) { console.error("[YouTube+][Update]", "Invalid version format - must be strings"); return 0; } const normalize = v => v.replace(/[^\d.]/g, "").split(".").map(n => parseInt(n, 10) || 0); const [parts1, parts2] = [ normalize(v1), normalize(v2) ]; const maxLength = Math.max(parts1.length, parts2.length); for (let i = 0; i < maxLength; i++) { const diff = (parts1[i] || 0) - (parts2[i] || 0); if (0 !== diff) { return diff; } } return 0; }, utils_parseMetadata = text => { if ("string" != typeof text || text.length > 1e5) { console.error("[YouTube+][Update]", "Invalid metadata text"); return { version: null, description: "", downloadUrl: UPDATE_CONFIG.autoInstallUrl }; } const extractField = field => text.match(new RegExp(`@${(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))(field)}\\s+([^\\r\\n]+)`))?.[1]?.trim(); let version = extractField("version"); const description = extractField("description") || ""; const downloadUrl = extractField("downloadURL") || UPDATE_CONFIG.autoInstallUrl; if (version) { version = version.replace(/^v/i, "").trim(); if (!/^\d+(?:\.\d+){0,2}$/.test(version)) { console.error("[YouTube+][Update]", "Invalid version format in metadata:", version); return { version: null, description: "", downloadUrl: UPDATE_CONFIG.autoInstallUrl }; } } return { version, description: description.substring(0, 500), downloadUrl }; }, utils_formatTimeAgo = timestamp => { if (!timestamp) { return t("never"); } const diffMs = Date.now() - timestamp; const diffDays = Math.floor(diffMs / 864e5); const diffHours = Math.floor(diffMs / 36e5); const diffMinutes = Math.floor(diffMs / 6e4); return diffDays > 0 ? pluralizeTime(diffDays, "day") : diffHours > 0 ? pluralizeTime(diffHours, "hour") : diffMinutes > 0 ? pluralizeTime(diffMinutes, "minute") : t("justNow"); }, utils_showNotification = (text, type = "info", duration = 3e3) => { try { YouTubeUtils.NotificationManager.show(text, { type, duration }); } catch (error) { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug(`[YouTube+] ${type.toUpperCase()}:`, text, error); } }; const markUpdateDismissed = details => { if (details?.version && "string" == typeof details.version) { try { sessionStorage.setItem("update_dismissed", details.version); } catch (err) { console.error("[YouTube+][Update]", "Failed to persist dismissal state:", err); } } }; const installUpdate = (details = updateState.updateDetails) => { const downloadUrl = details?.downloadUrl || UPDATE_CONFIG.autoInstallUrl; const validation = (downloadUrl => { if (!downloadUrl || "string" != typeof downloadUrl) { return { valid: !1, error: "Invalid download URL for installation" }; } try { const parsedUrl = new URL(downloadUrl); const allowedDomains = [ "update.greasyfork.org", "greasyfork.org" ]; return "https:" !== parsedUrl.protocol ? { valid: !1, error: "Only HTTPS URLs allowed for updates" } : allowedDomains.includes(parsedUrl.hostname) ? { valid: !0, error: null } : { valid: !1, error: `Update URL domain not in allowlist: ${parsedUrl.hostname}` }; } catch (error) { return { valid: !1, error: `Invalid URL format: ${error.message}` }; } })(downloadUrl); if (!validation.valid) { console.error("[YouTube+][Update]", validation.error); return !1; } const success = (url => { if (GM_openInTab_safe) { try { GM_openInTab_safe(url, { active: !0, insert: !0, setParent: !0 }); return !0; } catch (gmError) { console.error("[YouTube+] GM_openInTab update install failed:", gmError); } } try { const popup = window.open(url, "_blank", "noopener"); if (popup) { return !0; } } catch (popupError) { console.error("[YouTube+] window.open update install failed:", popupError); } try { window.location.assign(url); return !0; } catch (navigationError) { console.error("[YouTube+] Navigation to update URL failed:", navigationError); } return !1; })(downloadUrl); success && markUpdateDismissed(details); return success; }; const fetchUpdateMetadata = async (url = UPDATE_CONFIG.updateUrl) => (async requestUrl => { if ("undefined" != typeof GM_xmlhttpRequest) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => reject(new Error("Update check timeout")), 1e4); GM_xmlhttpRequest({ method: "GET", url: requestUrl, timeout: 1e4, headers: { Accept: "text/plain", "User-Agent": "YouTube+ UpdateChecker" }, onload: response => { clearTimeout(timeoutId); response.status >= 200 && response.status < 300 ? resolve(response.responseText) : reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); }, onerror: e => { clearTimeout(timeoutId); reject(new Error(`Network error: ${e}`)); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error("Update check timeout")); } }); }); } const controller = new AbortController; const timeoutId = setTimeout(() => controller.abort(), 1e4); try { const res = await fetch(requestUrl, { method: "GET", cache: "no-cache", signal: controller.signal, headers: { Accept: "text/plain", "User-Agent": "YouTube+ UpdateChecker" } }); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } return await res.text(); } finally { clearTimeout(timeoutId); } })(url); const handleUpdateResult = (updateDetails, force) => { const shouldShowNotification = updateState.updateAvailable && (force || sessionStorage.getItem("update_dismissed") !== updateDetails.version); if (shouldShowNotification) { (updateDetails => { const iconHtml = UPDATE_CONFIG.showNotificationIcon ? '<div style="background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));\n border-radius: 10px; padding: 10px; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.08);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);">\n <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M21 12c0 1-1 2-1 2s-1-1-1-2 1-2 1-2 1 1 1 2z"/>\n <path d="m21 12-5-5v3H8v4h8v3l5-5z"/>\n </svg>\n </div>' : ""; const notification = document.createElement("div"); notification.className = "youtube-enhancer-notification update-notification"; notification.setAttribute("role", "alertdialog"); notification.setAttribute("aria-label", t("updateAvailableTitle") || "Update available"); notification.style.cssText = "\n z-index: 10001; max-width: 360px;\n background: rgba(255,255,255,0.04); padding: 16px 18px; border-radius: 14px;\n color: rgba(255,255,255,0.95);\n box-shadow: 0 8px 30px rgba(11, 15, 25, 0.45), inset 0 1px 0 rgba(255,255,255,0.02);\n border: 1px solid rgba(255,255,255,0.08);\n -webkit-backdrop-filter: blur(10px) saturate(120%);\n backdrop-filter: blur(10px) saturate(120%);\n animation: slideInFromBottom 0.4s ease-out;\n "; notification.innerHTML = _createHTML(`\n <div style="position: relative; display: flex; align-items: flex-start; gap: 12px;">\n ${iconHtml}\n <div style="flex: 1; min-width: 0;">\n <div style="font-weight: 600; font-size: 15px; margin-bottom: 4px;">${t("updateAvailableTitle")}</div>\n <div style="font-size: 13px; opacity: 0.9; margin-bottom: 8px;">\n ${t("version")} ${updateDetails.version}\n </div>\n ${updateDetails.changelog || updateDetails.description ? (function() { const header = t("changelogHeader"); const raw = updateDetails.changelog && updateDetails.changelog.length > 0 ? updateDetails.changelog : updateDetails.description || ""; const text = (s => String(s).replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]*>?/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim())(raw); const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean); const listHtml = lines.map(l => `<div style="font-size:12px; opacity:0.85; margin-bottom:6px;">${l}</div>`).join(""); return `<div style="font-size:12px; font-weight:600; opacity:0.95; margin-bottom:6px;">${header}</div><div style="font-size:12px; line-height:1.4; max-height:120px; overflow-y:auto; padding:8px; background: rgba(0,0,0,0.2); border-radius:6px; border:1px solid rgba(255,255,255,0.05); white-space:normal;">${listHtml}</div>`; })() : `<div style="font-size: 12px); opacity: 0.85; margin-bottom: 12px;">${t("newFeatures")}</div>`}\n <div style="display: flex; gap: 8px;">\n <button id="update-install-btn" type="button" style="\n background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));\n color: #ff5a1a; border: 1px solid rgba(255,90,30,0.12);\n padding: 8px 16px; border-radius: 8px; cursor: pointer;\n font-size: 13px; font-weight: 700; transition: transform 0.15s ease;\n box-shadow: 0 6px 18px rgba(90,30,0,0.12);\n backdrop-filter: blur(6px);\n ">${t("installUpdate")}</button>\n <button id="update-dismiss-btn" type="button" style="\n background: transparent; color: rgba(255,255,255,0.9);\n border: 1px solid rgba(255,255,255,0.06); padding: 8px 12px;\n border-radius: 8px; cursor: pointer; font-size: 13px; transition: all 0.12s ease;\n ">${t("later")}</button>\n </div>\n </div>\n <button id="update-close-btn" aria-label="${t("dismiss")}" style="\n position: absolute; top: -8px; right: -8px; width: 28px; height: 28px;\n border-radius: 50%; border: none; cursor: pointer; display: flex;\n align-items: center; justify-content: center; font-size: 16px; line-height: 1;\n background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85); transition: background 0.18s ease;\n border: 1px solid rgba(255,255,255,0.06);\n ">×</button>\n </div>\n <style>\n @keyframes slideInFromBottom {\n from { transform: translateY(100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n }\n @keyframes slideOutToBottom {\n from { transform: translateY(0); opacity: 1; }\n to { transform: translateY(100%); opacity: 0; }\n }\n #update-close-btn:hover {\n background: rgba(255, 255, 255, 0.25);\n }\n </style>\n `); const _containerId = "youtube-enhancer-notification-container"; let _container = document.getElementById(_containerId); if (!_container) { _container = document.createElement("div"); _container.id = _containerId; _container.className = "youtube-enhancer-notification-container"; try { document.body.appendChild(_container); } catch { document.body.appendChild(notification); } } try { _container.insertBefore(notification, _container.firstChild); } catch { document.body.appendChild(notification); } const removeNotification = () => { notification.style.animation = "slideOutToBottom 0.35s ease-in forwards"; setTimeout(() => notification.remove(), 360); }; const installBtn = notification.querySelector("#update-install-btn"); installBtn && installBtn.addEventListener("click", () => { const success = installUpdate(updateDetails); if (success) { removeNotification(); setTimeout(() => utils_showNotification(t("installing")), 500); } else { utils_showNotification(t("manualInstallHint"), "error", 5e3); window.open("https://greasyfork.org/en/scripts/537017-youtube", "_blank"); } }); const dismissBtn = notification.querySelector("#update-dismiss-btn"); dismissBtn && dismissBtn.addEventListener("click", () => { updateDetails?.version && sessionStorage.setItem("update_dismissed", updateDetails.version); removeNotification(); }); const closeBtn = notification.querySelector("#update-close-btn"); closeBtn && closeBtn.addEventListener("click", () => { updateDetails?.version && sessionStorage.setItem("update_dismissed", updateDetails.version); removeNotification(); }); setTimeout(() => { notification.isConnected && removeNotification(); }, UPDATE_CONFIG.notificationDuration); })(updateDetails); window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug(`YouTube + Update available: ${updateDetails.version}`); } else if (force) { const message = updateState.updateAvailable ? t("updateAvailableMsg").replace("{version}", updateDetails.version) : t("upToDateMsg").replace("{version}", UPDATE_CONFIG.currentVersion); utils_showNotification(message); } }; const retrieveUpdateDetails = async () => { let metaText = await fetchUpdateMetadata(UPDATE_CONFIG.updateUrl); let details = utils_parseMetadata(metaText); if (!details.version) { try { const fallbackText = await fetchUpdateMetadata(UPDATE_CONFIG.autoInstallUrl); const fallbackDetails = utils_parseMetadata(fallbackText); if (fallbackDetails.version) { details = fallbackDetails; metaText = fallbackText; } } catch (fallbackErr) { "undefined" != typeof console && console.warn && console.warn("[YouTube+][Update] Fallback metadata fetch failed:", fallbackErr.message); } } if (details.version) { try { const changelog = await (async version => { try { const lang = getLanguage(); const url = `https://greasyfork.org/${lang}/scripts/537017-youtube/versions`; const fetchPage = async requestUrl => { if ("undefined" != typeof GM_xmlhttpRequest) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => reject(new Error("Changelog fetch timeout")), 1e4); GM_xmlhttpRequest({ method: "GET", url: requestUrl, timeout: 1e4, headers: { Accept: "text/html" }, onload: response => { clearTimeout(timeoutId); response.status >= 200 && response.status < 300 ? resolve(response.responseText) : reject(new Error(`HTTP ${response.status}`)); }, onerror: () => { clearTimeout(timeoutId); reject(new Error("Network error")); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error("Timeout")); } }); }); } const controller = new AbortController; const timeoutId = setTimeout(() => controller.abort(), 1e4); try { const res = await fetch(requestUrl, { method: "GET", cache: "no-cache", signal: controller.signal, headers: { Accept: "text/html" } }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return await res.text(); } finally { clearTimeout(timeoutId); } }; const html = await fetchPage(url); const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const versionRegex = new RegExp(`>[^<]*?${escapedVersion}</a>[\\s\\S]*?class="version-changelog"[^>]*>([\\s\\S]*?)</span>`, "i"); const match = html.match(versionRegex); if (match && match[1]) { let changelog = match[1].trim(); changelog = changelog.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'"); changelog = changelog.split("\n").map(line => line.trim()).filter(line => line.length > 0).join("\n"); return changelog || ""; } return ""; } catch (error) { console.warn("[YouTube+][Update] Failed to fetch changelog:", error.message); return ""; } })(details.version); details.changelog = "string" == typeof changelog && changelog.length > 0 ? changelog : ""; } catch (changelogErr) { console.warn("[YouTube+][Update] Failed to fetch changelog:", changelogErr.message); details.changelog = ""; } } else { details.changelog = ""; } return details; }; const validateUpdateConfiguration = () => { try { (url => { const parsedUrl = new URL(url); if ("https:" !== parsedUrl.protocol) { throw new Error("Update URL must use HTTPS"); } if (!parsedUrl.hostname.includes("greasyfork.org")) { throw new Error("Update URL must be from greasyfork.org"); } })(UPDATE_CONFIG.updateUrl); return !0; } catch (urlError) { console.error("[YouTube+][Update]", "Invalid update URL configuration:", urlError); throw urlError; } }; const checkForUpdates = async (force = !1, retryCount = 0) => { const now = Date.now(); if (((force, now) => !(!UPDATE_CONFIG.enabled || updateState.checkInProgress) && (force || now - updateState.lastCheck >= UPDATE_CONFIG.checkInterval))(force, now)) { updateState.checkInProgress = !0; try { validateUpdateConfiguration(); const updateDetails = await retrieveUpdateDetails(); updateDetails.version ? ((updateDetails, force, now) => { updateState.lastCheck = now; updateState.lastVersion = updateDetails.version; updateState.updateDetails = updateDetails; const comparison = utils_compareVersions(UPDATE_CONFIG.currentVersion, updateDetails.version); updateState.updateAvailable = comparison < 0; handleUpdateResult(updateDetails, force); utils_saveSettings(); if (updateState.updateAvailable && UPDATE_CONFIG.autoInstallOnCheck) { try { const dismissed = sessionStorage.getItem("update_dismissed"); if (dismissed !== updateDetails.version) { const started = installUpdate(updateDetails); if (started) { markUpdateDismissed(updateDetails); try { utils_showNotification(t("installing")); } catch {} } else { console.warn("[YouTube+][Update] Auto-install could not be initiated for", updateDetails.downloadUrl); } } } catch (e) { console.error("[YouTube+][Update] Auto-installation failed:", e); } } })(updateDetails, force, now) : (force => { updateState.updateAvailable = !1; force && utils_showNotification(t("updateCheckFailed").replace("{msg}", t("noUpdateInfo")), "error", 4e3); })(force); } catch (error) { await (async (error, force, retryCount) => { if ((error => "AbortError" === error.name || "NetworkError" === error.name || error.message && error.message.includes("fetch") || error.message && error.message.includes("network"))(error) && retryCount < 2) { console.warn(`[YouTube+][Update] Retry ${retryCount + 1}/2 after error:`, error.message); await new Promise(resolve => setTimeout(resolve, 2e3 * Math.pow(2, retryCount))); return checkForUpdates(force, retryCount + 1); } console.error("[YouTube+][Update] Check failed after retries:", error); force && utils_showNotification(t("updateCheckFailed").replace("{msg}", error.message), "error", 4e3); })(error, force, retryCount); } finally { updateState.checkInProgress = !1; } } }; const addUpdateSettings = () => { const aboutSection = YouTubeUtils.querySelector('.ytp-plus-settings-section[data-section="about"]'); if (!aboutSection || YouTubeUtils.querySelector(".update-settings-container")) { return; } const updateContainer = document.createElement("div"); updateContainer.className = "update-settings-container"; updateContainer.style.cssText = "\n padding: 16px; margin-top: 20px; border-radius: 12px;\n background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);\n -webkit-backdrop-filter: blur(10px) saturate(120%);\n backdrop-filter: blur(10px) saturate(120%);\n box-shadow: 0 6px 20px rgba(6, 10, 20, 0.45);\n "; const lastCheckTime = utils_formatTimeAgo(updateState.lastCheck); updateContainer.innerHTML = _createHTML(`\n <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">\n <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: var(--yt-spec-text-primary);">\n ${t("enhancedExperience")}\n </h3>\n </div>\n \n <div style="display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: center; \n padding: 16px; background: rgba(255, 255, 255, 0.03); border-radius: 10px; margin-bottom: 16px;">\n <div>\n <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">\n <span style="font-size: 14px; font-weight: 600; color: var(--yt-spec-text-primary);">${t("currentVersion")}</span>\n <span style="font-size: 13px; font-weight: 600; color: var(--yt-spec-text-primary); \n padding: 3px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 12px; \n border: 1px solid rgba(255, 255, 255, 0.2);">${UPDATE_CONFIG.currentVersion}</span>\n </div>\n <div style="font-size: 12px; color: var(--yt-spec-text-secondary);">\n ${t("lastChecked")}: <span style="font-weight: 500;">${lastCheckTime}</span>\n ${updateState.lastVersion && updateState.lastVersion !== UPDATE_CONFIG.currentVersion ? `<br>${t("latestAvailable")}: <span style="color: #ff6666; font-weight: 600;">${updateState.lastVersion}</span>` : ""}\n </div>\n </div>\n \n ${updateState.updateAvailable ? `\n <div style="display: flex; flex-direction: column; align-items: flex-end; gap: 8px;">\n <div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; \n background: linear-gradient(135deg, rgba(255, 68, 68, 0.2), rgba(255, 68, 68, 0.3)); \n border: 1px solid rgba(255, 68, 68, 0.4); border-radius: 20px;">\n <div style="width: 6px; height: 6px; background: #ff4444; border-radius: 50%; animation: pulse 2s infinite;"></div>\n <span style="font-size: 11px; color: #ff6666; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">\n ${t("updateAvailable")}\n </span>\n </div>\n <button id="install-update-btn" style="background: linear-gradient(135deg, #ff4500, #ff6b35); \n color: white; border: none; padding: 8px 16px; border-radius: 8px; cursor: pointer; \n font-size: 12px; font-weight: 600; transition: all 0.3s ease; \n box-shadow: 0 4px 12px rgba(255, 69, 0, 0.3);">${t("installUpdate")}</button>\n </div>\n ` : `\n <div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; \n background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.3)); \n border: 1px solid rgba(34, 197, 94, 0.4); border-radius: 20px;">\n <div style="width: 6px; height: 6px; background: #22c55e; border-radius: 50%;"></div>\n <span style="font-size: 11px; color: #22c55e; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">\n ${t("upToDate")}\n </span>\n </div>\n `}\n </div>\n \n <div style="display: flex; gap: 12px;">\n <button class="ytp-plus-button ytp-plus-button-primary" id="manual-update-check" \n style="flex: 1; padding: 12px; font-size: 13px; font-weight: 600;">\n <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">\n <path d="M21.5 2v6h-6M2.5 22v-6h6M19.13 11.48A10 10 0 0 0 12 2C6.48 2 2 6.48 2 12c0 .34.02.67.05 1M4.87 12.52A10 10 0 0 0 12 22c5.52 0 10-4.48 10-10 0-.34-.02-.67-.05-1"/>\n </svg>\n ${t("checkForUpdates")}\n </button>\n <button class="ytp-plus-button" id="open-update-page" \n style="padding: 12px 16px; font-size: 13px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);">\n <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="gray" stroke-width="2">\n <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>\n <polyline points="15,3 21,3 21,9"/>\n <line x1="10" y1="14" x2="21" y2="3"/>\n </svg>\n </button>\n </div>\n\n <style>\n @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.1); } }\n @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n </style>\n `); aboutSection.appendChild(updateContainer); const attachClickHandler = (id, handler) => { const element = document.getElementById(id); element && YouTubeUtils.cleanupManager.registerListener(element, "click", handler); }; attachClickHandler("manual-update-check", async ({target}) => { const button = target; const originalHTML = button.innerHTML; button.innerHTML = _createHTML(`\n <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" \n style="margin-right: 6px; animation: spin 1s linear infinite;">\n <path d="M21.5 2v6h-6M2.5 22v-6h6M19.13 11.48A10 10 0 0 0 12 2C6.48 2 2 6.48 2 12c0 .34.02.67.05 1M4.87 12.52A10 10 0 0 0 12 22c5.52 0 10-4.48 10-10 0-.34-.02-.67-.05-1"/>\n </svg>\n ${t("checkingForUpdates")}\n `); button.disabled = !0; await checkForUpdates(!0); setTimeout(() => { button.innerHTML = _createHTML(originalHTML); button.disabled = !1; }, 1e3); }); attachClickHandler("install-update-btn", () => { const success = installUpdate(); if (success) { utils_showNotification(t("installing")); } else { utils_showNotification(t("manualInstallHint"), "error", 5e3); window.open("https://greasyfork.org/en/scripts/537017-youtube", "_blank"); } }); attachClickHandler("open-update-page", () => { utils_showNotification(t("updatePageFallback")); window.open("https://greasyfork.org/en/scripts/537017-youtube", "_blank"); }); }; let _initDone = !1; const init = () => { if (!_initDone) { _initDone = !0; utils_loadSettings(); (() => { setTimeout(() => checkForUpdates(), 3e3); const intervalId = setInterval(() => checkForUpdates(), UPDATE_CONFIG.checkInterval); YouTubeUtils.cleanupManager.registerInterval(intervalId); YouTubeUtils.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(window, "beforeunload", () => clearInterval(intervalId)) : window.addEventListener("beforeunload", () => clearInterval(intervalId)); })(); (() => { const handler = () => { setTimeout(addUpdateSettings, 100); }; YouTubeUtils.cleanupManager?.registerListener ? YouTubeUtils.cleanupManager.registerListener(document, "youtube-plus-settings-modal-opened", handler) : document.addEventListener("youtube-plus-settings-modal-opened", handler); })(); YouTubeUtils.cleanupManager.registerListener(document, "click", ({target}) => { const el = target; el.classList?.contains("ytp-plus-settings-nav-item") && "about" === el.dataset?.section && setTimeout(addUpdateSettings, 50); }, { passive: !0, capture: !0 }); (() => { try { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("YouTube + Update Checker initialized", { version: UPDATE_CONFIG.currentVersion, enabled: UPDATE_CONFIG.enabled, lastCheck: new Date(updateState.lastCheck).toLocaleString(), updateAvailable: updateState.updateAvailable }); } catch {} })(); } }; "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", init) : init(); })(); /** * YouTube Music Enhancement Module * Provides UI improvements and features for YouTube Music * @module music * @version 2.3 * * Features: * - Scroll-to-top button with smart container detection * - Enhanced navigation styles (centered search, immersive mode) * - Sidebar hover effects and player enhancements * - Health monitoring and automatic recovery * - SPA navigation support with debounced updates */ !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); if ("undefined" != typeof location && "music.youtube.com" !== location.hostname) { return; } const qs = sel => window.YouTubeUtils?.$(sel) || document.querySelector(sel); const MUSIC_SETTINGS_DEFAULTS = { enableMusic: !0, immersiveSearchStyles: !0, hoverStyles: !0, playerSidebarStyles: !0, centeredPlayerStyles: !0, playerBarStyles: !0, centeredPlayerBarStyles: !0, miniPlayerStyles: !0 }; function mergeMusicSettings(parsed) { const merged = { ...MUSIC_SETTINGS_DEFAULTS }; if (!parsed || "object" != typeof parsed) { return merged; } "boolean" == typeof parsed.enableMusic && (merged.enableMusic = parsed.enableMusic); for (const key of Object.keys(MUSIC_SETTINGS_DEFAULTS)) { "enableMusic" !== key && "boolean" == typeof parsed[key] && (merged[key] = parsed[key]); } "boolean" == typeof parsed.enableImmersiveSearch && (merged.immersiveSearchStyles = parsed.enableImmersiveSearch); "boolean" == typeof parsed.enableSidebarHover && (merged.hoverStyles = parsed.enableSidebarHover); "boolean" == typeof parsed.enableCenteredPlayer && (merged.centeredPlayerStyles = parsed.enableCenteredPlayer); const legacyEnabled = !!(parsed.enableMusicStyles || parsed.enableMusicEnhancements || parsed.enableImmersiveSearch || parsed.enableSidebarHover || parsed.enableCenteredPlayer); legacyEnabled && "boolean" != typeof parsed.enableMusic && (merged.enableMusic = !0); return merged; } function readMusicSettings() { try { if ("undefined" != typeof GM_getValue) { const stored = GM_getValue("youtube-plus-music-settings", null); if ("string" == typeof stored && stored) { const parsed = JSON.parse(stored); return mergeMusicSettings(parsed); } } } catch {} try { const stored = localStorage.getItem("youtube-plus-music-settings"); if (!stored) { return { ...MUSIC_SETTINGS_DEFAULTS }; } const parsed = JSON.parse(stored); return mergeMusicSettings(parsed); } catch { return { ...MUSIC_SETTINGS_DEFAULTS }; } } function isMusicModuleEnabled(settings) { return !(!settings || !settings.enableMusic); } let musicSettingsSnapshot = readMusicSettings(); let musicStyleEl = null; let observer = null; let healthCheckIntervalId = null; let detachNavigationListeners = null; function applyStyles() { if ("music.youtube.com" !== window.location.hostname) { return; } const s = musicSettingsSnapshot || readMusicSettings(); if (!s.enableMusic) { return; } const styleParts = [ "\n /* Remove borders and shadows from nav/guide when bauhaus sidenav is enabled */\n ytmusic-app-layout[is-bauhaus-sidenav-enabled] #nav-bar-background.ytmusic-app-layout { border-bottom: none !important; box-shadow: none !important; }\n ytmusic-app-layout[is-bauhaus-sidenav-enabled] #nav-bar-divider.ytmusic-app-layout { border-top: none !important; }\n ytmusic-app-layout[is-bauhaus-sidenav-enabled] #mini-guide-background.ytmusic-app-layout { border-right: 0 !important; }\n ytmusic-nav-bar, ytmusic-app-layout[is-bauhaus-sidenav-enabled] .ytmusic-nav-bar { border: none !important; box-shadow: none !important; }\n /* Center the settings button in the top nav bar (fixes it being rendered at the bottom) */\n ytmusic-settings-button.style-scope.ytmusic-nav-bar, ytmusic-nav-bar ytmusic-settings-button.style-scope.ytmusic-nav-bar {position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; bottom: auto !important; margin: 0 !important; z-index: 1000 !important;}\n /* Center the search box in the top nav bar */\n ytmusic-search-box, ytmusic-nav-bar ytmusic-search-box, ytmusic-searchbox, ytmusic-nav-bar ytmusic-searchbox {position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; margin: 0 !important; max-width: 75% !important; width: auto !important; z-index: 900 !important;}\n " ]; s.immersiveSearchStyles && styleParts.push("\n /* yt-Immersive search behaviour for YouTube Music: expand/center the search when focused */\n ytmusic-search-box:has(input:focus), ytmusic-searchbox:has(input:focus), ytmusic-search-box:focus-within, ytmusic-searchbox:focus-within {position: fixed !important; left: 50% !important; top: 12vh !important; transform: translateX(-50%) !important; height: auto !important; max-width: 900px !important; width: min(90vw, 900px) !important; z-index: 1200 !important; display: block !important;}\n @media only screen and (min-width: 1400px) {ytmusic-search-box:has(input:focus), ytmusic-searchbox:has(input:focus) {top: 10vh !important; max-width: 1000px !important; transform: translateX(-50%) scale(1.05) !important;}}\n /* Highlight the input and add a soft glow */\n ytmusic-search-box:has(input:focus) input, ytmusic-searchbox:has(input:focus) input, ytmusic-search-box:focus-within input, ytmusic-searchbox:focus-within input {background-color: #fffb !important; box-shadow: black 0 0 30px !important;}\n @media (prefers-color-scheme: dark) {ytmusic-search-box:has(input:focus) input, ytmusic-searchbox:has(input:focus) input {background-color: #000b !important;}}\n /* Blur/scale the main content when immersive search is active */\n ytmusic-app-layout:has(ytmusic-search-box:has(input:focus)) #main-panel, ytmusic-app-layout:has(ytmusic-searchbox:has(input:focus)) #main-panel {filter: blur(18px) !important; transform: scale(1.03) !important;}\n "); s.hoverStyles && styleParts.push("\n .ytmusic-guide-renderer {opacity: 0.01 !important; transition: opacity 0.5s ease-in-out !important;} \n .ytmusic-guide-renderer:hover { opacity: 1 !important;} \n ytmusic-app[is-bauhaus-sidenav-enabled] #guide-wrapper.ytmusic-app {background-color: transparent !important; border: none !important;} \n "); s.playerSidebarStyles && styleParts.push('\n #side-panel {width: 40em !important; height: 80vh !important; padding: 0 2em !important; right: -30em !important; top: 10vh !important; opacity: 0 !important; position: absolute !important; transition: all 0.3s ease-in-out !important; backdrop-filter: blur(5px) !important; background-color: #0005 !important; border-radius: 1em !important; box-shadow: rgba(0, 0, 0, 0.15) 0px -36px 30px inset, rgba(0, 0, 0, 0.1) 0px -79px 40px inset, rgba(0, 0, 0, 0.06) 0px 2px 1px, rgba(0, 0, 0, 0.09) 0px 4px 2px, rgba(0, 0, 0, 0.09) 0px 8px 4px, rgba(0, 0, 0, 0.09) 0px 16px 8px, rgba(0, 0, 0, 0.09) 0px 32px 16px !important;} \n #side-panel tp-yt-paper-tabs {transition: height 0.3s ease-in-out !important; height: 0 !important;} \n #side-panel:hover {right: 0 !important; opacity: 1 !important;} \n #side-panel:hover tp-yt-paper-tabs {height: 4em !important;} \n #side-panel:has(ytmusic-tab-renderer[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"]):not(:has(ytmusic-message-renderer:not([style="display: none;"]))) {right: 0 !important; opacity: 1 !important;} \n #side-panel {min-width: auto !important;}\n /* Allow JS to control visibility; ensure pointer-events and positioning only. */\n #side-panel .ytmusic-top-button { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; }\n /* When button is placed inside the panel, prefer absolute positioning inside it\n so it won\'t be forced to fixed by the global rule. Use high specificity + !important */\n #side-panel .ytmusic-top-button {position: absolute !important; bottom: 20px !important; right: 20px !important; z-index: 1200 !important;}\n '); s.centeredPlayerStyles && styleParts.push('\n ytmusic-app-layout:not([player-ui-state="FULLSCREEN"]) #main-panel {position: absolute !important; height: 70vh !important; max-width: 70vw !important; aspect-ratio: 1 !important; top: 50vh !important; left: 50vw !important; transform: translate(-50%, -50%) !important;} \n #player-page {padding: 0 !important; margin: 0 !important; left: 0 !important; top: 0 !important; height: 100% !important; width: 100% !important;}\n '); s.playerBarStyles && styleParts.push('\n ytmusic-player-bar, #player-bar-background {margin: 1vw !important; width: 98vw !important; border-radius: 1em !important; overflow: hidden !important; transition: all 0.5s ease-in-out !important; background-color: #0002 !important; box-shadow: rgba(0, 0, 0, 0.15) 0px -36px 30px inset, rgba(0, 0, 0, 0.1) 0px -79px 40px inset, rgba(0, 0, 0, 0.06) 0px 2px 1px, rgba(0, 0, 0, 0.09) 0px 4px 2px, rgba(0, 0, 0, 0.09) 0px 8px 4px, rgba(0, 0, 0, 0.09) 0px 16px 8px, rgba(0, 0, 0, 0.09) 0px 32px 16px !important;} \n #layout:not([player-ui-state="PLAYER_PAGE_OPEN"]) #player-bar-background {background-color: #0005 !important;}\n '); s.centeredPlayerBarStyles && styleParts.push("\n #left-controls {position: absolute !important; left: 49vw !important; bottom: 15px !important; transform: translateX(-50%) !important; width: fit-content !important; order: 1 !important;} \n .time-info {position: absolute !important; bottom: -10px !important; left: 0 !important; width: 100% !important; text-align: center !important; padding: 0 !important; margin: 0 !important;}\n .middle-controls {position: absolute !important; left: 1vw !important; bottom: 15px !important; max-width: 30vw !important; order: 0 !important;}\n "); s.miniPlayerStyles && styleParts.push('\n #main-panel:has(ytmusic-player[player-ui-state="MINIPLAYER"]) {position: fixed !important; width: 100vw !important; height: 100vh !important; top: -100vh !important; left: 0 !important; margin: 0 !important; padding: 0 !important; transform: none !important; max-width: 100vw !important;} \n ytmusic-player[player-ui-state="MINIPLAYER"] {position: fixed !important; bottom: calc(100vh + 120px) !important; right: 30px !important; width: 350px !important; height: fit-content !important;} \n #av-id:has(ytmusic-av-toggle) {position: absolute !important; left: 50% !important; transform: translateX(-50%) !important; top: -4em !important; opacity: 0 !important; transition: all 0.3s ease-in-out !important;} \n #av-id:has(ytmusic-av-toggle):hover {opacity: 1 !important;} \n #player[player-ui-state="MINIPLAYER"] {display: none !important;}\n /* Chrome-specific robustness: ensure the AV toggle container is above overlays\n and can receive hover even if :has() behaves differently. Also provide a\n non-:has fallback so the element is hoverable regardless of child matching. */\n /* Use absolute positioning (keeps internal menu alignment) but promote\n stacking and rendering to ensure it sits above overlays and receives clicks. */\n #av-id {position: absolute !important; left: 50% !important; transform: translateX(-50%) translateZ(0) !important; top: -4em !important; z-index: 10000 !important; pointer-events: auto !important; display: block !important; visibility: visible !important; width: auto !important; height: auto !important; will-change: transform, opacity !important;}\n #av-id ytmusic-av-toggle {pointer-events: auto !important;}\n #av-id:hover {opacity: 1 !important;}\n /* Prevent overlapping overlays from stealing clicks when hovering the toggle.\n This is a conservative rule; if a specific overlay still steals clicks we\n can target it explicitly later. */\n #av-id:hover, #av-id:active { filter: none !important; }\n '); const allStyles = `\n${styleParts.join("\n")}\n`; if (musicStyleEl && musicStyleEl.isConnected) { musicStyleEl.textContent = allStyles; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Styles updated"); } else { try { if ("undefined" != typeof GM_addStyle) { const el = GM_addStyle(allStyles); if (el && "STYLE" === el.tagName) { musicStyleEl = el; try { musicStyleEl.id = "youtube-plus-music-styles"; } catch {} } } } catch {} if (!musicStyleEl || !musicStyleEl.isConnected) { const style = document.createElement("style"); style.id = "youtube-plus-music-styles"; style.textContent = allStyles; document.head.appendChild(style); musicStyleEl = style; } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Styles applied"); } } const getDebounce = () => window.YouTubeUtils?.debounce ? window.YouTubeUtils.debounce : (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; const t = window.YouTubeUtils?.t || (key => key || ""); const scrollContainerCache = new WeakMap; function attachButtonToContainer(button, sidePanel, sc, MusicUtils) { try { !(function setupScrollBehavior(button, sc, MusicUtils, sidePanel) { if (MusicUtils.setupScrollToTop) { MusicUtils.setupScrollToTop(button, sc); return; } const findNearestScrollable = startEl => { let el = startEl; for (;el && el !== document.body; ) { try { if (el.scrollHeight > el.clientHeight + 10) { return el; } } catch {} el = el.parentElement; } return null; }; button.addEventListener("click", ev => { try { ev.preventDefault?.(); } catch {} try { ev.stopPropagation?.(); } catch {} let target = sc; target && target.scrollHeight > target.clientHeight + 1 || (target = sidePanel && findNearestScrollable(sidePanel)); target || (target = findNearestScrollable(button.parentElement)); target || (target = document.scrollingElement || document.documentElement || document.body); try { const info = { chosen: target && (target.id || target.tagName || "(window)"), scrollTop: target && "scrollTop" in target ? target.scrollTop : null, scrollHeight: target && "scrollHeight" in target ? target.scrollHeight : null, clientHeight: target && "clientHeight" in target ? target.clientHeight : null }; try { window.YouTubeMusic = window.YouTubeMusic || {}; window.YouTubeMusic._lastClickDebug = info; } catch {} window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "ScrollToTop click target", info); } catch {} const tryScroll = el => { if (!el) { return !1; } try { if ("function" == typeof el.scrollTo) { el.scrollTo({ top: 0, behavior: "smooth" }); return !0; } if ("scrollTop" in el) { el.scrollTop = 0; return !0; } } catch {} return !1; }; let scrolled = !1; scrolled = tryScroll(target) || scrolled; sc && sc !== target && (scrolled = tryScroll(sc) || scrolled); scrolled = tryScroll(document.scrollingElement || document.documentElement || document.body) || scrolled; if (!scrolled) { try { window.scrollTo(0, 0); } catch (err2) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Final scroll fallback failed", err2); } } }, { passive: !1 }); })(button, sc, MusicUtils, sidePanel); const attachInsidePanel = !!sidePanel; !(function setupButtonPosition(button, sidePanel, MusicUtils, options = {}) { if (MusicUtils.setupButtonStyles) { MusicUtils.setupButtonStyles(button, sidePanel, options); } else { if (options.insideSidePanel && sidePanel) { button.style.setProperty("position", "absolute", "important"); button.style.setProperty("bottom", "20px", "important"); button.style.setProperty("right", "20px", "important"); button.style.setProperty("z-index", "1200", "important"); button.style.setProperty("pointer-events", "auto", "important"); button.style.display = "flex"; } else { button.style.position = "fixed"; button.style.bottom = "100px"; button.style.right = "20px"; button.style.zIndex = "10000"; button.style.pointerEvents = "auto"; button.style.display = "flex"; } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Button positioned:", { position: button.style.position, bottom: button.style.bottom, right: button.style.right, zIndex: button.style.zIndex, insideSidePanel: !!options.insideSidePanel }); } })(button, sidePanel, MusicUtils, { insideSidePanel: attachInsidePanel }); if (attachInsidePanel) { try { sidePanel.appendChild(button); } catch (err) { document.body.appendChild(button); window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Appending to sidePanel failed, appended to body", err); } } else { document.body.appendChild(button); } !(function setupScrollVisibility(button, sc, MusicUtils) { if (window.YouTubePlusScrollManager && window.YouTubePlusScrollManager.addScrollListener) { try { const cleanup = window.YouTubePlusScrollManager.addScrollListener(sc, () => { const shouldShow = sc.scrollTop > 100; button.classList.toggle("visible", shouldShow); window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `Scroll position: ${sc.scrollTop}px, button visible: ${shouldShow}`); }, { debounce: 100, runInitial: !0 }); button._scrollCleanup = cleanup; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Using ScrollManager for scroll handling"); return; } catch { console.error("[YouTube+][Music] ScrollManager failed, using fallback"); } } if (MusicUtils.setupScrollVisibility) { MusicUtils.setupScrollVisibility(button, sc, 100); return; } let isTabVisible = !document.hidden; let rafId = null; const updateVisibility = () => { rafId && cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { if (!isTabVisible) { return; } const currentScroll = sc.scrollTop || 0; const shouldShow = currentScroll > 100; const wasVisible = button.classList.contains("visible"); button.classList.toggle("visible", shouldShow); shouldShow !== wasVisible && window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `Button visibility changed: ${shouldShow ? "SHOWN" : "HIDDEN"} (scroll: ${currentScroll}px)`); }); }; const debounce = getDebounce(); const scrollHandler = debounce(updateVisibility, 100); const visibilityHandler = () => { isTabVisible = !document.hidden; isTabVisible && updateVisibility(); }; sc.addEventListener("scroll", scrollHandler, { passive: !0 }); document.addEventListener("visibilitychange", visibilityHandler); setTimeout(updateVisibility, 100); setTimeout(updateVisibility, 500); button._scrollCleanup = () => { rafId && cancelAnimationFrame(rafId); sc.removeEventListener("scroll", scrollHandler); document.removeEventListener("visibilitychange", visibilityHandler); }; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Using fallback scroll handler"); })(button, sc, MusicUtils); const initialScroll = sc.scrollTop || 0; if (initialScroll > 100) { button.classList.add("visible"); window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `Button shown immediately (scroll: ${initialScroll}px)`); } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Scroll to top button created successfully", { buttonId: button.id, scrollContainer: sc.tagName, scrollContainerId: sc.id || "no-id", scrollHeight: sc.scrollHeight, clientHeight: sc.clientHeight, scrollTop: initialScroll, position: button.style.position, computedDisplay: window.getComputedStyle(button).display, computedOpacity: window.getComputedStyle(button).opacity, computedVisibility: window.getComputedStyle(button).visibility }); } catch (err) { console.error("[YouTube+][Music] attachButton error:", err); } } const buttonCreationState = { attempts: 0, maxAttempts: 5, lastAttempt: 0, minInterval: 500 }; function createScrollToTopButton() { try { if ("music.youtube.com" !== window.location.hostname) { return; } const existingButton = document.getElementById("ytmusic-side-panel-top-button"); if (existingButton) { if (document.body.contains(existingButton) && existingButton._scrollCleanup) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Button already exists and is properly attached"); return; } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Removing orphaned button"); existingButton.remove(); } const now = Date.now(); if (now - buttonCreationState.lastAttempt < buttonCreationState.minInterval) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Rate limited, skipping button creation"); return; } buttonCreationState.attempts++; buttonCreationState.lastAttempt = now; if (buttonCreationState.attempts > buttonCreationState.maxAttempts) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `Max attempts (${buttonCreationState.maxAttempts}) reached, stopping retries`); return; } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `Creating button (attempt ${buttonCreationState.attempts}/${buttonCreationState.maxAttempts})`); const sidePanel = qs("#side-panel"); const MusicUtils = window.YouTubePlusMusicUtils || {}; const button = (function createButton() { const button = document.createElement("button"); button.id = "ytmusic-side-panel-top-button"; button.className = "ytmusic-top-button top-button"; button.title = t("scrollToTop"); button.setAttribute("aria-label", t("scrollToTop")); button.innerHTML = _createHTML('<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>'); button.setAttribute("data-ytmusic-scroll-button", "true"); window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Button element created", { id: button.id, className: button.className }); return button; })(); if (!sidePanel) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "No side-panel found, checking for main content or queue"); const queueRenderer = qs("ytmusic-queue-renderer"); if (queueRenderer) { const queueContents = queueRenderer.querySelector("#contents"); if (queueContents) { attachButtonToContainer(button, queueRenderer, queueContents, MusicUtils); buttonCreationState.attempts = 0; return; } } const mainContent = qs("ytmusic-browse"); if (mainContent) { const scrollContainer = mainContent.querySelector("ytmusic-section-list-renderer"); if (scrollContainer) { attachButtonToContainer(button, mainContent, scrollContainer, MusicUtils); buttonCreationState.attempts = 0; return; } } setTimeout(createScrollToTopButton, 1e3); return; } const scrollContainer = (function findScrollContainer(sidePanel, MusicUtils) { if (scrollContainerCache.has(sidePanel)) { const cached = scrollContainerCache.get(sidePanel); if (cached && document.body.contains(cached) && cached.scrollHeight > cached.clientHeight + 10) { return cached; } scrollContainerCache.delete(sidePanel); } if (MusicUtils.findScrollContainer) { const result = MusicUtils.findScrollContainer(sidePanel); result && scrollContainerCache.set(sidePanel, result); return result; } const selectors = [ 'ytmusic-tab-renderer[tab-identifier="FEmusic_queue"] #contents', 'ytmusic-tab-renderer[tab-identifier="FEmusic_up_next"] #contents', 'ytmusic-tab-renderer[tab-identifier="FEmusic_lyrics"] #contents', "ytmusic-tab-renderer[selected] #contents", "ytmusic-tab-renderer #contents", "ytmusic-queue-renderer #contents", "ytmusic-playlist-shelf-renderer #contents", "#side-panel #contents", "#contents.ytmusic-tab-renderer", ".ytmusic-section-list-renderer", '[role="tabpanel"]', ".ytmusic-player-queue", "ytmusic-tab-renderer", ".scroller", "[scroll-container]" ]; for (const selector of selectors) { const container = sidePanel?.querySelector(selector); if (container) { const isScrollable = container.scrollHeight > container.clientHeight + 10; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `Checking ${selector}: scrollHeight=${container.scrollHeight}, clientHeight=${container.clientHeight}, isScrollable=${isScrollable}`); if (isScrollable) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `✓ Found scroll container: ${selector}`); scrollContainerCache.set(sidePanel, container); return container; } } } if (sidePanel && sidePanel.scrollHeight > sidePanel.clientHeight + 10) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "✓ Using side-panel as scroll container"); scrollContainerCache.set(sidePanel, sidePanel); return sidePanel; } if (sidePanel) { const fallbackSelectors = [ "div[id]", "div[class]", '[role="tabpanel"]', '[role="list"]', '[role="listbox"]' ]; let best = null; let bestDelta = 0; for (const sel of fallbackSelectors) { try { const candidates = sidePanel.querySelectorAll(sel); for (const el of candidates) { const delta = (el.scrollHeight || 0) - (el.clientHeight || 0); if (delta > 10 && delta > bestDelta) { bestDelta = delta; best = el; } } } catch {} } if (best) { const tag = best.tagName.toLowerCase(); const id = best.id ? `#${best.id}` : ""; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", `✓ Best scroll container chosen: ${tag}${id}`, { scrollHeight: best.scrollHeight, clientHeight: best.clientHeight }); scrollContainerCache.set(sidePanel, best); return best; } } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "✗ No scroll container found in side-panel"); return null; })(sidePanel, MusicUtils); if (!scrollContainer) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "No scroll container found, will retry with backoff"); const backoffDelay = Math.min(500 * buttonCreationState.attempts, 3e3); setTimeout(createScrollToTopButton, backoffDelay); return; } attachButtonToContainer(button, sidePanel, scrollContainer, MusicUtils); buttonCreationState.attempts = 0; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "✓ Button created successfully"); } catch (error) { console.error("[YouTube+][Music] Error creating scroll to top button:", error); buttonCreationState.attempts < buttonCreationState.maxAttempts && setTimeout(createScrollToTopButton, 1e3); } } function checkAndCreateButton() { try { const existingButton = document.getElementById("ytmusic-side-panel-top-button"); if (existingButton) { if (existingButton._scrollCleanup && document.body.contains(existingButton)) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Button is healthy, no action needed"); return; } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Cleaning up orphaned/detached button"); if (existingButton._scrollCleanup) { try { existingButton._scrollCleanup(); } catch {} } if (existingButton._positionCleanup) { try { existingButton._positionCleanup(); } catch {} } existingButton.remove(); } const sidePanel = qs("#side-panel"); const mainContent = qs("ytmusic-browse"); const queueRenderer = qs("ytmusic-queue-renderer"); const tabRenderer = qs("ytmusic-tab-renderer[tab-identifier]"); if (sidePanel || mainContent || queueRenderer || tabRenderer) { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Found container, scheduling button creation"); setTimeout(createScrollToTopButton, 300); } else { window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "No suitable container found yet"); } } catch (error) { console.error("[YouTube+][Music] Error in checkAndCreateButton:", error); } } const createObserver = () => { const debounce = getDebounce(); const debouncedCheck = debounce(checkAndCreateButton, 200); let lastCheckTime = 0; return new MutationObserver(mutations => { const now = Date.now(); if (now - lastCheckTime < 300) { return; } const existingButton = document.getElementById("ytmusic-side-panel-top-button"); if (existingButton && document.body.contains(existingButton) && existingButton._scrollCleanup) { return; } const hasRelevantChange = mutations.some(mutation => { if (0 === mutation.addedNodes.length) { return !1; } let hasElements = !1; for (let i = 0; i < mutation.addedNodes.length; i++) { if (1 === mutation.addedNodes[i].nodeType) { hasElements = !0; break; } } return !!hasElements && Array.from(mutation.addedNodes).some(node => { if (1 !== node.nodeType) { return !1; } const element = node; if ("side-panel" === element.id || "contents" === element.id) { return !0; } const tagName = element.tagName; return "YTMUSIC-BROWSE" === tagName || "YTMUSIC-PLAYER-PAGE" === tagName || "YTMUSIC-QUEUE-RENDERER" === tagName || "YTMUSIC-TAB-RENDERER" === tagName || null != element.querySelector?.("#side-panel, #contents, ytmusic-browse, ytmusic-queue-renderer, ytmusic-tab-renderer"); }); }); const hasTabChange = mutations.some(mutation => "attributes" === mutation.type && "selected" === mutation.attributeName && mutation.target instanceof Element && mutation.target.matches?.("ytmusic-tab-renderer, tp-yt-paper-tab")); if (hasRelevantChange || hasTabChange) { lastCheckTime = now; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Detected relevant DOM change, checking button"); debouncedCheck(); } }); }; const observeDocumentBodySafely = () => { if (observer) { return; } const startObserving = () => { if (document.body) { try { observer = createObserver(); observer.observe(document.body, { childList: !0, subtree: !0, attributes: !0, attributeFilter: [ "selected", "tab-identifier", "page-type" ] }); window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "✓ Observer started with enhanced config"); } catch (observeError) { console.error("[YouTube+][Music] Failed to observe document.body:", observeError); try { observer = createObserver(); observer.observe(document.body, { childList: !0, subtree: !0 }); window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "✓ Observer started with basic config"); } catch (retryError) { console.error("[YouTube+][Music] Failed to start observer (retry):", retryError); } } } }; document.body ? startObserving() : document.addEventListener("DOMContentLoaded", startObserving, { once: !0 }); }; function stopScrollToTopRuntime() { try { if (null != healthCheckIntervalId) { clearInterval(healthCheckIntervalId); healthCheckIntervalId = null; } if (observer) { observer.disconnect(); observer = null; } if (detachNavigationListeners) { try { detachNavigationListeners(); } catch {} detachNavigationListeners = null; } const button = document.getElementById("ytmusic-side-panel-top-button"); if (button?._scrollCleanup) { try { button._scrollCleanup(); } catch {} } if (button?._positionCleanup) { try { button._positionCleanup(); } catch {} } button && button.remove(); } catch (e) { console.error("[YouTube+][Music] stopScrollToTopRuntime error:", e); } } function applySettingsChanges() { musicSettingsSnapshot = readMusicSettings(); if (isMusicModuleEnabled(musicSettingsSnapshot)) { if ("music.youtube.com" === window.location.hostname) { applyStyles(); stopScrollToTopRuntime(); } } else { stopScrollToTopRuntime(); musicStyleEl && musicStyleEl.isConnected && musicStyleEl.remove(); try { document.querySelectorAll("#youtube-plus-music-styles").forEach(el => el !== musicStyleEl && el.remove()); } catch {} musicStyleEl = null; } } "undefined" != typeof window && (window.YouTubeMusic = { observeDocumentBodySafely, checkAndCreateButton, createScrollToTopButton, saveSettings: function saveSettings(s) { musicSettingsSnapshot = s && "object" == typeof s ? { ...musicSettingsSnapshot, ...s } : readMusicSettings(); }, applySettingsChanges, version: "2.4.5" }); window.addEventListener("beforeunload", () => { try { stopScrollToTopRuntime(); musicStyleEl && musicStyleEl.isConnected && musicStyleEl.remove(); musicStyleEl = null; window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Cleanup completed"); } catch (error) { console.error("[YouTube+][Music] Cleanup error:", error); } }); !(function startIfEnabled() { if ("music.youtube.com" === window.location.hostname) { musicSettingsSnapshot = readMusicSettings(); if (isMusicModuleEnabled(musicSettingsSnapshot)) { "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", applyStyles, { once: !0 }) : applyStyles(); 0; } } })(); try { "undefined" != typeof GM_addValueChangeListener && GM_addValueChangeListener("youtube-plus-music-settings", (_name, _oldValue, newValue) => { try { if ("string" == typeof newValue && newValue) { const parsed = JSON.parse(newValue); musicSettingsSnapshot = mergeMusicSettings(parsed); } else { musicSettingsSnapshot = readMusicSettings(); } } catch { musicSettingsSnapshot = readMusicSettings(); } applySettingsChanges(); }); } catch (e) { console.warn("[YouTube+][Music] Settings listener registration error:", e); } window.YouTubeUtils?.logger?.debug?.("[YouTube+][Music]", "Module loaded (lazy)", { version: "2.4.5", hostname: window.location.hostname, enabled: "music.youtube.com" === window.location.hostname && isMusicModuleEnabled(musicSettingsSnapshot) }); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); const {$, $$} = window.YouTubeUtils || {}; const onDomReady = (() => { let ready = "loading" !== document.readyState; const queue = []; ready || document.addEventListener("DOMContentLoaded", () => { ready = !0; for (;queue.length; ) { const cb = queue.shift(); try { cb(); } catch {} } }, { once: !0 }); return cb => { ready ? cb() : queue.push(cb); }; })(); const CONFIG = { enabled: !0, storageKey: "youtube_endscreen_settings", selectors: ".ytp-ce-element-show,.ytp-ce-element,.ytp-endscreen-element,.ytp-ce-covering-overlay,.ytp-cards-teaser,.teaser-carousel,.ytp-cards-button,.iv-drawer,.iv-branding,.video-annotations,.ytp-cards-teaser-text", debounceMs: 32, batchSize: 20 }; const state = { observer: null, styleEl: null, isActive: !1, removeCount: 0, lastCheck: 0, ytNavigateListenerKey: null, settingsNavListenerKey: null }; const debounce = window.YouTubeUtils?.debounce || ((fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }); const settings = { load: () => { try { const data = localStorage.getItem(CONFIG.storageKey); CONFIG.enabled = !data || (JSON.parse(data).enabled ?? !0); } catch { CONFIG.enabled = !0; } }, save: () => { try { localStorage.setItem(CONFIG.storageKey, JSON.stringify({ enabled: CONFIG.enabled })); } catch {} settings.apply(); }, apply: () => CONFIG.enabled ? init() : cleanup() }; const removeEndScreens = () => { if (!CONFIG.enabled) { return; } const now = performance.now(); if (now - state.lastCheck < CONFIG.debounceMs) { return; } state.lastCheck = now; const elements = $$(CONFIG.selectors); elements.length && (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 {} } } })(elements); }; const isRelevantNode = node => { if (!(node instanceof Element)) { return !1; } const classNameValue = (node => "string" == typeof node.className ? node.className : node.className && "object" == typeof node.className && "baseVal" in node.className ? node.className.baseVal : "")(node); return classNameValue.includes("ytp-") || node.querySelector?.(".ytp-ce-element"); }; const createEndScreenObserver = throttledRemove => new MutationObserver(mutations => { (mutations => { for (const {addedNodes} of mutations) { for (const node of addedNodes) { if (isRelevantNode(node)) { return !0; } } } return !1; })(mutations) && throttledRemove(); }); const cleanup = () => { state.observer?.disconnect(); state.observer = null; if (state.styleEl) { try { YouTubeUtils.StyleManager.remove(state.styleEl); } catch {} } state.styleEl = null; state.isActive = !1; }; const init = () => { if (!state.isActive && CONFIG.enabled) { state.isActive = !0; (() => { if (state.styleEl || !CONFIG.enabled) { return; } const styles = `${CONFIG.selectors}{display:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important;transform:scale(0)!important}`; YouTubeUtils.StyleManager.add("end-screen-remover", styles); state.styleEl = "end-screen-remover"; })(); removeEndScreens(); (() => { if (state.observer || !CONFIG.enabled) { return; } const throttledRemove = debounce(removeEndScreens, CONFIG.debounceMs); state.observer = createEndScreenObserver(throttledRemove); YouTubeUtils.cleanupManager.registerObserver(state.observer); const observeTarget = (attempt = 0) => { const target = $("#movie_player"); target ? state.observer.observe(target, { childList: !0, subtree: !0, attributeFilter: [ "class", "style" ] }) : attempt < 3 ? setTimeout(() => observeTarget(attempt + 1), 500) : state.observer.observe(document.body, { childList: !0, subtree: !0, attributeFilter: [ "class", "style" ] }); }; observeTarget(); })(); } }; const setupEndscreenSettingsDelegation = (() => { let attached = !1; return () => { if (attached) { return; } attached = !0; const delegator = window.YouTubePlusEventDelegation; const handler = (ev, target) => { const input = target; if (input && input.classList?.contains("ytp-plus-settings-checkbox") && input.closest?.(".endscreen-settings")) { CONFIG.enabled = input.checked; settings.save(); } }; if (delegator?.on) { delegator.on(document, "change", ".endscreen-settings .ytp-plus-settings-checkbox", handler, { passive: !0 }); } else { const changeHandler = ev => { const target = ev.target?.closest?.(".ytp-plus-settings-checkbox"); target && handler(0, target); }; window.YouTubeUtils && YouTubeUtils.cleanupManager ? YouTubeUtils.cleanupManager.registerListener(document, "change", changeHandler, { passive: !0, capture: !0 }) : document.addEventListener("change", changeHandler, { passive: !0, capture: !0 }); } }; })(); const addSettingsUI = () => { const enhancedSlot = $(".endscreen-settings-slot"); const enhancedCard = $(".enhanced-submenu .glass-card"); const host = enhancedSlot || enhancedCard; if (!host || $(".endscreen-settings", host)) { return; } const container = document.createElement("div"); container.className = "ytp-plus-settings-item endscreen-settings"; container.innerHTML = _createHTML(`\n <div>\n <label class="ytp-plus-settings-item-label">${YouTubeUtils.t("endscreenHideLabel")}</label>\n <div class="ytp-plus-settings-item-description">${YouTubeUtils.t("endscreenHideDesc")}${state.removeCount ? ` (${state.removeCount} ${YouTubeUtils.t("removedSuffix").replace("{n}", "")?.trim() || "removed"})` : ""}</div>\n </div>\n <input type="checkbox" class="ytp-plus-settings-checkbox" ${CONFIG.enabled ? "checked" : ""}>\n `); enhancedSlot ? enhancedSlot.replaceWith(container) : host.appendChild(container); setupEndscreenSettingsDelegation(); }; const handlePageChange = debounce(() => { if ("/watch" === location.pathname) { cleanup(); requestIdleCallback ? requestIdleCallback(init) : setTimeout(init, 1); } }, 50); settings.load(); onDomReady(init); const handleSettingsNavClick = e => { const {target} = e; "advanced" === target?.dataset?.section && setTimeout(addSettingsUI, 10); }; state.ytNavigateListenerKey || (state.ytNavigateListenerKey = YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", handlePageChange, { passive: !0 })); YouTubeUtils.cleanupManager.registerListener(document, "youtube-plus-settings-modal-opened", () => setTimeout(addSettingsUI, 25)); state.settingsNavListenerKey || (state.settingsNavListenerKey = YouTubeUtils.cleanupManager.registerListener(document, "click", handleSettingsNavClick, { passive: !0, capture: !0 })); })(); (async function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); let featureEnabled = !0; let stopRandomPlayTimers = null; let scheduleApplyRandomPlay = null; let addButtonRetryTimer = null; let addButtonRetryAttempts = 0; const loadFeatureEnabled = () => window.YouTubeUtils?.loadFeatureEnabled?.("enablePlayAll") ?? !0; const setFeatureEnabled = nextEnabled => { featureEnabled = !1 !== nextEnabled; if (featureEnabled) { try { queueDesktopAddButton(); } catch {} try { "function" == typeof scheduleApplyRandomPlay && scheduleApplyRandomPlay(); } catch {} } else { try { removeButton(); } catch {} try { addButtonRetryTimer && clearTimeout(addButtonRetryTimer); addButtonRetryTimer = null; addButtonRetryAttempts = 0; } catch {} try { "function" == typeof stopRandomPlayTimers && stopRandomPlayTimers(); } catch {} } }; featureEnabled = loadFeatureEnabled(); const {$, $$} = window.YouTubeUtils || {}; const _cm = window.YouTubeUtils?.cleanupManager; const onDomReady = (() => { let ready = "loading" !== document.readyState; const queue = []; ready || document.addEventListener("DOMContentLoaded", () => { ready = !0; for (;queue.length; ) { const cb = queue.shift(); try { cb(); } catch (e) { console.warn("[Play All] DOMReady callback error:", e); } } }, { once: !0 }); return cb => { ready ? cb() : queue.push(cb); }; })(); const t = window.YouTubeUtils?.t || (key => key || ""); const getPlayAllLabel = () => { if ((key => { try { if (window.YouTubePlusI18n?.hasTranslation) { return window.YouTubePlusI18n.hasTranslation(key); } } catch {} return !1; })("playAllButton")) { const localized = t("playAllButton"); if (localized && "playAllButton" !== localized) { return localized; } } return "Play All"; }; const getPlayAllAriaLabel = () => { const localized = t("enablePlayAllLabel"); return localized && "enablePlayAllLabel" !== localized ? localized : getPlayAllLabel(); }; const globalContext = "undefined" != typeof unsafeWindow ? unsafeWindow : window; const gmInfo = globalContext?.GM_info ?? null; const scriptVersion = gmInfo?.script?.version ?? null; if (scriptVersion && /-(alpha|beta|dev|test)$/.test(scriptVersion)) { try { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.info && YouTubeUtils.logger.info("%cytp - YouTube Play All\n", "color: #bf4bcc; font-size: 32px; font-weight: bold", "You are currently running a test version:", scriptVersion); } catch {} } (fn => { "function" == typeof requestIdleCallback ? requestIdleCallback(fn, { timeout: 2e3 }) : setTimeout(fn, 200); })(() => (html => { try { const target = document.head || document.documentElement; if (target && "function" == typeof target.insertAdjacentHTML) { target.insertAdjacentHTML("beforeend", _createHTML(html)); return; } const onReady = () => { try { const t = document.head || document.documentElement; t && "function" == typeof t.insertAdjacentHTML && t.insertAdjacentHTML("beforeend", _createHTML(html)); } catch {} }; onDomReady(onReady); } catch (e) { console.warn("[Play All] Style insertion error:", e); } })("<style>\n .ytp-btn {border-radius: 8px; font-family: 'Roboto', 'Arial', sans-serif; font-size: 1.4rem; line-height: 3.2rem; font-weight: 500; padding: 0 12px; margin-left: 0; user-select: none; white-space: nowrap;} \n .ytp-btn, .ytp-btn > * {text-decoration: none; cursor: pointer;} \n .ytp-badge {border-radius: 8px; padding: 0.2em; font-size: 0.8em; vertical-align: top;} \n .ytp-random-badge, .ytp-random-notice {background-color: #2b66da; color: white;} \n /* Style Play All as a YouTube chip button */\n .ytp-play-all-btn {display:inline-flex;align-items:center;justify-content:center;height:32px;padding:0 12px;white-space:nowrap;flex-shrink:0;max-width:fit-content;border-radius:8px;font-size:1.4rem;line-height:2rem;font-weight:500;background-color:var(--yt-spec-badge-chip-background,rgba(255,255,255,0.1));color:var(--yt-spec-text-primary,#fff);border:none;transition:background-color .2s;cursor:pointer;text-decoration:none;}\n .ytp-play-all-btn:hover {background-color:var(--yt-spec-badge-chip-background-hover,rgba(255,255,255,0.2));} \n html:not([dark]) .ytp-play-all-btn {background-color:var(--yt-spec-badge-chip-background,rgba(0,0,0,0.05));color:var(--yt-spec-text-primary,#0f0f0f);}\n html:not([dark]) .ytp-play-all-btn:hover {background-color:var(--yt-spec-badge-chip-background-hover,rgba(0,0,0,0.1));}\n .ytp-button-row-wrapper {width: 100%; display: block; margin: 0 0 0.6rem 0;} \n .ytp-button-container {display: inline-flex; align-items: center; gap: 0.6em; width: auto; margin: 0; flex-wrap: nowrap; overflow-x: auto; max-width: 100%;} \n /* Ensure Play All sits inside chip bar container flow */\n ytd-feed-filter-chip-bar-renderer .ytp-play-all-btn,\n yt-chip-cloud-renderer .ytp-play-all-btn,\n chip-bar-view-model.ytChipBarViewModelHost .ytp-play-all-btn,\n .ytp-button-container .ytp-play-all-btn {height:32px;line-height:32px;vertical-align:middle;}\n ytd-rich-grid-renderer .ytp-button-row-wrapper {margin-left: 0;} \n /* fetch() API introduces a race condition. This hides the occasional duplicate buttons */\n .ytp-play-all-btn ~ .ytp-play-all-btn {display: none;} \n /* Fix for mobile view */\n ytm-feed-filter-chip-bar-renderer .ytp-btn {margin-right: 12px; padding: 0.4em;} \n body:has(#secondary ytd-playlist-panel-renderer[ytp-random]) .ytp-prev-button.ytp-button, body:has(#secondary ytd-playlist-panel-renderer[ytp-random]) .ytp-next-button.ytp-button:not([ytp-random=\"applied\"]) {display: none !important;} \n #secondary ytd-playlist-panel-renderer[ytp-random] ytd-menu-renderer.ytd-playlist-panel-renderer {height: 1em; visibility: hidden;} \n #secondary ytd-playlist-panel-renderer[ytp-random]:not(:hover) ytd-playlist-panel-video-renderer {filter: blur(2em);} \n #secondary ytd-playlist-panel-renderer[ytp-random] #header {display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;} \n .ytp-random-notice {padding: 0.3em 0.7em; z-index: 1000; white-space: nowrap;} \n </style>")); const getVideoId = url => { try { return new URLSearchParams(new URL(url).search).get("v"); } catch { return null; } }; const queryHTMLElement = selector => { const el = $(selector); return el instanceof HTMLElement ? el : null; }; const getPlayer = () => $("#movie_player"); let id = ""; const apply = (retryCount = 0) => { if ("" === id) { console.warn("[Play All] Channel ID not yet determined"); return; } let parent = null; if ("m.youtube.com" === location.host) { parent = queryHTMLElement("ytm-feed-filter-chip-bar-renderer .chip-bar-contents, ytm-feed-filter-chip-bar-renderer > div"); } else { const desktopParentSelectors = [ "chip-bar-view-model.ytChipBarViewModelHost", "ytd-feed-filter-chip-bar-renderer iron-selector#chips", "ytd-feed-filter-chip-bar-renderer #chips-wrapper", "yt-chip-cloud-renderer #chips", "yt-chip-cloud-renderer .yt-chip-cloud-renderer" ]; for (const selector of desktopParentSelectors) { const candidate = document.querySelector(selector); if (candidate instanceof HTMLElement) { parent = candidate; break; } } } if (null === parent) { const grid = queryHTMLElement("ytd-rich-grid-renderer, ytm-rich-grid-renderer, div.ytChipBarViewModelChipWrapper"); if (!grid) { retryCount < 12 && setTimeout(() => apply(retryCount + 1), 300); return; } const chipBarInGrid = grid.querySelector("chip-bar-view-model.ytChipBarViewModelHost, ytd-feed-filter-chip-bar-renderer iron-selector#chips, ytd-feed-filter-chip-bar-renderer #chips-wrapper, yt-chip-cloud-renderer #chips"); if (chipBarInGrid instanceof HTMLElement) { parent = chipBarInGrid; } else { if (retryCount < 8) { setTimeout(() => apply(retryCount + 1), 300); return; } { let existingContainer = grid.querySelector(".ytp-button-container"); if (!existingContainer) { grid.insertAdjacentHTML("afterbegin", _createHTML('<div class="ytp-button-container"></div>')); existingContainer = grid.querySelector(".ytp-button-container"); } parent = existingContainer instanceof HTMLElement ? existingContainer : null; } } } if (!parent) { console.warn("[Play All] Could not find parent container"); return; } if (parent.querySelector(".ytp-play-all-btn")) { try { window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug && YouTubeUtils.logger.debug("[Play All] Buttons already exist, skipping"); } catch {} return; } const [allPlaylist] = window.location.pathname.endsWith("/videos") ? [ "UULF" ] : window.location.pathname.endsWith("/shorts") ? [ "UUSH" ] : [ "UULV" ]; const playlistSuffix = id.startsWith("UC") ? id.substring(2) : id; parent.insertAdjacentHTML("beforeend", _createHTML(`<a class="ytp-btn ytp-play-all-btn" href="/playlist?list=${allPlaylist}${playlistSuffix}&playnext=1&ytp-random=random&ytp-random-initial=1" title="${getPlayAllAriaLabel()}" aria-label="${getPlayAllAriaLabel()}">${getPlayAllLabel()}</a>`)); const navigate = href => { window.location.assign(href); }; if ("m.youtube.com" === location.host) { if (!parent.hasAttribute("data-ytp-delegated")) { parent.setAttribute("data-ytp-delegated", "true"); parent.addEventListener("click", event => { const btn = event.target.closest(".ytp-btn"); if (btn && btn.href) { event.preventDefault(); navigate(btn.href); } }); } } else if (!parent.hasAttribute("data-ytp-delegated")) { parent.setAttribute("data-ytp-delegated", "true"); parent.addEventListener("click", event => { const btn = event.target.closest(".ytp-play-all-btn"); if (btn && btn.href) { event.preventDefault(); event.stopPropagation(); navigate(btn.href); } }); } }; let observerFrame = 0; const runObserverWork = () => { observerFrame = 0; if (featureEnabled) { removeButton(); apply(); } }; const observer = new MutationObserver(() => { featureEnabled && (observerFrame || (observerFrame = "function" != typeof requestAnimationFrame ? setTimeout(runObserverWork, 16) : requestAnimationFrame(runObserverWork))); }); const addButton = async () => { observer.disconnect(); if (!featureEnabled) { return; } if (!(window.location.pathname.endsWith("/videos") || window.location.pathname.endsWith("/shorts") || window.location.pathname.endsWith("/streams"))) { return; } const observeTarget = document.querySelector("ytd-rich-grid-renderer") || document.querySelector("chip-bar-view-model.ytChipBarViewModelHost") || $("ytm-feed-filter-chip-bar-renderer .iron-selected, ytm-feed-filter-chip-bar-renderer .chip-bar-contents .selected"); observeTarget && observer.observe(observeTarget, { attributes: !0, childList: !1, subtree: !1 }); if ($(".ytp-play-all-btn")) { return; } const resolvedFromDom = (() => { try { const metaChannel = document.querySelector('meta[itemprop="channelId"]'); const metaValue = metaChannel?.getAttribute("content"); if (metaValue && /^UC[a-zA-Z0-9_-]{22}$/.test(metaValue)) { return metaValue; } const browseNode = document.querySelector('ytd-browse[page-subtype="channels"]'); const attrId = browseNode?.getAttribute?.("channel-id") || browseNode?.getAttribute?.("external-id"); if (attrId && /^UC[a-zA-Z0-9_-]{22}$/.test(attrId)) { return attrId; } const href = location.href; const fromUrl = href.match(/\/channel\/(UC[a-zA-Z0-9_-]{22})/); if (fromUrl?.[1]) { return fromUrl[1]; } const initialData = window.ytInitialData; const headerId = initialData?.header?.c4TabbedHeaderRenderer?.channelId || initialData?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows?.[0]?.metadataParts?.find?.(p => /^UC[a-zA-Z0-9_-]{22}$/.test(p?.text?.content || ""))?.text?.content; if (headerId && /^UC[a-zA-Z0-9_-]{22}$/.test(headerId)) { return headerId; } if (window.ytcfg?.get) { const cfgId = window.ytcfg.get("CHANNEL_ID"); if (cfgId && /^UC[a-zA-Z0-9_-]{22}$/.test(cfgId)) { return cfgId; } } } catch (e) { console.warn("[Play All] Failed to resolve channel ID from DOM:", e); } return null; })(); if (resolvedFromDom) { id = resolvedFromDom; apply(); } else { try { const canonical = $('link[rel="canonical"]'); if (canonical && canonical.href) { const match = canonical.href.match(/\/channel\/(UC[a-zA-Z0-9_-]{22})/); if (match && match[1]) { id = match[1]; apply(); return; } const handleMatch = canonical.href.match(/\/@([^\/]+)/); if (handleMatch) { const pageData = $('ytd-browse[page-subtype="channels"]'); if (pageData) { const channelId = pageData.getAttribute("channel-id"); if (channelId && channelId.startsWith("UC")) { id = channelId; apply(); return; } } } } } catch (e) { console.warn("[Play All] Error extracting channel ID from canonical:", e); } try { const currentUrl = location.href; const parsedUrl = new URL(currentUrl); if ("www.youtube.com" !== parsedUrl.hostname && "youtube.com" !== parsedUrl.hostname && "m.youtube.com" !== parsedUrl.hostname) { console.warn("[Play All] Skipping fetch for non-YouTube URL"); return; } const html = await (await fetch(currentUrl)).text(); const canonicalMatch = html.match(/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[a-zA-Z0-9_-]{22})"/); if (canonicalMatch && canonicalMatch[1]) { id = canonicalMatch[1]; } else { const channelIdMatch = html.match(/"channelId":"(UC[a-zA-Z0-9_-]{22})"/); channelIdMatch && channelIdMatch[1] && (id = channelIdMatch[1]); } id ? apply() : console.warn("[Play All] Could not extract channel ID"); } catch (e) { console.error("[Play All] Error fetching channel data:", e); } } }; const stopAddButtonRetries = () => { addButtonRetryTimer && clearTimeout(addButtonRetryTimer); addButtonRetryTimer = null; addButtonRetryAttempts = 0; }; const queueDesktopAddButton = (reset = !0) => { if ("m.youtube.com" === location.host) { addButton(); return; } reset && stopAddButtonRetries(); const run = () => { if (featureEnabled) { if (window.location.pathname.endsWith("/videos") || window.location.pathname.endsWith("/shorts") || window.location.pathname.endsWith("/streams")) { addButton(); if (document.querySelector(".ytp-play-all-btn")) { stopAddButtonRetries(); } else if (addButtonRetryAttempts >= 30) { stopAddButtonRetries(); } else { addButtonRetryAttempts += 1; addButtonRetryTimer = setTimeout(run, 300); } } else { stopAddButtonRetries(); } } else { stopAddButtonRetries(); } }; run(); }; const removeButton = () => { $$(".ytp-play-all-btn, .ytp-random-badge, .ytp-random-notice").forEach(element => element.remove()); }; if ("m.youtube.com" === location.host) { let lastUrl = location.href; const checkUrlChange = () => { if (location.href !== lastUrl) { lastUrl = location.href; addButton(); } }; const _ytpNavHandler = () => setTimeout(checkUrlChange, 50); if (_cm?.registerListener) { _cm.registerListener(window, "ytp-history-navigate", _ytpNavHandler, { passive: !0 }); _cm.registerListener(window, "popstate", checkUrlChange, { passive: !0 }); } else { window.addEventListener("ytp-history-navigate", _ytpNavHandler, { passive: !0 }); window.addEventListener("popstate", checkUrlChange, { passive: !0 }); } addButton(); } else { const _navStartHandler = () => { stopAddButtonRetries(); removeButton(); id = ""; }; const _navFinishHandler = () => { setTimeout(() => queueDesktopAddButton(), 120); setTimeout(() => queueDesktopAddButton(!1), 800); }; const _pageshowHandler = () => setTimeout(() => queueDesktopAddButton(), 120); const _visChangeHandler = () => { "visible" === document.visibilityState && queueDesktopAddButton(); }; if (_cm?.registerListener) { _cm.registerListener(window, "yt-navigate-start", _navStartHandler); _cm.registerListener(window, "yt-navigate-finish", _navFinishHandler); _cm.registerListener(document, "yt-page-data-updated", _navFinishHandler); _cm.registerListener(window, "pageshow", _pageshowHandler); _cm.registerListener(document, "visibilitychange", _visChangeHandler); } else { window.addEventListener("yt-navigate-start", _navStartHandler); window.addEventListener("yt-navigate-finish", _navFinishHandler); document.addEventListener("yt-page-data-updated", _navFinishHandler); window.addEventListener("pageshow", _pageshowHandler); document.addEventListener("visibilitychange", _visChangeHandler); } try { setTimeout(() => queueDesktopAddButton(), 300); } catch {} } const _settingsUpdHandler = e => { try { const nextEnabled = !1 !== e?.detail?.enablePlayAll; if (nextEnabled === featureEnabled) { return; } setFeatureEnabled(nextEnabled); } catch { setFeatureEnabled(loadFeatureEnabled()); } }; _cm?.registerListener ? _cm.registerListener(window, "youtube-plus-settings-updated", _settingsUpdHandler) : window.addEventListener("youtube-plus-settings-updated", _settingsUpdHandler); (() => { if ("m.youtube.com" === location.host) { return; } const getRandomConfig = () => { const params = new URLSearchParams(window.location.search); const modeParam = params.get("ytp-random"); if (!modeParam || "0" === modeParam) { return null; } const list = params.get("list") || ""; return list ? { params, mode: "random", list, storageKey: `ytp-random-${list}` } : null; }; const getStorage = storageKey => { try { return JSON.parse(localStorage.getItem(storageKey) || "{}"); } catch { return {}; } }; const isWatched = (storageKey, videoId) => getStorage(storageKey)[videoId] || !1; const markWatched = (storageKey, videoId) => { localStorage.setItem(storageKey, JSON.stringify({ ...getStorage(storageKey), [videoId]: !0 })); document.querySelectorAll("#wc-endpoint[href*=zsA3X40nz9w]").forEach(element => element.parentElement.setAttribute("hidden", "")); }; const playNextRandom = (cfg, reload = !1) => { const playerInstance = getPlayer(); playerInstance && "function" == typeof playerInstance.pauseVideo && playerInstance.pauseVideo(); const videos = Object.entries(getStorage(cfg.storageKey)).filter(([_, watched]) => !watched); const params = new URLSearchParams(window.location.search); if (0 === videos.length) { return; } let videoIndex = Math.floor(Math.random() * videos.length); videoIndex < 0 && (videoIndex = 0); videoIndex >= videos.length && (videoIndex = videos.length - 1); if (reload) { params.set("v", videos[videoIndex][0]); params.set("ytp-random", cfg.mode); params.delete("t"); params.delete("index"); params.delete("ytp-random-initial"); window.location.href = `${window.location.pathname}?${params.toString()}`; } else { try { ((v, list, ytpRandom = null) => { if ("m.youtube.com" === location.host) { const url = `/watch?v=${v}&list=${list}${null !== ytpRandom ? `&ytp-random=${ytpRandom}` : ""}`; window.location.href = url; } else { try { const playlistPanel = $("ytd-playlist-panel-renderer #items"); if (playlistPanel) { const redirector = document.createElement("a"); redirector.className = "yt-simple-endpoint style-scope ytd-playlist-panel-video-renderer"; redirector.setAttribute("hidden", ""); redirector.data = { commandMetadata: { webCommandMetadata: { url: `/watch?v=${v}&list=${list}${null !== ytpRandom ? `&ytp-random=${ytpRandom}` : ""}`, webPageType: "WEB_PAGE_TYPE_WATCH", rootVe: 3832 } }, watchEndpoint: { videoId: v, playlistId: list } }; playlistPanel.append(redirector); redirector.click(); } else { const url = `/watch?v=${v}&list=${list}${null !== ytpRandom ? `&ytp-random=${ytpRandom}` : ""}`; window.location.href = url; } } catch { const url = `/watch?v=${v}&list=${list}${null !== ytpRandom ? `&ytp-random=${ytpRandom}` : ""}`; window.location.href = url; } } })(videos[videoIndex][0], params.get("list"), cfg.mode); } catch (error) { console.error("[Play All] Error using redirect(), falling back to manual redirect:", error); const redirector = document.createElement("a"); redirector.className = "yt-simple-endpoint style-scope ytd-playlist-panel-video-renderer"; redirector.setAttribute("hidden", ""); redirector.data = { commandMetadata: { webCommandMetadata: { url: `/watch?v=${videos[videoIndex][0]}&list=${params.get("list")}&ytp-random=${cfg.mode}`, webPageType: "WEB_PAGE_TYPE_WATCH", rootVe: 3832 } }, watchEndpoint: { videoId: videos[videoIndex][0], playlistId: params.get("list") } }; const listContainer = $("ytd-playlist-panel-renderer #items"); listContainer instanceof HTMLElement ? listContainer.append(redirector) : document.body.appendChild(redirector); redirector.click(); } } }; let applyRetryTimeoutId = null; let progressIntervalId = null; stopRandomPlayTimers = () => { applyRetryTimeoutId && ("object" == typeof applyRetryTimeoutId && applyRetryTimeoutId.stop ? applyRetryTimeoutId.stop() : clearTimeout(applyRetryTimeoutId)); applyRetryTimeoutId = null; progressIntervalId && "boolean" != typeof progressIntervalId && clearInterval(progressIntervalId); progressIntervalId = null; }; const applyRandomPlay = cfg => { if (!featureEnabled) { return; } if (!window.location.pathname.endsWith("/watch")) { return; } const playlistContainer = $("#secondary ytd-playlist-panel-renderer"); if (null === playlistContainer) { return; } if (playlistContainer.hasAttribute("ytp-random")) { return; } playlistContainer.setAttribute("ytp-random", "applied"); const headerContainer = playlistContainer.querySelector("#header"); headerContainer && !headerContainer.querySelector(".ytp-random-notice") && headerContainer.insertAdjacentHTML("beforeend", _createHTML('<span class="ytp-random-notice">Play All mode</span>')); const storage = getStorage(cfg.storageKey); const anchors = []; [ "#wc-endpoint", "ytd-playlist-panel-video-renderer a#wc-endpoint", "ytd-playlist-panel-video-renderer a", "a#video-title", '#secondary ytd-playlist-panel-renderer a[href*="/watch?"]' ].forEach(sel => { playlistContainer.querySelectorAll(sel).forEach(a => { a instanceof Element && "A" === a.tagName && anchors.push(a); }); }); const uniq = []; const seen = new Set; anchors.forEach(a => { const href = a.href || a.getAttribute("href") || ""; if (!seen.has(href)) { seen.add(href); uniq.push(a); } }); uniq.forEach(element => { let videoId = null; try { videoId = new URL(element.href, window.location.origin).searchParams.get("v"); } catch { videoId = new URLSearchParams(element.search || "").get("v"); } if (!videoId) { return; } isWatched(cfg.storageKey, videoId) || (storage[videoId] = !1); try { const u = new URL(element.href, window.location.origin); u.searchParams.set("ytp-random", cfg.mode); element.href = u.toString(); } catch {} element.setAttribute("data-ytp-random-link", "true"); const entryKey = getVideoId(element.href); isWatched(cfg.storageKey, entryKey) && element.parentElement?.setAttribute("hidden", ""); }); if (playlistContainer && !playlistContainer.hasAttribute("data-ytp-random-delegated")) { playlistContainer.setAttribute("data-ytp-random-delegated", "true"); playlistContainer.addEventListener("click", event => { const link = event.target.closest("a[data-ytp-random-link]"); if (link && link.href) { event.preventDefault(); (href => { window.location.href = href; })(link.href); } }); } localStorage.setItem(cfg.storageKey, JSON.stringify(storage)); if ("1" === cfg.params.get("ytp-random-initial") || isWatched(cfg.storageKey, getVideoId(location.href))) { playNextRandom(cfg); return; } const header = playlistContainer.querySelector("h3 a"); if (header && "A" === header.tagName) { const anchorHeader = header; anchorHeader.insertAdjacentHTML("beforeend", _createHTML(' <span class="ytp-badge ytp-random-badge">Play All <span style="font-size: 2rem; vertical-align: top">×</span></span>')); anchorHeader.href = "#"; const badge = anchorHeader.querySelector(".ytp-random-badge"); badge && badge.addEventListener("click", event => { event.preventDefault(); localStorage.removeItem(cfg.storageKey); const params = new URLSearchParams(location.search); params.delete("ytp-random"); window.location.href = `${window.location.pathname}?${params.toString()}`; }); } const _keydownHandler = event => { if (event.shiftKey && "n" === event.key.toLowerCase()) { event.stopImmediatePropagation(); event.preventDefault(); const videoId = getVideoId(location.href); markWatched(cfg.storageKey, videoId); playNextRandom(cfg, !0); } }; _cm?.registerListener ? _cm.registerListener(document, "keydown", _keydownHandler, !0) : document.addEventListener("keydown", _keydownHandler, !0); if (progressIntervalId) { return; } const videoEl = $("video"); if (!videoEl) { return; } videoEl.addEventListener("timeupdate", () => { const videoId = getVideoId(location.href); const params = new URLSearchParams(location.search); params.set("ytp-random", cfg.mode); window.history.replaceState({}, "", `${window.location.pathname}?${params.toString()}`); const player = getPlayer(); if (!player || "function" != typeof player.getProgressState) { return; } const progressState = player.getProgressState(); if (!progressState || "number" != typeof progressState.current || "number" != typeof progressState.duration) { return; } if (!$(".ad-interrupting")) { progressState.current / progressState.duration >= .9 && videoId && markWatched(cfg.storageKey, videoId); if (progressState.current >= progressState.duration - 2) { "function" == typeof player.pauseVideo && player.pauseVideo(); "function" == typeof player.seekTo && player.seekTo(0); playNextRandom(cfg); } } const nextButton = $('#ytd-player .ytp-next-button.ytp-button:not([ytp-random="applied"])'); if (nextButton instanceof HTMLElement) { const newButton = document.createElement("span"); newButton.className = nextButton.className; newButton.innerHTML = _createHTML(nextButton.innerHTML); nextButton.replaceWith(newButton); newButton.setAttribute("ytp-random", "applied"); newButton.addEventListener("click", () => { videoId && markWatched(cfg.storageKey, videoId); playNextRandom(cfg); }); } }, { passive: !0 }); progressIntervalId = !0; }; scheduleApplyRandomPlay = () => { if (!featureEnabled) { return; } stopRandomPlayTimers(); if (!window.location.pathname.endsWith("/watch")) { return; } const scheduler = window.YouTubeUtils?.createRetryScheduler?.({ check: () => { const cfg = getRandomConfig(); if (!cfg) { return !1; } try { const current = localStorage.getItem(cfg.storageKey); current && Array.isArray(JSON.parse(current)) && localStorage.removeItem(cfg.storageKey); } catch { localStorage.removeItem(cfg.storageKey); } applyRandomPlay(cfg); return !!document.querySelector("#secondary ytd-playlist-panel-renderer[ytp-random]"); }, maxAttempts: 30, interval: 250 }); scheduler && (applyRetryTimeoutId = scheduler); }; const onNavigate = () => { if (featureEnabled) { stopRandomPlayTimers(); scheduleApplyRandomPlay(); } else { stopRandomPlayTimers(); } }; onNavigate(); const _navFinishRandom = () => setTimeout(onNavigate, 200); _cm?.registerListener ? _cm.registerListener(window, "yt-navigate-finish", _navFinishRandom) : window.addEventListener("yt-navigate-finish", _navFinishRandom); })(); })().catch(error => console.error("%cytp - YouTube Play All\n", "color: #bf4bcc; font-size: 32px; font-weight: bold", error)); !(function() { "use strict"; let featureEnabled = !0; let activeCleanup = null; const loadFeatureEnabled = () => window.YouTubeUtils?.loadFeatureEnabled?.("enableResumeTime") ?? !0; const setFeatureEnabled = nextEnabled => { featureEnabled = !1 !== nextEnabled; if (featureEnabled) { try { initResume(); } catch {} } else { const existingOverlay = byId(OVERLAY_ID); if (existingOverlay) { try { existingOverlay.remove(); } catch {} } if ("function" == typeof activeCleanup) { try { activeCleanup(); } catch {} activeCleanup = null; } } }; featureEnabled = loadFeatureEnabled(); const {$, byId} = window.YouTubeUtils || {}; const onDomReady = (() => { let ready = "loading" !== document.readyState; const queue = []; ready || document.addEventListener("DOMContentLoaded", () => { ready = !0; for (;queue.length; ) { const cb = queue.shift(); try { cb(); } catch (e) { console.error("[YouTube+] DOMReady callback error:", e); } } }, { once: !0 }); return cb => { ready ? cb() : queue.push(cb); }; })(); const setupResumeDelegation = (() => { let attached = !1; return () => { if (attached) { return; } attached = !0; const delegator = window.YouTubePlusEventDelegation; const handler = (ev, target) => { const action = target?.dataset?.ytpResumeAction; if (!action) { return; } const wrap = target.closest(".ytp-resume-overlay"); wrap && ("resume" === action ? wrap.dispatchEvent(new CustomEvent("ytp:resume", { bubbles: !0 })) : "restart" === action && wrap.dispatchEvent(new CustomEvent("ytp:restart", { bubbles: !0 }))); }; if (delegator?.on) { delegator.on(document, "click", ".ytp-resume-btn", handler); delegator.on(document, "keydown", ".ytp-resume-btn", (ev, target) => { if ("Enter" === ev.key || " " === ev.key) { ev.preventDefault(); handler(0, target); } }); } else { const clickHandler = ev => { const target = ev.target?.closest?.(".ytp-resume-btn"); target && handler(0, target); }; const keyHandler = ev => { const target = ev.target?.closest?.(".ytp-resume-btn"); if (target && ("Enter" === ev.key || " " === ev.key)) { ev.preventDefault(); handler(0, target); } }; if (window.YouTubeUtils?.cleanupManager?.registerListener) { window.YouTubeUtils.cleanupManager.registerListener(document, "click", clickHandler, !0); window.YouTubeUtils.cleanupManager.registerListener(document, "keydown", keyHandler, !0); } else { document.addEventListener("click", clickHandler, !0); document.addEventListener("keydown", keyHandler, !0); } } }; })(); const OVERLAY_ID = "yt-resume-overlay"; const _localFallback = { resumePlayback: { en: "Resume playback?", ru: "Продолжить воспроизведение?" }, resume: { en: "Resume", ru: "Продолжить" }, startOver: { en: "Start over", ru: "Начать сначала" } }; const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const t = (key, params = {}) => { const U = window.YouTubeUtils; if (U?.t) { return U.t(key, params); } const htmlLang = document.documentElement.lang || "en"; const lang = htmlLang.startsWith("ru") ? "ru" : "en"; const val = _localFallback[key]?.[lang] || _localFallback[key]?.en || key; if (!params || 0 === Object.keys(params).length) { return val; } let result = val; for (const [k, v] of Object.entries(params)) { result = result.replace(new RegExp(`\\{${escapeRegex(k)}\\}`, "g"), String(v)); } return result; }; const readStorage = () => { try { return JSON.parse(localStorage.getItem("youtube_resume_times_v1") || "{}"); } catch { return {}; } }; const getVideoId = () => { try { const urlParams = new URLSearchParams(window.location.search); const videoIdFromUrl = urlParams.get("v"); if (videoIdFromUrl) { return videoIdFromUrl; } const meta = $('link[rel="canonical"]'); if (meta && meta.href) { const u = new URL(meta.href); const vParam = u.searchParams.get("v"); if (vParam) { return vParam; } const pathMatch = u.pathname.match(/\/(watch|shorts)\/([^\/\?]+)/); if (pathMatch && pathMatch[2]) { return pathMatch[2]; } } if (window.ytInitialPlayerResponse && window.ytInitialPlayerResponse.videoDetails && window.ytInitialPlayerResponse.videoDetails.videoId) { return window.ytInitialPlayerResponse.videoDetails.videoId; } const pathMatch = window.location.pathname.match(/\/(watch|shorts)\/([^\/\?]+)/); return pathMatch && pathMatch[2] ? pathMatch[2] : null; } catch { return null; } }; const createOverlay = (seconds, onResume, onRestart) => { if (byId(OVERLAY_ID)) { return null; } const wrap = document.createElement("div"); wrap.id = OVERLAY_ID; wrap.setAttribute("role", "alertdialog"); wrap.setAttribute("aria-label", t("resumePlayback") || "Resume playback"); const player = $("#movie_player"); const inPlayer = !!player; const resumeOverlayStyles = "\n .ytp-resume-overlay{min-width:180px;max-width:36vw;background:rgba(24, 24, 24, 0.3);color:var(--yt-spec-text-primary,#fff);padding:12px 14px;border-radius:12px;backdrop-filter:blur(8px) saturate(150%);-webkit-backdrop-filter:blur(8px) saturate(150%);box-shadow:0 14px 40px rgba(0,0,0,0.48);border:1.25px solid rgba(255,255,255,0.06);font-family:Arial,Helvetica,sans-serif;display:flex;flex-direction:column;align-items:center;text-align:center;animation:ytp-resume-fadein 0.3s ease-out}\n @keyframes ytp-resume-fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}\n .ytp-resume-overlay .ytp-resume-title{font-weight:600;margin-bottom:8px;font-size:13px}\n .ytp-resume-overlay .ytp-resume-actions{display:flex;gap:8px;justify-content:center;margin-top:6px}\n .ytp-resume-overlay .ytp-resume-btn{padding:6px 12px;border-radius:8px;border:none;cursor:pointer;font-size:12px;font-weight:500;transition:all 0.2s ease;outline:none}\n .ytp-resume-overlay .ytp-resume-btn:focus{box-shadow:0 0 0 2px rgba(255,255,255,0.3);outline:2px solid transparent}\n .ytp-resume-overlay .ytp-resume-btn:hover{transform:translateY(-1px)}\n .ytp-resume-overlay .ytp-resume-btn:active{transform:translateY(0)}\n .ytp-resume-overlay .ytp-resume-btn.primary{background:#1e88e5;color:#fff}\n .ytp-resume-overlay .ytp-resume-btn.primary:hover{background:#1976d2}\n .ytp-resume-overlay .ytp-resume-btn.ghost{background:rgba(255,255,255,0.06);color:#fff}\n .ytp-resume-overlay .ytp-resume-btn.ghost:hover{background:rgba(255,255,255,0.12)}\n "; try { if (window.YouTubeUtils && YouTubeUtils.StyleManager) { YouTubeUtils.StyleManager.add("ytp-resume-overlay-styles", resumeOverlayStyles); } else if (!byId("ytp-resume-overlay-styles")) { const s = document.createElement("style"); s.id = "ytp-resume-overlay-styles"; s.textContent = resumeOverlayStyles; (document.head || document.documentElement).appendChild(s); } } catch (e) { console.warn("[YouTube+] Failed to inject resume overlay styles:", e); } if (inPlayer) { try { const playerStyle = window.getComputedStyle(player); "static" === playerStyle.position && (player.style.position = "relative"); } catch {} wrap.className = "ytp-resume-overlay"; wrap.style.cssText = "position:absolute;left:50%;bottom:5%;transform:translate(-50%,-50%);z-index:9999;pointer-events:auto;"; player.appendChild(wrap); } else { wrap.className = "ytp-resume-overlay"; wrap.style.cssText = "position:fixed;left:50%;bottom:5%;transform:translate(-50%,-50%);z-index:1200;pointer-events:auto;"; document.body.appendChild(wrap); } const title = document.createElement("div"); title.className = "ytp-resume-title"; title.textContent = `${t("resumePlayback")} (${formatTime(seconds)})`; const btnResume = document.createElement("button"); btnResume.className = "ytp-resume-btn primary"; btnResume.textContent = t("resume"); btnResume.setAttribute("aria-label", `${t("resume")} at ${formatTime(seconds)}`); btnResume.tabIndex = 0; btnResume.dataset.ytpResumeAction = "resume"; const btnRestart = document.createElement("button"); btnRestart.className = "ytp-resume-btn ghost"; btnRestart.textContent = t("startOver"); btnRestart.setAttribute("aria-label", t("startOver")); btnRestart.tabIndex = 0; btnRestart.dataset.ytpResumeAction = "restart"; setupResumeDelegation(); wrap.addEventListener("ytp:resume", () => (() => { try { onResume(); } catch (err) { console.error("[YouTube+] Resume error:", err); } try { wrap.remove(); } catch {} })(), { once: !0 }); wrap.addEventListener("ytp:restart", () => (() => { try { onRestart(); } catch (err) { console.error("[YouTube+] Restart error:", err); } try { wrap.remove(); } catch {} })(), { once: !0 }); const actions = document.createElement("div"); actions.className = "ytp-resume-actions"; actions.appendChild(btnResume); actions.appendChild(btnRestart); wrap.appendChild(title); wrap.appendChild(actions); try { requestAnimationFrame(() => { btnResume.focus(); }); } catch {} const to = setTimeout(() => { try { wrap.remove(); } catch {} }, 1e4); const cancel = () => clearTimeout(to); window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.register(() => { try { cancel(); } catch {} try { wrap.remove(); } catch {} }); return cancel; }; const formatTime = secs => { const s = Math.floor(secs % 60).toString().padStart(2, "0"); const m = Math.floor(secs / 60 % 60).toString(); const h = Math.floor(secs / 3600); return h ? `${h}:${m.padStart(2, "0")}:${s}` : `${m}:${s}`; }; const attachResumeHandlers = videoEl => { if (!featureEnabled) { return null; } if (!videoEl || "VIDEO" !== videoEl.tagName) { console.warn("[YouTube+] Invalid video element for resume handlers"); return; } if (videoEl._ytpResumeAttached) { return; } videoEl._ytpResumeAttached = !0; const getCurrentVideoId = () => getVideoId(); const vid = getCurrentVideoId(); if (!vid) { return; } const storage = readStorage(); const saved = storage[vid]; let timeUpdateHandler = null; let lastSavedAt = 0; const startSaving = () => { if (!timeUpdateHandler) { timeUpdateHandler = () => { try { const currentVid = getCurrentVideoId(); if (!currentVid) { return; } const t = Math.floor(videoEl.currentTime || 0); const now = Date.now(); if (t && (!lastSavedAt || now - lastSavedAt > 800)) { const s = readStorage(); s[currentVid] = t; (obj => { try { localStorage.setItem("youtube_resume_times_v1", JSON.stringify(obj)); } catch (e) { console.warn("[YouTube+] Failed to save resume time:", e); } })(s); lastSavedAt = now; } } catch (e) { console.warn("[YouTube+] Error saving playback time:", e); } }; videoEl.addEventListener("timeupdate", timeUpdateHandler, { passive: !0 }); window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.register(() => { try { videoEl.removeEventListener("timeupdate", timeUpdateHandler); } catch {} }); } }; if (saved && saved > 5 && !byId(OVERLAY_ID)) { const cancelTimeout = createOverlay(saved, () => { try { videoEl.currentTime = saved; videoEl.play(); } catch (e) { console.error("[YouTube+] Failed to resume playback:", e); } }, () => { try { videoEl.currentTime = 0; videoEl.play(); } catch (e) { console.error("[YouTube+] Failed to start over:", e); } }); try { const overlayEl = byId(OVERLAY_ID); overlayEl && vid && (overlayEl.dataset.vid = vid); } catch {} window.YouTubeUtils && YouTubeUtils.cleanupManager && cancelTimeout && YouTubeUtils.cleanupManager.register(cancelTimeout); } const onPlay = () => startSaving(); const onPause = () => (() => { if (timeUpdateHandler) { try { videoEl.removeEventListener("timeupdate", timeUpdateHandler); } catch {} timeUpdateHandler = null; lastSavedAt = 0; } })(); videoEl.addEventListener("play", onPlay, { passive: !0 }); videoEl.addEventListener("pause", onPause, { passive: !0 }); const cleanupHandlers = () => { try { videoEl.removeEventListener("play", onPlay); videoEl.removeEventListener("pause", onPause); timeUpdateHandler && videoEl.removeEventListener("timeupdate", timeUpdateHandler); delete videoEl._ytpResumeAttached; } catch (err) { console.error("[YouTube+] Resume cleanup error:", err); } }; window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.register(cleanupHandlers); activeCleanup = cleanupHandlers; return cleanupHandlers; }; const initResume = () => { if (!featureEnabled) { const existingOverlay = byId(OVERLAY_ID); if (existingOverlay) { try { existingOverlay.remove(); } catch {} } return; } if ("/watch" !== window.location.pathname) { const existingOverlay = byId(OVERLAY_ID); existingOverlay && existingOverlay.remove(); return; } const currentVid = getVideoId(); const existingOverlay = byId(OVERLAY_ID); if (existingOverlay) { try { existingOverlay.dataset && existingOverlay.dataset.vid === currentVid || existingOverlay.remove(); } catch { try { existingOverlay.remove(); } catch {} } } const videoEl = (() => { const selectors = [ "video.html5-main-video", "video.video-stream", "#movie_player video", "video" ]; for (const selector of selectors) { const video = $(selector); if (video && "VIDEO" === video.tagName) { return video; } } return null; })(); videoEl ? attachResumeHandlers(videoEl) : setTimeout(initResume, 500); }; const onNavigate = () => setTimeout(initResume, 150); onDomReady(initResume); window && window.document && (window.YouTubeUtils && YouTubeUtils.cleanupManager ? YouTubeUtils.cleanupManager.registerListener(document, "yt-navigate-finish", onNavigate, { passive: !0 }) : document.addEventListener("yt-navigate-finish", onNavigate, { passive: !0 })); const settingsUpdatedHandler = e => { try { const nextEnabled = !1 !== e?.detail?.enableResumeTime; if (nextEnabled === featureEnabled) { return; } setFeatureEnabled(nextEnabled); } catch { setFeatureEnabled(loadFeatureEnabled()); } }; window.YouTubeUtils && YouTubeUtils.cleanupManager ? YouTubeUtils.cleanupManager.registerListener(window, "youtube-plus-settings-updated", settingsUpdatedHandler) : window.addEventListener("youtube-plus-settings-updated", settingsUpdatedHandler); })(); !(function() { "use strict"; const initZoomModule = () => { const _createHTML = window._ytplusCreateHTML || (s => s); let featureEnabled = !0; const loadFeatureEnabled = () => window.YouTubeUtils?.loadFeatureEnabled?.("enableZoom") ?? !0; const setFeatureEnabled = nextEnabled => { featureEnabled = !1 !== nextEnabled; if (featureEnabled) { try { initZoom(); } catch {} } else { (() => { try { const ui = byId("ytp-zoom-control"); ui && ui.remove(); } catch {} try { const styles = byId("ytp-zoom-styles"); styles && styles.remove(); } catch {} try { const video = findVideoElement(); if (video) { video.style.transform = ""; video.style.willChange = ""; video.style.transition = ""; video.style.cursor = ""; } } catch {} })(); } }; featureEnabled = loadFeatureEnabled(); const {$, byId} = window.YouTubeUtils || {}; const ZOOM_PAN_STORAGE_KEY = "ytp_zoom_pan"; const RESTORE_LOG_KEY = "ytp_zoom_restore_log"; const DEFAULT_ZOOM = 1; const MIN_ZOOM = .5; const MAX_ZOOM = 2.5; const ZOOM_STEP = .05; const FULLSCREEN_APPLY_DELAY = 80; const FULLSCREEN_APPLY_RETRIES = 4; const FULLSCREEN_APPLY_RETRY_DELAY = 120; function readZoomPan() { try { const raw = localStorage.getItem(ZOOM_PAN_STORAGE_KEY); if (!raw) { return { zoom: DEFAULT_ZOOM, panX: 0, panY: 0 }; } const obj = JSON.parse(raw); const zoom = Number(obj && obj.zoom) || DEFAULT_ZOOM; const panX = Number(obj && obj.panX) || 0; const panY = Number(obj && obj.panY) || 0; return { zoom, panX, panY }; } catch { return { zoom: DEFAULT_ZOOM, panX: 0, panY: 0 }; } } function saveZoomPan(zoom, panX, panY) { try { const obj = { zoom: Number(zoom) || DEFAULT_ZOOM, panX: Number(panX) || 0, panY: Number(panY) || 0 }; localStorage.setItem(ZOOM_PAN_STORAGE_KEY, JSON.stringify(obj)); } catch (e) { console.warn("[YouTube+] Failed to save zoom/pan settings:", e); } } function logRestoreEvent(evt) { try { const entry = Object.assign({ time: (new Date).toISOString() }, evt); try { const raw = sessionStorage.getItem(RESTORE_LOG_KEY); const arr = raw ? JSON.parse(raw) : []; arr.push(entry); arr.length > 200 && arr.splice(0, arr.length - 200); sessionStorage.setItem(RESTORE_LOG_KEY, JSON.stringify(arr)); } catch {} ("undefined" != typeof window && window.YTP_DEBUG || window.YouTubePlusConfig?.debug) && console.warn("[YouTube+] Zoom restore:", entry); } catch {} } const findVideoElement = () => { const selectors = [ "#movie_player video", "video.video-stream", "video" ]; for (const s of selectors) { const v = $(s); if (v && "VIDEO" === v.tagName) { return v; } } return null; }; let _lastTransformApplied = ""; let _isApplyingTransform = !1; const applyZoomToVideo = (videoEl, zoom, panX = 0, panY = 0, skipTransformTracking = !1, skipTransition = !1) => { if (!videoEl) { return; } const container = videoEl.parentElement || videoEl; try { skipTransformTracking || (_isApplyingTransform = !0); container.style.overflow = "visible"; container.style.position && "static" !== container.style.position || (container.style.position = "relative"); videoEl.style.transformOrigin = "center center"; const transformStr = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${zoom.toFixed(3)})`; videoEl.style.transform = transformStr; skipTransformTracking || (_lastTransformApplied = transformStr); videoEl.style.willChange = 1 !== zoom ? "transform" : "auto"; videoEl.style.transition = skipTransition ? "none" : "transform .08s ease-out"; skipTransformTracking || setTimeout(() => { _isApplyingTransform = !1; }, 100); } catch (e) { console.error("[YouTube+] applyZoomToVideo error:", e); _isApplyingTransform = !1; } }; function createZoomUI() { const player = $("#movie_player"); if (!player) { return null; } if (byId("ytp-zoom-control")) { return byId("ytp-zoom-control"); } if (!byId("ytp-zoom-styles")) { const s = document.createElement("style"); s.id = "ytp-zoom-styles"; s.textContent = "\n /* Compact control bar matching YouTube control style */\n #ytp-zoom-control{position: absolute; left: 12px; bottom: 64px; z-index: 2200; display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 24px; background: rgba(0,0,0,0.35); color: #fff; font-size: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.5); backdrop-filter: blur(6px);}\n #ytp-zoom-control input[type=range]{width: 120px; -webkit-appearance: none; background: transparent; height: 24px;}\n /* WebKit track */\n #ytp-zoom-control input[type=range]::-webkit-slider-runnable-track{height: 4px; background: rgba(255,255,255,0.12); border-radius: 3px;}\n #ytp-zoom-control input[type=range]::-webkit-slider-thumb{-webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 6px rgba(255,255,255,0.06); margin-top: -4px;}\n /* Firefox */\n #ytp-zoom-control input[type=range]::-moz-range-track{height: 4px; background: rgba(255,255,255,0.12); border-radius: 3px;}\n #ytp-zoom-control input[type=range]::-moz-range-thumb{width: 12px; height: 12px; border-radius: 50%; background: #fff; border: none;}\n #ytp-zoom-control .zoom-label{min-width:36px;text-align:center;font-size:11px;padding:0 6px;user-select:none}\n #ytp-zoom-control::after{content:'Shift + Wheel to zoom';position:absolute;bottom:100%;right:0;padding:4px 8px;background:rgba(0,0,0,0.8);color:#fff;font-size:10px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;transform:translateY(4px);transition:opacity .2s,transform .2s}\n #ytp-zoom-control:hover::after{opacity:1;transform:translateY(-4px)}\n #ytp-zoom-control .zoom-reset{background: rgba(255,255,255,0.06); border: none; color: inherit; padding: 4px; display: flex; align-items: center; justify-content: center; border-radius: 50%; cursor: pointer; width: 28px; height: 28px;}\n #ytp-zoom-control .zoom-reset:hover{background: rgba(255,255,255,0.12)}\n #ytp-zoom-control .zoom-reset svg{display:block;width:14px;height:14px}\n /* Hidden state to mirror YouTube controls autohide */\n #ytp-zoom-control.ytp-hidden{opacity:0;transform:translateY(6px);pointer-events:none}\n #ytp-zoom-control{transition:opacity .18s ease, transform .18s ease}\n "; (document.head || document.documentElement).appendChild(s); } const wrap = document.createElement("div"); wrap.id = "ytp-zoom-control"; const input = document.createElement("input"); input.type = "range"; input.min = String(MIN_ZOOM); input.max = String(MAX_ZOOM); input.step = String(ZOOM_STEP); const label = document.createElement("div"); label.className = "zoom-label"; label.setAttribute("role", "status"); label.setAttribute("aria-live", "polite"); label.setAttribute("aria-label", "Current zoom level"); const reset = document.createElement("button"); reset.className = "zoom-reset"; reset.type = "button"; reset.setAttribute("aria-label", "Reset zoom"); reset.title = "Reset zoom"; reset.innerHTML = _createHTML('\n <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">\n <path d="M12 4V1l-5 5 5 5V7a7 7 0 1 1-7 7" stroke="currentColor" stroke-width="2" fill="none"/>\n </svg>\n '); wrap.appendChild(input); wrap.appendChild(label); wrap.appendChild(reset); let video = findVideoElement(); const stored = readZoomPan().zoom; const initZoomVal = Number.isFinite(stored) && !Number.isNaN(stored) ? stored : DEFAULT_ZOOM; const setZoom = z => { const clamped = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number(z))); input.value = String(clamped); const percentage = Math.round(100 * clamped); label.textContent = `${percentage}%`; label.setAttribute("aria-label", `Current zoom level ${percentage} percent`); if (video) { clampPan(clamped); requestAnimationFrame(() => { try { applyZoomToVideo(video, clamped, panX, panY); try { video.style.cursor = clamped > 1 ? "grab" : ""; } catch {} } catch (err) { console.error("[YouTube+] Apply zoom error:", err); } }); } try { saveZoomPan(clamped, panX, panY); } catch (err) { console.error("[YouTube+] Save zoom error:", err); } }; input.addEventListener("input", e => setZoom(e.target.value)); reset.addEventListener("click", () => { try { panX = 0; panY = 0; setZoom(DEFAULT_ZOOM); try { saveZoomPan(DEFAULT_ZOOM, 0, 0); } catch (e) { console.warn("[YouTube+] Failed to persist zoom reset:", e); } reset.style.transform = "scale(0.9)"; setTimeout(() => { reset.style.transform = ""; }, 150); } catch (err) { console.error("[YouTube+] Reset zoom error:", err); } }); let wheelThrottleTimer = null; let panSaveTimer = null; const scheduleSavePan = () => { try { panSaveTimer && clearTimeout(panSaveTimer); panSaveTimer = setTimeout(() => { try { const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM; saveZoomPan(currentZoom, panX, panY); } catch (err) { console.error("[YouTube+] Save pan error:", err); } panSaveTimer = null; }, 220); } catch (err) { console.error("[YouTube+] Schedule save pan error:", err); } }; const wheelHandler = ev => { try { if (!featureEnabled) { return; } if (!ev.shiftKey) { return; } ev.preventDefault(); if (wheelThrottleTimer) { return; } wheelThrottleTimer = setTimeout(() => { wheelThrottleTimer = null; }, 50); const delta = ev.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP; const current = readZoomPan().zoom || DEFAULT_ZOOM; const newZoom = current + delta; newZoom >= MIN_ZOOM && newZoom <= MAX_ZOOM && setZoom(newZoom); } catch (err) { console.error("[YouTube+] Wheel zoom error:", err); } }; player.addEventListener("wheel", wheelHandler, { passive: !1 }); if (video) { try { video.addEventListener("wheel", wheelHandler, { passive: !1 }); } catch (err) { console.error("[YouTube+] Failed to attach wheel handler to video:", err); } } const keydownHandler = ev => { try { if (!featureEnabled) { return; } const active = document.activeElement; if (active && ("INPUT" === active.tagName || "TEXTAREA" === active.tagName || active.isContentEditable)) { return; } if ("+" === ev.key || "=" === ev.key) { ev.preventDefault(); const current = readZoomPan().zoom || DEFAULT_ZOOM; setZoom(Math.min(MAX_ZOOM, current + ZOOM_STEP)); } else if ("-" === ev.key) { ev.preventDefault(); const current = readZoomPan().zoom || DEFAULT_ZOOM; setZoom(Math.max(MIN_ZOOM, current - ZOOM_STEP)); } } catch (e) { console.error("[YouTube+] Keyboard zoom error:", e); } }; window.addEventListener("keydown", keydownHandler); let panX = 0; let panY = 0; let videoStyleObserver = null; let dragging = !1; let dragStartX = 0; let dragStartY = 0; let dragStartPanX = 0; let dragStartPanY = 0; const clampPan = (zoom = readZoomPan().zoom) => { try { if (!video) { return; } const container = video.parentElement || video; if (!container) { return; } const containerRect = container.getBoundingClientRect(); if (!containerRect || 0 === containerRect.width || 0 === containerRect.height) { return; } const baseW = video.videoWidth || video.offsetWidth || containerRect.width; const baseH = video.videoHeight || video.offsetHeight || containerRect.height; if (!(baseW && baseH && Number.isFinite(baseW) && Number.isFinite(baseH))) { return; } const scaledW = baseW * zoom; const scaledH = baseH * zoom; const maxX = Math.max(0, (scaledW - containerRect.width) / 2); const maxY = Math.max(0, (scaledH - containerRect.height) / 2); Number.isFinite(maxX) && Number.isFinite(panX) && (panX = Math.max(-maxX, Math.min(maxX, panX))); Number.isFinite(maxY) && Number.isFinite(panY) && (panY = Math.max(-maxY, Math.min(maxY, panY))); } catch (err) { console.error("[YouTube+] Clamp pan error:", err); } }; const pointers = new Map; let initialPinchDist = null; let pinchStartZoom = null; let prevTouchAction = null; const getDistance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y); const pointerDown = ev => { try { if (!featureEnabled) { return; } pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY }); try { ev.target.setPointerCapture(ev.pointerId); } catch {} try { const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM; if ("mouse" === ev.pointerType && 0 === ev.button && pointers.size <= 1 && video && currentZoom > 1) { dragging = !0; dragStartX = ev.clientX; dragStartY = ev.clientY; dragStartPanX = panX; dragStartPanY = panY; try { video.style.cursor = "grabbing"; } catch {} } } catch {} if (2 === pointers.size) { const pts = Array.from(pointers.values()); initialPinchDist = getDistance(pts[0], pts[1]); pinchStartZoom = readZoomPan().zoom; prevTouchAction = player.style.touchAction; try { player.style.touchAction = "none"; } catch {} } } catch {} }; const pointerMove = ev => { try { if (!featureEnabled) { return; } pointers.has(ev.pointerId) && pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY }); if (dragging && "mouse" === ev.pointerType && video) { const dx = ev.clientX - dragStartX; const dy = ev.clientY - dragStartY; panX = dragStartPanX + dx; panY = dragStartPanY + dy; clampPan(); applyZoomToVideo(video, parseFloat(input.value) || DEFAULT_ZOOM, panX, panY); scheduleSavePan(); ev.preventDefault(); return; } if (2 === pointers.size && initialPinchDist && null != pinchStartZoom) { const pts = Array.from(pointers.values()); const dist = getDistance(pts[0], pts[1]); if (dist <= 0) { return; } const ratio = dist / initialPinchDist; const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, pinchStartZoom * ratio)); setZoom(newZoom); ev.preventDefault(); } } catch {} }; const pointerUp = ev => { try { if (!featureEnabled) { return; } pointers.delete(ev.pointerId); try { ev.target.releasePointerCapture(ev.pointerId); } catch {} try { if (dragging && "mouse" === ev.pointerType) { dragging = !1; try { video && (video.style.cursor = parseFloat(input.value) > 1 ? "grab" : ""); } catch {} } } catch {} if (pointers.size < 2) { initialPinchDist = null; pinchStartZoom = null; if (null != prevTouchAction) { try { player.style.touchAction = prevTouchAction; } catch {} prevTouchAction = null; } } } catch {} }; player.addEventListener("pointerdown", pointerDown, { passive: !0 }); player.addEventListener("pointermove", pointerMove, { passive: !1 }); player.addEventListener("pointerup", pointerUp, { passive: !0 }); player.addEventListener("pointercancel", pointerUp, { passive: !0 }); let touchDragging = !1; let touchDragStartX = 0; let touchDragStartY = 0; let touchDragStartPanX = 0; let touchDragStartPanY = 0; let touchInitialDist = null; let touchPinchStartZoom = null; const getTouchDistance = (t1, t2) => Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY); const touchStart = ev => { try { if (!featureEnabled) { return; } if (!video) { return; } if (1 === ev.touches.length) { const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM; if (currentZoom > 1) { touchDragging = !0; touchDragStartX = ev.touches[0].clientX; touchDragStartY = ev.touches[0].clientY; touchDragStartPanX = panX; touchDragStartPanY = panY; ev.preventDefault(); } } else if (2 === ev.touches.length) { touchInitialDist = getTouchDistance(ev.touches[0], ev.touches[1]); touchPinchStartZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM; try { prevTouchAction = player.style.touchAction; player.style.touchAction = "none"; } catch {} ev.preventDefault(); } } catch (e) { console.error("[YouTube+] touchStart error:", e); } }; const touchMove = ev => { try { if (!featureEnabled) { return; } if (!video) { return; } if (1 === ev.touches.length && touchDragging) { const dx = ev.touches[0].clientX - touchDragStartX; const dy = ev.touches[0].clientY - touchDragStartY; panX = touchDragStartPanX + dx; panY = touchDragStartPanY + dy; clampPan(); applyZoomToVideo(video, parseFloat(input.value) || DEFAULT_ZOOM, panX, panY); scheduleSavePan(); ev.preventDefault(); return; } if (2 === ev.touches.length && touchInitialDist && null != touchPinchStartZoom) { const dist = getTouchDistance(ev.touches[0], ev.touches[1]); if (dist <= 0) { return; } const ratio = dist / touchInitialDist; const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, touchPinchStartZoom * ratio)); setZoom(newZoom); ev.preventDefault(); } } catch (e) { console.error("[YouTube+] touchMove error:", e); } }; const touchEnd = ev => { try { if (!featureEnabled) { return; } touchDragging && 0 === ev.touches.length && (touchDragging = !1); if (ev.touches.length < 2) { touchInitialDist = null; touchPinchStartZoom = null; if (null != prevTouchAction) { try { player.style.touchAction = prevTouchAction; } catch {} prevTouchAction = null; } } } catch (e) { console.error("[YouTube+] touchEnd error:", e); } }; try { player.addEventListener("touchstart", touchStart, { passive: !1 }); player.addEventListener("touchmove", touchMove, { passive: !1 }); player.addEventListener("touchend", touchEnd, { passive: !0 }); player.addEventListener("touchcancel", touchEnd, { passive: !0 }); } catch (e) { console.error("[YouTube+] Failed to attach touch handlers:", e); } const mouseDownHandler = ev => { try { if (!featureEnabled) { return; } if (0 !== ev.button || !video) { return; } const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM; if (currentZoom <= 1) { return; } dragging = !0; dragStartX = ev.clientX; dragStartY = ev.clientY; dragStartPanX = panX; dragStartPanY = panY; try { video.style.cursor = "grabbing"; } catch {} ev.preventDefault(); } catch {} }; const mouseMoveHandler = ev => { try { if (!featureEnabled) { return; } if (!dragging || !video) { return; } const dx = ev.clientX - dragStartX; const dy = ev.clientY - dragStartY; panX = dragStartPanX + dx; panY = dragStartPanY + dy; clampPan(); video._panRAF || (video._panRAF = requestAnimationFrame(() => { applyZoomToVideo(video, parseFloat(input.value) || DEFAULT_ZOOM, panX, panY); scheduleSavePan(); video._panRAF = null; })); ev.preventDefault(); } catch (err) { console.error("[YouTube+] Mouse move error:", err); } }; const mouseUpHandler = () => { try { if (!featureEnabled) { return; } if (dragging) { dragging = !1; try { video && (video.style.cursor = parseFloat(input.value) > 1 ? "grab" : ""); } catch {} } } catch {} }; if (video) { try { video.addEventListener("mousedown", mouseDownHandler); } catch {} try { window.addEventListener("mousemove", mouseMoveHandler); } catch {} try { window.addEventListener("mouseup", mouseUpHandler); } catch {} try { const attachStyleObserver = () => { try { if (videoStyleObserver) { try { videoStyleObserver.disconnect(); } catch {} videoStyleObserver = null; } if (!video) { return; } videoStyleObserver = new MutationObserver(muts => { try { if (_isApplyingTransform) { return; } for (const m of muts) { if ("attributes" === m.type && "style" === m.attributeName) { const current = video && video.style && video.style.transform || ""; const expectedZoom = readZoomPan().zoom || parseFloat(input.value) || DEFAULT_ZOOM; const expected = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${expectedZoom.toFixed(3)})`; expectedZoom !== DEFAULT_ZOOM && current !== expected && current !== _lastTransformApplied && requestAnimationFrame(() => { try { applyZoomToVideo(video, expectedZoom, panX, panY); try { logRestoreEvent({ action: "restore_transform", currentTransform: current, expectedTransform: expected, zoom: expectedZoom, panX, panY }); } catch {} } catch {} }); } } } catch {} }); videoStyleObserver.observe(video, { attributes: !0, attributeFilter: [ "style" ] }); try { window.YouTubeUtils?.cleanupManager?.registerObserver?.(videoStyleObserver, video); } catch {} } catch {} }; attachStyleObserver(); } catch {} } const playerObserver = new MutationObserver(() => { try { const newVideo = findVideoElement(); if (newVideo && newVideo !== video) { try { if (video) { video.removeEventListener("mousedown", mouseDownHandler); video.removeEventListener("wheel", wheelHandler); if (video._panRAF) { cancelAnimationFrame(video._panRAF); video._panRAF = null; } } } catch (err) { console.error("[YouTube+] Error detaching from old video:", err); } video = newVideo; try { if (videoStyleObserver) { try { videoStyleObserver.disconnect(); } catch {} videoStyleObserver = null; } if (video) { videoStyleObserver = new MutationObserver(muts => { try { if (_isApplyingTransform) { return; } for (const m of muts) { if ("attributes" === m.type && "style" === m.attributeName) { const current = video && video.style && video.style.transform || ""; const expectedZoom = readZoomPan().zoom || parseFloat(input.value) || DEFAULT_ZOOM; const expected = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${expectedZoom.toFixed(3)})`; expectedZoom !== DEFAULT_ZOOM && current !== expected && current !== _lastTransformApplied && requestAnimationFrame(() => { try { applyZoomToVideo(video, expectedZoom, panX, panY); try { logRestoreEvent({ action: "restore_transform", currentTransform: current, expectedTransform: expected, zoom: expectedZoom, panX, panY }); } catch {} } catch {} }); } } } catch {} }); videoStyleObserver.observe(video, { attributes: !0, attributeFilter: [ "style" ] }); try { window.YouTubeUtils?.cleanupManager?.registerObserver?.(videoStyleObserver, video); } catch {} } } catch (err) { console.error("[YouTube+] Error attaching style observer to new video:", err); } try { const current = readZoomPan().zoom || DEFAULT_ZOOM; clampPan(current); applyZoomToVideo(video, current, panX, panY); } catch (err) { console.error("[YouTube+] Error applying zoom to new video:", err); } try { video.addEventListener("mousedown", mouseDownHandler); } catch (err) { console.error("[YouTube+] Error attaching mousedown to new video:", err); } try { video.addEventListener("wheel", wheelHandler, { passive: !1 }); } catch (err) { console.error("[YouTube+] Error attaching wheel to new video:", err); } } } catch (err) { console.error("[YouTube+] Player observer error:", err); } }); try { playerObserver.observe(player, { childList: !0, subtree: !0 }); window.YouTubeUtils?.cleanupManager?.registerObserver && window.YouTubeUtils.cleanupManager.registerObserver(playerObserver); window.YouTubeUtils?.ObserverRegistry?.track && window.YouTubeUtils.ObserverRegistry.track(); } catch (err) { console.error("[YouTube+] Failed to observe player for video changes:", err); } const fullscreenHandler = () => { try { const current = readZoomPan().zoom || DEFAULT_ZOOM; setTimeout(() => { try { let attempts = 0; const tryApply = () => { try { const newVideo = findVideoElement(); let swapped = !1; if (newVideo && newVideo !== video) { try { video && video.removeEventListener("wheel", wheelHandler); } catch {} video = newVideo; swapped = !0; try { video.addEventListener("wheel", wheelHandler, { passive: !1 }); } catch {} } clampPan(current); video && applyZoomToVideo(video, current, panX, panY, !1, !0); if (!swapped && (!video || attempts < FULLSCREEN_APPLY_RETRIES)) { attempts += 1; setTimeout(tryApply, FULLSCREEN_APPLY_RETRY_DELAY); } } catch (e) { console.error("[YouTube+] Fullscreen apply attempt error:", e); } }; tryApply(); } catch (e) { console.error("[YouTube+] Fullscreen inner apply error:", e); } }, FULLSCREEN_APPLY_DELAY); } catch (err) { console.error("[YouTube+] Fullscreen handler error:", err); } }; [ "fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange" ].forEach(evt => { document.addEventListener(evt, fullscreenHandler); window.YouTubeUtils?.cleanupManager?.registerListener && window.YouTubeUtils.cleanupManager.registerListener(document, evt, fullscreenHandler); }); try { try { const s = readZoomPan(); Number.isFinite(s.panX) && (panX = s.panX); Number.isFinite(s.panY) && (panY = s.panY); clampPan(initZoomVal); } catch (err) { console.error("[YouTube+] Restore pan error:", err); } } catch (err) { console.error("[YouTube+] Initial zoom setup error:", err); } try { const initialTransform = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${initZoomVal.toFixed(3)})`; _lastTransformApplied = initialTransform; } catch {} setZoom(initZoomVal); const updateZoomPosition = () => { try { const chrome = player.querySelector(".ytp-chrome-bottom"); if (chrome && chrome.offsetHeight) { const offset = chrome.offsetHeight + 8; wrap.style.bottom = `${offset}px`; } else { wrap.style.bottom = ""; } } catch {} }; updateZoomPosition(); const ro = new ResizeObserver(() => { try { "undefined" != typeof window && "function" == typeof window.requestAnimationFrame ? requestAnimationFrame(() => { try { updateZoomPosition(); } catch (e) { try { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Enhanced", "updateZoomPosition failed", e); } catch {} } }) : updateZoomPosition(); } catch (e) { try { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Enhanced", "ResizeObserver callback error", e); } catch {} } }); try { window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.registerObserver(ro); } catch {} try { const chromeEl = player.querySelector(".ytp-chrome-bottom"); chromeEl && ro.observe(chromeEl); } catch (e) { try { YouTubeUtils && YouTubeUtils.logError && YouTubeUtils.logError("Enhanced", "Failed to observe chrome element", e); } catch {} } try { window.addEventListener("resize", updateZoomPosition, { passive: !0 }); window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.registerListener(window, "resize", updateZoomPosition); } catch {} [ "fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange" ].forEach(evt => { try { document.addEventListener(evt, updateZoomPosition); window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.registerListener(document, evt, updateZoomPosition); } catch {} }); player.appendChild(wrap); const chromeBottom = player.querySelector(".ytp-chrome-bottom"); const updateHidden = () => { try { (() => { try { if (player.classList.contains("ytp-autohide") || player.classList.contains("ytp-hide-controls")) { return !0; } if (chromeBottom) { const style = window.getComputedStyle(chromeBottom); if (style && ("0" === style.opacity || "hidden" === style.visibility || "none" === style.display)) { return !0; } } } catch {} return !1; })() ? wrap.classList.add("ytp-hidden") : wrap.classList.remove("ytp-hidden"); } catch {} }; const visObserver = new MutationObserver(() => updateHidden()); try { visObserver.observe(player, { attributes: !0, attributeFilter: [ "class", "style" ] }); chromeBottom && visObserver.observe(chromeBottom, { attributes: !0, attributeFilter: [ "class", "style" ] }); try { window.YouTubeUtils?.cleanupManager?.registerObserver?.(visObserver, player); } catch {} } catch {} let showTimer = null; const mouseMoveShow = () => { try { wrap.classList.remove("ytp-hidden"); showTimer && clearTimeout(showTimer); showTimer = setTimeout(updateHidden, 2200); } catch {} }; player.addEventListener("mousemove", mouseMoveShow, { passive: !0 }); updateHidden(); const cleanup = () => { try { if (wheelThrottleTimer) { clearTimeout(wheelThrottleTimer); wheelThrottleTimer = null; } if (panSaveTimer) { clearTimeout(panSaveTimer); panSaveTimer = null; } if (video && video._panRAF) { cancelAnimationFrame(video._panRAF); video._panRAF = null; } player.removeEventListener("wheel", wheelHandler); player.removeEventListener("pointerdown", pointerDown); player.removeEventListener("pointermove", pointerMove); player.removeEventListener("pointerup", pointerUp); player.removeEventListener("pointercancel", pointerUp); player.removeEventListener("mousemove", mouseMoveShow); window.removeEventListener("keydown", keydownHandler); if (video) { try { video.removeEventListener("mousedown", mouseDownHandler); } catch {} try { video.removeEventListener("wheel", wheelHandler); } catch {} try { window.removeEventListener("mousemove", mouseMoveHandler); } catch {} try { window.removeEventListener("mouseup", mouseUpHandler); } catch {} try { video.style.cursor = ""; video.style.transform = ""; video.style.willChange = "auto"; video.style.transition = ""; } catch {} } if (videoStyleObserver) { try { videoStyleObserver.disconnect(); } catch {} videoStyleObserver = null; } if (visObserver) { try { visObserver.disconnect(); } catch {} } try { playerObserver && playerObserver.disconnect(); } catch {} try { document.removeEventListener("fullscreenchange", fullscreenHandler); } catch {} if (showTimer) { clearTimeout(showTimer); showTimer = null; } wrap.remove(); } catch (err) { console.error("[YouTube+] Cleanup error:", err); } }; window.YouTubeUtils && YouTubeUtils.cleanupManager && YouTubeUtils.cleanupManager.register(cleanup); return wrap; } let _navigateListenerAdded = !1; function initZoom() { try { if (!featureEnabled) { return; } const ensure = () => { const player = $("#movie_player"); if (!player) { return setTimeout(ensure, 400); } createZoomUI(); }; ensure(); if (!_navigateListenerAdded) { _navigateListenerAdded = !0; window.addEventListener("yt-navigate-finish", () => setTimeout(() => createZoomUI(), 300)); } } catch { console.error("initZoom error"); } } window.addEventListener("youtube-plus-settings-updated", e => { try { const nextEnabled = !1 !== e?.detail?.enableZoom; if (nextEnabled === featureEnabled) { return; } setFeatureEnabled(nextEnabled); } catch { setFeatureEnabled(loadFeatureEnabled()); } }); try { initZoom(); } catch {} }; window.YouTubePlusLazyLoader ? window.YouTubePlusLazyLoader.register("zoom", initZoomModule, { priority: 1 }) : initZoomModule(); })(); !(function() { "use strict"; const _createHTML = window._ytplusCreateHTML || (s => s); if ("undefined" == typeof window) { return; } const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxkcGNjb2N4bHJkc3llamZocnZjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIyMTAyNDYsImV4cCI6MjA4Nzc4NjI0Nn0.QfwrAG4SMJBPLoP-Mcq3hETQXt0ezinoi0CpN57Zn90"; let votingInitialized = !1; let voteRequestInFlight = !1; function setVoteControlsBusy(container, busy) { container && container.querySelectorAll(".ytp-plus-vote-btn, .ytp-plus-vote-bar-btn").forEach(el => { if (busy) { el.setAttribute("aria-disabled", "true"); el.style.pointerEvents = "none"; el.style.opacity = "0.7"; } else { el.removeAttribute("aria-disabled"); el.style.pointerEvents = ""; el.style.opacity = ""; } }); } const t = window.YouTubeUtils?.t || (key => key || ""); const tf = (key, fallback, params = {}) => { try { const value = t(key, params); if ("string" == typeof value && value && value !== key) { return value; } } catch {} return fallback || key || ""; }; function getLocalUserId() { let userId = localStorage.getItem("ytp_voting_user_id"); if (!userId) { const arr = new Uint8Array(16); void 0 !== globalThis.crypto && globalThis.crypto.getRandomValues ? globalThis.crypto.getRandomValues(arr) : arr.forEach((_, i, a) => { a[i] = 256 * Math.random() | 0; }); const hex = Array.from(arr, b => b.toString(16).padStart(2, "0")).join(""); userId = "user_" + hex + "_" + Date.now().toString(36); localStorage.setItem("ytp_voting_user_id", userId); } return userId; } function normalizeVoteType(value) { const numeric = Number(value); return 1 === numeric ? 1 : -1 === numeric ? -1 : 0; } async function supabaseFetch(endpoint, options = {}) { const url = `https://ldpccocxlrdsyejfhrvc.supabase.co/rest/v1/${endpoint}`; const headers = { apikey: SUPABASE_KEY, Authorization: `Bearer ${SUPABASE_KEY}`, "Content-Type": "application/json", Prefer: options.prefer || "return=representation" }; try { const response = await fetch(url, { ...options, headers: { ...headers, ...options.headers } }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.message || `HTTP ${response.status}`); } const data = await response.json().catch(() => null); return { data, error: null }; } catch (error) { return { data: null, error: error.message }; } } async function getFeatures() { const {data, error} = await supabaseFetch("ytplus_feature_requests?select=*&order=created_at.desc"); if (error) { console.error("[Voting] Error fetching features:", error); return []; } return data || []; } async function getAllVotes() { const {data, error} = await supabaseFetch("ytplus_feature_votes?select=feature_id,vote_type,ip_address"); if (error) { console.error("[Voting] Error fetching votes:", error); return {}; } const votes = {}; (data || []).forEach(v => { votes[v.feature_id] || (votes[v.feature_id] = { upvotes: 0, downvotes: 0 }); const voteType = normalizeVoteType(v.vote_type); 1 === voteType ? votes[v.feature_id].upvotes++ : -1 === voteType && votes[v.feature_id].downvotes++; }); return votes; } async function getUserVotes() { const userId = getLocalUserId(); const {data, error} = await supabaseFetch(`ytplus_feature_votes?select=feature_id,vote_type&ip_address=eq.${userId}`); if (error) { console.error("[Voting] Error fetching user votes:", error); return {}; } const userVotes = {}; (data || []).forEach(v => { const voteType = normalizeVoteType(v.vote_type); voteType && (userVotes[v.feature_id] = voteType); }); return userVotes; } async function vote(featureId, voteType) { const userId = getLocalUserId(); const {data: existing} = await supabaseFetch(`ytplus_feature_votes?feature_id=eq.${featureId}&ip_address=eq.${userId}&select=id`); if (existing && existing.length > 0) { const existingVote = existing[0]; if (0 === voteType) { await supabaseFetch(`ytplus_feature_votes?id=eq.${existingVote.id}`, { method: "DELETE" }); return { success: !0, action: "removed" }; } await supabaseFetch(`ytplus_feature_votes?id=eq.${existingVote.id}`, { method: "PATCH", body: JSON.stringify({ vote_type: voteType }) }); return { success: !0, action: "updated" }; } if (0 === voteType) { return { success: !0, action: "none" }; } const {error} = await supabaseFetch("ytplus_feature_votes", { method: "POST", body: JSON.stringify({ feature_id: featureId, vote_type: voteType, ip_address: userId }) }); if (error) { console.error("[Voting] Vote error:", error); return { success: !1, error }; } return { success: !0, action: "added" }; } async function submitFeature(title, description) { const stripHTML = s => String(s || "").replace(/<[^>]*>/g, "").trim(); title = stripHTML(title).slice(0, 200); description = stripHTML(description).slice(0, 2e3); if (!title) { return { success: !1, error: "Title is required" }; } const userId = getLocalUserId(); const {error} = await supabaseFetch("ytplus_feature_requests", { method: "POST", body: JSON.stringify({ title, description, author_ip: userId }) }); if (error) { console.error("[Voting] Submit error:", error); return { success: !1, error }; } return { success: !0 }; } function isPreviewFeature(feature) { return "__ytp_preview_vote__" === String(feature?.title || "").trim(); } async function ensurePreviewFeature(features) { const fromList = Array.isArray(features) ? features.find(isPreviewFeature) : null; if (fromList) { return fromList; } const userId = getLocalUserId(); const {data, error} = await supabaseFetch("ytplus_feature_requests", { method: "POST", body: JSON.stringify({ title: "__ytp_preview_vote__", description: "Internal row for ytp-plus-voting-preview", status: "proposed", author_ip: userId }) }); if (error) { console.error("[Voting] Error creating preview row:", error); const encodedTitle = encodeURIComponent("__ytp_preview_vote__"); const {data: existingPreview} = await supabaseFetch(`ytplus_feature_requests?select=id,title,description,status&title=eq.${encodedTitle}&limit=1`); return Array.isArray(existingPreview) && existingPreview[0] ? existingPreview[0] : null; } if (Array.isArray(data) && data[0]) { return data[0]; } const refreshed = await getFeatures(); return refreshed.find(isPreviewFeature) || null; } async function loadFeatures() { const listEl = document.getElementById("ytp-plus-voting-list"); if (!listEl) { return; } const allFeaturesRaw = await getFeatures(); const previewFeature = await ensurePreviewFeature(allFeaturesRaw); const features = (allFeaturesRaw || []).filter(f => !isPreviewFeature(f)); const [allVotes, userVotes] = await Promise.all([ getAllVotes(), getUserVotes() ]); const renderFeatures = [ ...features ]; if (0 !== renderFeatures.length) { listEl.innerHTML = _createHTML(renderFeatures.map(f => { const votes = allVotes[f.id] || { upvotes: 0, downvotes: 0 }; const userVote = userVotes[f.id] || 0; const totalVotes = votes.upvotes + votes.downvotes; const upPercent = totalVotes > 0 ? Math.round(votes.upvotes / totalVotes * 100) : 50; const statusMeta = (function getStatusMeta(status) { const normalized = String(status || "").toLowerCase(); return "completed" === normalized ? { className: "completed", label: tf("statusCompleted", "Completed") } : "in_progress" === normalized ? { className: "in-progress", label: tf("statusInProgress", "In progress") } : { className: "proposed", label: tf("statusProposed", "Proposed") }; })(f.status); return `\n <div class="ytp-plus-voting-item" data-feature-id="${f.id}">\n <div class="ytp-plus-voting-item-content">\n <div class="ytp-plus-voting-item-title">${escapeHtml(f.title)}</div>\n <div class="ytp-plus-voting-item-desc">${escapeHtml(f.description || "")}</div>\n <div class="ytp-plus-voting-item-status ${statusMeta.className}">${escapeHtml(statusMeta.label)}</div>\n </div>\n <div class="ytp-plus-voting-item-votes">\n <div class="ytp-plus-voting-score">\n <span class="ytp-plus-vote-total">${totalVotes} ${tf("votes", "votes")}</span>\n </div>\n <div class="ytp-plus-voting-buttons">\n <div class="ytp-plus-voting-buttons-track" style="background:linear-gradient(to right, #4caf50 ${upPercent}%, #f44336 ${upPercent}%);"></div>\n <button class="ytp-plus-vote-btn ${1 === userVote ? "active" : ""}" data-vote="1" title="${tf("like", "Like")}" type="button" aria-label="${tf("like", "Like")}">\n <svg class="ytp-plus-vote-icon" viewBox="0 0 24 24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>\n </button>\n <button class="ytp-plus-vote-btn ${-1 === userVote ? "active" : ""}" data-vote="-1" title="${tf("dislike", "Dislike")}" type="button" aria-label="${tf("dislike", "Dislike")}">\n <svg class="ytp-plus-vote-icon" viewBox="0 0 24 24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>\n </button>\n </div>\n </div>\n </div>\n `; }).join("")); listEl.querySelectorAll(".ytp-plus-vote-btn").forEach(btn => { btn.addEventListener("click", async () => { if (voteRequestInFlight) { return; } const featureId = btn.closest(".ytp-plus-voting-item").dataset.featureId; const voteType = parseInt(btn.dataset.vote, 10); const currentUserVote = userVotes[featureId] || 0; let newVoteType = voteType; currentUserVote === voteType && (newVoteType = 0); try { voteRequestInFlight = !0; setVoteControlsBusy(listEl.closest(".ytp-plus-settings-section, .ytp-plus-voting") || listEl, !0); const result = await vote(featureId, newVoteType); result.success && await loadFeatures(); } finally { voteRequestInFlight = !1; setVoteControlsBusy(listEl.closest(".ytp-plus-settings-section, .ytp-plus-voting") || listEl, !1); } }); }); updateVoteBar(allVotes, userVotes, previewFeature?.id || null); } else { listEl.innerHTML = _createHTML(`<div class="ytp-plus-voting-empty">${tf("noFeatures", "No feature requests yet")}</div>`); updateVoteBar(allVotes, userVotes, previewFeature?.id || null); } } function escapeHtml(str) { if (!str) { return ""; } if (window.YouTubeSecurityUtils?.escapeHtml) { return window.YouTubeSecurityUtils.escapeHtml(str); } const div = document.createElement("div"); div.textContent = str; return div.innerHTML; } function updateVoteBar(allVotes, userVotes, previewFeatureId) { const fillEl = document.getElementById("ytp-plus-vote-bar-fill"); const countEl = document.getElementById("ytp-plus-vote-bar-count"); const upBtn = document.getElementById("ytp-plus-vote-bar-up"); const downBtn = document.getElementById("ytp-plus-vote-bar-down"); if (!fillEl || !countEl) { return; } const previewVotes = previewFeatureId && allVotes[previewFeatureId] || { upvotes: 0, downvotes: 0 }; const totalUp = previewVotes.upvotes || 0; const totalDown = previewVotes.downvotes || 0; const total = totalUp + totalDown; const pct = total > 0 ? Math.round(totalUp / total * 100) : 50; fillEl.style.background = `linear-gradient(to right, #4caf50 ${pct}%, #f44336 ${pct}%)`; countEl.textContent = total > 0 ? `${total}` : "0"; const previewUserVote = previewFeatureId && userVotes[previewFeatureId] || 0; upBtn && upBtn.classList.toggle("active", 1 === previewUserVote); downBtn && downBtn.classList.toggle("active", -1 === previewUserVote); } function initVoting() { if (votingInitialized) { return; } votingInitialized = !0; const voteBarHandler = async e => { const barBtn = e.target.closest(".ytp-plus-vote-bar-btn"); if (barBtn) { if (voteRequestInFlight) { return; } const features = await getFeatures(); const previewFeature = await ensurePreviewFeature(features); if (!previewFeature?.id) { return; } const userVotes = await getUserVotes(); const voteType = parseInt(barBtn.dataset.vote, 10); const currentUserVote = userVotes[previewFeature.id] || 0; const newVoteType = currentUserVote === voteType ? 0 : voteType; const controlsRoot = barBtn.closest(".ytp-plus-settings-section, .ytp-plus-voting") || document.body; try { voteRequestInFlight = !0; setVoteControlsBusy(controlsRoot, !0); await vote(previewFeature.id, newVoteType); await loadFeatures(); } finally { voteRequestInFlight = !1; setVoteControlsBusy(controlsRoot, !1); } } }; const addFeatureHandler = e => { const showAddBtn = e.target.closest("#ytp-plus-show-add-feature"); const cancelBtn = e.target.closest("#ytp-plus-cancel-feature"); const submitBtn = e.target.closest("#ytp-plus-submit-feature"); if (showAddBtn) { const addFormEl = document.getElementById("ytp-plus-voting-add-form"); const showAddEl = document.getElementById("ytp-plus-show-add-feature"); addFormEl && (addFormEl.style.display = "block"); showAddEl && (showAddEl.style.display = "none"); } if (cancelBtn) { const addFormEl = document.getElementById("ytp-plus-voting-add-form"); const showAddEl = document.getElementById("ytp-plus-show-add-feature"); const titleEl = document.getElementById("ytp-plus-feature-title"); const descEl = document.getElementById("ytp-plus-feature-desc"); addFormEl && (addFormEl.style.display = "none"); showAddEl && (showAddEl.style.display = "block"); titleEl && (titleEl.value = ""); descEl && (descEl.value = ""); } if (submitBtn) { const titleInput = document.getElementById("ytp-plus-feature-title"); const descInput = document.getElementById("ytp-plus-feature-desc"); const title = titleInput?.value?.trim() || ""; const desc = descInput?.value?.trim() || ""; if (!title) { return; } submitBtn.disabled = !0; submitBtn.textContent = tf("loading", "Loading..."); submitFeature(title, desc).then(result => { submitBtn.disabled = !1; submitBtn.textContent = tf("submit", "Submit"); if (result.success) { const addFormEl = document.getElementById("ytp-plus-voting-add-form"); const showAddEl = document.getElementById("ytp-plus-show-add-feature"); addFormEl && (addFormEl.style.display = "none"); showAddEl && (showAddEl.style.display = "block"); titleInput && (titleInput.value = ""); descInput && (descInput.value = ""); loadFeatures(); } }); } }; if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { YouTubeUtils.cleanupManager.registerListener(document, "click", voteBarHandler); YouTubeUtils.cleanupManager.registerListener(document, "click", addFeatureHandler); } else { document.addEventListener("click", voteBarHandler); document.addEventListener("click", addFeatureHandler); } } const VotingSystem = { init: initVoting, createUI: function createVotingUI(container) { container.innerHTML = _createHTML(`\n <div class="ytp-plus-voting">\n <div class="ytp-plus-voting-header">\n <h3>${tf("featureRequests", "Feature Requests")}</h3>\n <button class="ytp-plus-voting-add-btn" id="ytp-plus-show-add-feature">\n + ${tf("addFeature", "Add Feature")}\n </button>\n </div>\n <div class="ytp-plus-voting-list" id="ytp-plus-voting-list">\n <div class="ytp-plus-voting-loading">${tf("loading", "Loading...")}</div>\n </div>\n <div class="ytp-plus-voting-add-form" id="ytp-plus-voting-add-form" style="display:none;">\n <input type="text" id="ytp-plus-feature-title" placeholder="${tf("featureTitle", "Feature title")}" />\n <textarea id="ytp-plus-feature-desc" placeholder="${tf("featureDescription", "Description")}"></textarea>\n <div class="ytp-plus-voting-form-actions">\n <button class="ytp-plus-voting-cancel" id="ytp-plus-cancel-feature">${tf("cancel", "Cancel")}</button>\n <button class="ytp-plus-voting-submit" id="ytp-plus-submit-feature">${tf("submit", "Submit")}</button>\n </div>\n </div>\n </div>\n `); }, loadFeatures, getFeatures, vote, submitFeature, initSlider: function initSlider() { const container = document.querySelector(".ytp-plus-ba-container"); if (!container || container.dataset.sliderInit) { return; } container.dataset.sliderInit = "1"; const afterEl = container.querySelector(".ytp-plus-ba-after"); const divider = container.querySelector(".ytp-plus-ba-divider"); if (!afterEl || !divider) { return; } let dragging = !1; let resumeTimer = null; let rafId = null; function setPosition(pct, manual = !1) { const clamped = Math.max(2, Math.min(98, pct)); afterEl.style.clipPath = `inset(0 0 0 ${clamped}%)`; manual && (divider.style.left = `${clamped}%`); divider.setAttribute("aria-valuenow", String(Math.round(clamped))); } function getPct(clientX) { const rect = container.getBoundingClientRect(); return (clientX - rect.left) / rect.width * 100; } function pauseAutoplay() { divider.classList.remove("autoplay"); if (rafId) { cancelAnimationFrame(rafId); rafId = null; } resumeTimer && clearTimeout(resumeTimer); resumeTimer = setTimeout(() => { divider.classList.add("autoplay"); startAutoplayRaf(); }, 3e3); } function startAutoplayRaf() { rafId || (rafId = requestAnimationFrame(function loop() { if (!divider.classList.contains("autoplay")) { rafId = null; return; } const rect = container.getBoundingClientRect(); const dRect = divider.getBoundingClientRect(); const pct = (dRect.left + dRect.width / 2 - rect.left) / rect.width * 100; setPosition(pct, !1); rafId = requestAnimationFrame(loop); })); } container.addEventListener("mousedown", e => { dragging = !0; pauseAutoplay(); setPosition(getPct(e.clientX), !0); e.preventDefault(); }); const onMousemove = e => { dragging && setPosition(getPct(e.clientX), !0); }; const onMouseup = () => { dragging = !1; }; if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { YouTubeUtils.cleanupManager.registerListener(window, "mousemove", onMousemove); YouTubeUtils.cleanupManager.registerListener(window, "mouseup", onMouseup); } else { window.addEventListener("mousemove", onMousemove); window.addEventListener("mouseup", onMouseup); } container.addEventListener("touchstart", e => { dragging = !0; pauseAutoplay(); setPosition(getPct(e.touches[0].clientX), !0); }, { passive: !0 }); const onTouchmove = e => { dragging && setPosition(getPct(e.touches[0].clientX), !0); }; const onTouchend = () => { dragging = !1; }; if (window.YouTubeUtils && YouTubeUtils.cleanupManager) { YouTubeUtils.cleanupManager.registerListener(window, "touchmove", onTouchmove, { passive: !0 }); YouTubeUtils.cleanupManager.registerListener(window, "touchend", onTouchend); } else { window.addEventListener("touchmove", onTouchmove, { passive: !0 }); window.addEventListener("touchend", onTouchend); } divider.addEventListener("keydown", e => { pauseAutoplay(); const cur = parseFloat(divider.getAttribute("aria-valuenow") || "50"); if ("ArrowLeft" === e.key) { setPosition(cur - 2, !0); e.preventDefault(); } if ("ArrowRight" === e.key) { setPosition(cur + 2, !0); e.preventDefault(); } }); setPosition(50, !0); setTimeout(() => { divider.classList.add("autoplay"); startAutoplayRaf(); }, 400); }, updateVoteBar }; void 0 === window.YouTubePlus && (window.YouTubePlus = {}); window.YouTubePlus.Voting = VotingSystem; window.YouTubePlusLazyLoader && window.YouTubePlusLazyLoader.register("voting", initVoting, { priority: 0 }); })();