// ==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/signals-core@1.12.1/dist/signals-core.min.js // @require https://cdn.jsdelivr.net/npm/browser-id3-writer@4.4.0/dist/browser-id3-writer.min.js // @require https://cdn.jsdelivr.net/npm/preact@10.27.2/dist/preact.min.js // @require https://cdn.jsdelivr.net/npm/preact@10.27.2/hooks/dist/hooks.umd.js // @require https://cdn.jsdelivr.net/npm/@preact/signals@2.5.0/dist/signals.min.js // @require https://cdn.jsdelivr.net/npm/dayjs@1.11.19/dayjs.min.js // @grant GM_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 // @downloadURL none // ==/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 \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(`