Greasy Fork is available in English.
Scarica video/audio di alta qualità, ripristina i dislike e altre funzioni VIP per YouTube e YouTube Music.
// ==UserScript== // @name YouTube Ultimate Tools // @name:vi Bộ Công Cụ YouTube Tối Ưu // @name:es Herramientas Definitivas de YouTube // @name:fr Outils Ultimes YouTube // @name:de Ultimative YouTube-Tools // @name:pt Ferramentas Definitivas do YouTube // @name:ru YouTube Ultimate Инструменты // @name:ja YouTube アルティメット ツール // @name:zh-CN YouTube 终极工具 // @name:zh-TW YouTube 終極工具 // @name:ko YouTube 얼티밋 도구 // @name:it Strumenti Definitivi per YouTube // @description Download high-quality video/audio, return dislikes, and more VIP features for YouTube and YouTube Music. // @description:vi Tải video/audio chất lượng cao, hiện nút dislike, và nhiều tính năng VIP khác cho YouTube và YouTube Music. // @description:es Descarga videos/audio de alta calidad, recupera dislikes y más funciones VIP para YouTube và YouTube Music. // @description:fr Téléchargez des vidéos/audio de haute qualità, récupérez les dislikes et plus de fonctionnalités VIP pour YouTube và YouTube Music. // @description:de Laden Sie hochwertige Videos/Audio herunter, stellen Sie Dislikes wieder her und weitere VIP-Funktionen für YouTube và YouTube Music. // @description:pt Baixe vídeos/áudio de alta qualidade, recupere dislikes e mais recursos VIP para YouTube và YouTube Music. // @description:ru Скачивайте видео/аудио высокого качества, возвращайте дизлайки и другие VIP-функции для YouTube и YouTube Music. // @description:ja 高品質なビデオ/オーディオのダウンロード、低評価の表示、YouTubeおよびYouTube Music向けのVIP機能。 // @description:zh-CN 下载高质量视频/音频,还原消失的回复,以及更多 YouTube 和 YouTube Music 的 VIP 功能。 // @description:zh-TW 下載高品質影片/音訊,還原消失的回复,以及更多 YouTube 和 YouTube Music 的 VIP 功能。 // @description:ko 고품질 비디오/오디오 다운로드, 싫어요 표시, YouTube 및 YouTube Music을 위한 더 많은 VIP 기능. // @description:it Scarica video/audio di alta qualità, ripristina i dislike e altre funzioni VIP per YouTube e YouTube Music. // @homepage https://greasyfork.org/users/1597067-nguyen-ngocanh // @version 0.0.6.0 // @author Akari, DeveloperMDCM // @contributor nvbangg // @match *://www.youtube.com/* // @match *://music.youtube.com/* // @match *://*.music.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant GM_info // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @run-at document-end // @grant GM_registerMenuCommand // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/js/iziToast.min.js // @compatible chrome // @compatible firefox // @compatible opera // @compatible safari // @compatible edge // @license GPL-3.0 // @namespace https://greasyfork.org/users/1597067-nguyen-ngocanh // @keywords youtube, download, mp3, mp4, high quality, return dislikes, tools // ==/UserScript== (function () { 'use strict'; let validoUrl = document.location.href; const isYTMusic = location.hostname === 'music.youtube.com'; const SETTINGS_KEY = isYTMusic ? 'ytmSettingsMDCM' : 'ytSettingsMDCM'; const $e = (el) => document.querySelector(el); // any element const $id = (el) => document.getElementById(el); // element by id const $m = (el) => document.querySelectorAll(el); // multiple all elements const $cl = (el) => document.createElement(el); // create element const $sp = (el, pty) => document.documentElement.style.setProperty(el, pty); // set property variable css const $ap = (el) => document.body.appendChild(el); // append element const apiDislikes = "https://returnyoutubedislikeapi.com/Votes?videoId="; // Api dislikes const apiGoogleTranslate = "https://translate.googleapis.com/translate_a/t"; // Api google translate let selectedBgColor = "#252525"; // Background color menu default let selectedTextColor = "#ffffff"; // Text color menu default let selectedBgAccentColor = "#ff0000"; // Accent color menu default const urlSharedCode = "https://greasyfork.org/scripts/576162-youtube-ultimate-tools"; const API_URL_AUDIO_VIDEO = "https://p.savenow.to/ajax/download.php?copyright=0&allow_extended_duration=1&" // API URL AUDIO VIDEO const API_KEY_DEVELOPERMDCM = 'dfcb6d76f2f6a9894gjkege8a4ab232222'; // API KEY FOR DOWNLOAD AUDIO VIDEO // Download API fallbacks (region/session issues) const DOWNLOAD_API_FALLBACK_BASES = [ "https://p.savenow.to", "https://p.lbserver.xyz", ]; // Alternative provider fallback const DUBS_START_ENDPOINT = "https://dubs.io/wp-json/tools/v1/download-video"; const DUBS_STATUS_ENDPOINT = "https://dubs.io/wp-json/tools/v1/status-video"; // for translate comments video const languagesTranslate = { "vi": "Vietnamese", "en": "English", "es": "Spanish", "fr": "French", "ja": "Japanese", "zh-CN": "Chinese (Simplified)", "ko": "Korean", "ru": "Russian", "de": "German", "pt": "Portuguese", "zh-TW": "Chinese (Traditional)", "it": "Italian" }; // var for wave let currentVideo = null; let waveStyle = 'dinamica'; let audioCtx = null; let analyser = null; let source = null; let animationId = null; let canvas = null; let ctx = null; let controlPanel = null; let bufferLength = 0; let dataArray = null; let smoothedData = []; let isSetup = false; const smoothingFactor = 0.05; const canvasHeight = 240; const scale = canvasHeight / 90; const PROCESSED_FLAG = 'wave_visualizer_processed'; // ------------------------------ // PERF: runtime guards + dynamic style (avoid style/event/interval leaks) let validoBotones = true; // ------------------------------ const __ytToolsRuntime = { dynamicStyleEl: null, dynamicCssLast: '', settingsLoaded: false, bookmarkClickHandlerInitialized: false, bookmarksPanelOpen: false, continueWatching: { enabled: false, map: null, flushT: null, boundVideo: null, boundVideoId: null, lastSaveAt: 0, lastSavedTime: -1, lastKnownVideoId: null, navHandlerInitialized: false, panelOpen: false, clickHandlerInitialized: false, pagehideHandlerInitialized: false, handlers: null, }, shortsChannelName: { enabled: false, observer: null, io: null, scanT: null, cache: new Map(), // videoId -> channelName inflight: new Map(), // videoId -> Promise<string> fetchChain: Promise.resolve(), }, dislikesCache: { videoId: null, dislikes: null, ts: 0, }, downloadClickHandlerInitialized: false, shortsObserver: null, statsObserver: null, statsIntervalId: null, lockupCachedStatsObserver: null, lockupCachedStatsObserveTarget: null, lockupCachedStatsIntervalId: null, updateShortsViewsButton: function () { }, updateShortsRatingButton: function () { }, nonstopPlayback: { enabled: false, hiddenDescriptor: null, visibilityStateDescriptor: null, blockVisibilityEvent: null, keepAliveTimer: null, }, audioOnly: { enabled: false, lastArtUrl: '', refreshTimer: null, }, }; function setDynamicCss(cssText = '') { if (!__ytToolsRuntime.dynamicStyleEl) { const style = document.createElement('style'); style.id = 'yt-tools-mdcm-dynamic-style'; document.head.appendChild(style); __ytToolsRuntime.dynamicStyleEl = style; } if (__ytToolsRuntime.dynamicCssLast === cssText) return; __ytToolsRuntime.dynamicCssLast = cssText; __ytToolsRuntime.dynamicStyleEl.textContent = cssText; } function paramsVideoURL() { const parametrosURL = new URLSearchParams(window.location.search); return parametrosURL.get('v'); } function isWatchPage() { return window.location.href.includes('youtube.com/watch'); } function checkDarkModeActive() { const htmlElement = document.documentElement; const isDarkModeYT = htmlElement.hasAttribute('dark') || htmlElement.getAttribute('style')?.includes('color-scheme: dark'); const isDarkModeYTM = document.querySelector('ytmusic-app')?.hasAttribute('dark'); return !!(isDarkModeYT || isDarkModeYTM); } function FormatterNumber(num, digits) { const lookup = [ { value: 1, symbol: "" }, { value: 1e3, symbol: "k" }, { value: 1e6, symbol: "M" }, { value: 1e9, symbol: "G" }, { value: 1e12, symbol: "T" }, { value: 1e15, symbol: "P" }, { value: 1e18, symbol: "E" } ]; const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; const item = lookup.slice().reverse().find(function (item) { return num >= item.value; }); return item ? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol : "0"; } const scheduleApplySettings = (() => { let t = null; return () => { // Prevent overwriting saved config with defaults before loadSettings finishes. if (!__ytToolsRuntime.settingsLoaded) return; clearTimeout(t); t = setTimeout(() => { try { applySettings(); } catch (err) { console.error('applySettings error:', err); } }, 120); }; })(); // Create a Trusted Types policy let policy = null; try { const tt = (typeof unsafeWindow !== 'undefined' ? unsafeWindow.trustedTypes : window.trustedTypes); if (tt) { try { policy = tt.createPolicy('yt-tools-mdcm', { createHTML: (s) => s }); } catch (e) { policy = tt.defaultPolicy || null; } } } catch (e) { policy = null; } function safeHTML(str) { if (policy && typeof policy.createHTML === 'function') return policy.createHTML(str); return str; } // ------------------------------ // Feature helpers: videoId / channelId / storage // ------------------------------ const STORAGE_KEYS_MDCM = { BOOKMARKS: 'ytBookmarksMDCM', CONTINUE_WATCHING: 'ytContinueWatchingMDCM', SHORTS_CHANNEL_CACHE: 'ytShortsChannelCacheMDCM', LIKES_DISLIKES_CACHE: 'ytLikesDislikesCacheMDCM', VERSION_CHECK_LAST: 'ytVersionCheckLastMDCM', }; const UPDATE_META_URL = 'https://update.greasyfork.org/scripts/576162/YouTube%20Ultimate%20Tools.meta.js'; const VERSION_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // once per day const SHORTS_CHANNEL_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days const LIKES_DISLIKES_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days const PERSISTED_CACHE_MAX_ENTRIES = 500; function getShortsChannelFromPersistedCache(videoId) { try { const map = readJsonGM(STORAGE_KEYS_MDCM.SHORTS_CHANNEL_CACHE, {}); const entry = map?.[videoId]; if (!entry || typeof entry.channelName !== 'string') return null; const age = Date.now() - (Number(entry.ts) || 0); if (age > SHORTS_CHANNEL_TTL_MS) return null; return entry.channelName; } catch (e) { return null; } } function setShortsChannelToPersistedCache(videoId, channelName) { if (!videoId || typeof channelName !== 'string') return; try { const map = readJsonGM(STORAGE_KEYS_MDCM.SHORTS_CHANNEL_CACHE, {}); map[videoId] = { channelName, ts: Date.now() }; const entries = Object.entries(map).sort((a, b) => (Number(b[1]?.ts) || 0) - (Number(a[1]?.ts) || 0)); const pruned = Object.fromEntries(entries.slice(0, PERSISTED_CACHE_MAX_ENTRIES)); writeJsonGM(STORAGE_KEYS_MDCM.SHORTS_CHANNEL_CACHE, pruned); } catch (e) { } } function getLikesDislikesFromPersistedCache(videoId) { try { const map = readJsonGM(STORAGE_KEYS_MDCM.LIKES_DISLIKES_CACHE, {}); const entry = map?.[videoId]; if (!entry) return null; const age = Date.now() - (Number(entry.ts) || 0); if (age > LIKES_DISLIKES_TTL_MS) return null; const dislikes = Number(entry.dislikes); const likes = Number(entry.likes); const viewCount = Number(entry.viewCount); const rating = Number(entry.rating); return { likes: Number.isFinite(likes) ? likes : null, dislikes: Number.isFinite(dislikes) ? dislikes : null, viewCount: Number.isFinite(viewCount) ? viewCount : null, rating: Number.isFinite(rating) && rating >= 0 && rating <= 5 ? rating : null, }; } catch (e) { return null; } } function setLikesDislikesToPersistedCache(videoId, likes, dislikes, viewCount, rating) { if (!videoId) return; try { const map = readJsonGM(STORAGE_KEYS_MDCM.LIKES_DISLIKES_CACHE, {}); map[videoId] = { likes: likes ?? null, dislikes: dislikes ?? null, viewCount: viewCount ?? null, rating: rating ?? null, ts: Date.now() }; const entries = Object.entries(map).sort((a, b) => (Number(b[1]?.ts) || 0) - (Number(a[1]?.ts) || 0)); const pruned = Object.fromEntries(entries.slice(0, Math.min(PERSISTED_CACHE_MAX_ENTRIES, 300))); writeJsonGM(STORAGE_KEYS_MDCM.LIKES_DISLIKES_CACHE, pruned); } catch (e) { } } function getCurrentVideoId() { try { if (location.pathname.startsWith('/shorts/')) { const parts = location.pathname.split('/').filter(Boolean); return parts[1] || null; } if (location.href.includes('youtube.com/watch')) { return paramsVideoURL(); } return null; } catch (e) { return null; } } function readJsonGM(key, fallback) { try { const raw = GM_getValue(key, ''); if (!raw) return fallback; return JSON.parse(raw); } catch (e) { return fallback; } } function writeJsonGM(key, value) { try { GM_setValue(key, JSON.stringify(value)); } catch (e) { console.error('writeJsonGM error:', e); } } const AUDIO_ONLY_TAB_OVERRIDE_KEY = 'ytToolsAudioOnlyTabOverrideMDCM'; // Feature source/inspiration: // Nonstop & Audio Only for YouTube & YouTube Music // Author: nvbangg / Nguyen Van Bang // Repo: https://github.com/nvbangg/Nonstop_Audio_Only_for_Youtube_YTMusic // Greasy Fork: https://greasyfork.org/scripts/546130 // Source license: GPL-3.0. This userscript implements an independent adaptation of the behavior. function applyNonstopPlayback(enabled) { const rt = __ytToolsRuntime.nonstopPlayback; if (enabled && rt.enabled) return; if (!enabled && !rt.enabled) return; if (enabled) { rt.enabled = true; rt.hiddenDescriptor = Object.getOwnPropertyDescriptor(document, 'hidden') || null; rt.visibilityStateDescriptor = Object.getOwnPropertyDescriptor(document, 'visibilityState') || null; try { Object.defineProperties(document, { hidden: { configurable: true, get: () => false }, visibilityState: { configurable: true, get: () => 'visible' }, }); } catch (e) { console.warn('[YT Tools] Could not override visibility state:', e); } rt.blockVisibilityEvent = (event) => { event.stopImmediatePropagation(); }; document.addEventListener('visibilitychange', rt.blockVisibilityEvent, true); window.addEventListener('visibilitychange', rt.blockVisibilityEvent, true); const refreshActivity = () => { try { const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; if ('_lact' in pageWindow) pageWindow._lact = Date.now(); } catch (e) { } }; refreshActivity(); rt.keepAliveTimer = setInterval(refreshActivity, 60000); return; } rt.enabled = false; if (rt.blockVisibilityEvent) { document.removeEventListener('visibilitychange', rt.blockVisibilityEvent, true); window.removeEventListener('visibilitychange', rt.blockVisibilityEvent, true); rt.blockVisibilityEvent = null; } if (rt.keepAliveTimer) { clearInterval(rt.keepAliveTimer); rt.keepAliveTimer = null; } try { if (rt.hiddenDescriptor) Object.defineProperty(document, 'hidden', rt.hiddenDescriptor); else delete document.hidden; if (rt.visibilityStateDescriptor) Object.defineProperty(document, 'visibilityState', rt.visibilityStateDescriptor); else delete document.visibilityState; } catch (e) { } rt.hiddenDescriptor = null; rt.visibilityStateDescriptor = null; } function getAudioOnlyTabOverride() { const value = sessionStorage.getItem(AUDIO_ONLY_TAB_OVERRIDE_KEY); if (value === 'true') return true; if (value === 'false') return false; return null; } function setAudioOnlyTabOverride(enabled, defaultEnabled) { if (!!enabled === !!defaultEnabled) { sessionStorage.removeItem(AUDIO_ONLY_TAB_OVERRIDE_KEY); return; } sessionStorage.setItem(AUDIO_ONLY_TAB_OVERRIDE_KEY, enabled ? 'true' : 'false'); } function getEffectiveAudioOnly(settings) { const override = getAudioOnlyTabOverride(); return override === null ? !!settings?.audioOnly : override; } function syncAudioOnlyTabCheckbox(settings) { const tabToggle = $id('audio-only-tab-toggle'); if (!tabToggle) return; tabToggle.checked = getEffectiveAudioOnly(settings); tabToggle.title = getAudioOnlyTabOverride() === null ? 'Following the global Audio-only mode setting' : 'Audio-only override is active for this browser tab'; } function getActiveAudioOnlyVideo() { const videos = Array.from(document.querySelectorAll('video')); if (isYTMusic) return videos[0] || null; return videos.find((video) => { const rect = video.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }) || videos[0] || null; } async function getAudioOnlyThumbnailUrl() { try { const moviePlayer = document.getElementById('movie_player'); if (moviePlayer && typeof moviePlayer.getVideoData === 'function') { const data = moviePlayer.getVideoData(); if (data?.video_id) return `https://i.ytimg.com/vi/${data.video_id}/hqdefault.jpg`; } } catch (e) { } const videoId = getCurrentVideoId() || paramsVideoURL(); if (!videoId) return ''; const host = isYTMusic ? 'i1.ytimg.com' : 'img.youtube.com'; return `https://${host}/vi/${videoId}/hqdefault.jpg`; } async function applyAudioOnlyMode(enabled) { const rt = __ytToolsRuntime.audioOnly; rt.enabled = !!enabled; document.body.classList.toggle('yt-tools-audio-only-active', rt.enabled); document.querySelectorAll('.yt-tools-audio-only-video').forEach((el) => el.classList.remove('yt-tools-audio-only-video')); document.querySelectorAll('.yt-tools-audio-only-player').forEach((el) => el.classList.remove('yt-tools-audio-only-player')); if (!rt.enabled) { rt.lastArtUrl = ''; setAudioOnlyBackground(''); if (rt.refreshTimer) { clearInterval(rt.refreshTimer); rt.refreshTimer = null; } return; } const video = getActiveAudioOnlyVideo(); const player = video?.parentNode?.parentNode || video?.parentElement || null; if (video) video.classList.add('yt-tools-audio-only-video'); if (player) player.classList.add('yt-tools-audio-only-player'); const artUrl = await getAudioOnlyThumbnailUrl(); if (artUrl && artUrl !== rt.lastArtUrl) { rt.lastArtUrl = artUrl; setAudioOnlyBackground(artUrl); } if (!rt.refreshTimer) { rt.refreshTimer = setInterval(() => { if (document.visibilityState !== 'visible') return; const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); applyAudioOnlyMode(getEffectiveAudioOnly(settings)); }, 3000); } } function setAudioOnlyBackground(url) { let style = $id('yt-tools-audio-only-style'); if (!style) { style = document.createElement('style'); style.id = 'yt-tools-audio-only-style'; document.documentElement.appendChild(style); } style.textContent = url ? `.yt-tools-audio-only-player{background-image:url("${url}")!important;}` : ''; } function isVersionNewer(latestStr, currentStr) { if (!latestStr || !currentStr) return false; const parse = (s) => String(s).trim().split('.').map((n) => parseInt(n, 10) || 0); const a = parse(latestStr); const b = parse(currentStr); const len = Math.max(a.length, b.length); for (let i = 0; i < len; i++) { const x = a[i] || 0; const y = b[i] || 0; if (x > y) return true; if (x < y) return false; } return false; } async function checkNewVersion() { try { const last = GM_getValue(STORAGE_KEYS_MDCM.VERSION_CHECK_LAST, 0); if (Date.now() - last < VERSION_CHECK_INTERVAL_MS) return; GM_setValue(STORAGE_KEYS_MDCM.VERSION_CHECK_LAST, Date.now()); const res = await fetch(UPDATE_META_URL, { cache: 'no-store' }); if (!res.ok) return; const text = await res.text(); const m = text.match(/@version\s+([\d.]+)/); if (!m) return; const latestVer = m[1].trim(); const currentVer = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? String(GM_info.script.version).trim() : ''; if (!currentVer || !isVersionNewer(latestVer, currentVer)) return; const updateUrl = 'https://update.greasyfork.org/scripts/576162/YouTube%20Ultimate%20Tools.user.js'; iziToast.show({ title: 'New Update', message: 'A new version YoutubeTools is available.', buttons: [ ['<button>View Now</button>', function (instance, toast) { window.open(updateUrl, '_blank'); instance.hide({ transitionOut: 'fadeOut' }, toast, 'button'); }, true] // true = focus ] }); } catch (e) { // silent: network or parse error } } function formatTimeShort(sec) { const s = Math.max(0, Math.floor(Number(sec) || 0)); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const r = s % 60; return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}` : `${m}:${String(r).padStart(2, '0')}`; } // ------------------------------ // Feature: History / Continue watching (per video) // ------------------------------ function getMainVideoEl() { return ( document.querySelector('#movie_player video.video-stream.html5-main-video') || document.querySelector('ytd-player video.video-stream.html5-main-video') || document.querySelector('video.video-stream.html5-main-video') || document.querySelector('video') ); } function getCurrentVideoMeta() { try { // DOM first (updates earlier on SPA navigation) const domTitle = $e('ytd-watch-metadata h1 yt-formatted-string')?.textContent?.trim() || $e('h1.ytd-watch-metadata yt-formatted-string')?.textContent?.trim() || ''; const domAuthor = $e('#owner ytd-channel-name a, ytd-video-owner-renderer ytd-channel-name a, #text-container.ytd-channel-name a')?.textContent?.trim() || $e('#owner a[href^="/@"], #owner a[href^="/channel/"]')?.textContent?.trim() || ''; const titleFromDom = (domTitle || document.title || '').replace(/\s*-\s*YouTube\s*$/i, '').trim(); const authorFromDom = (domAuthor || '').trim(); const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window; const pr = w?.ytInitialPlayerResponse || window.ytInitialPlayerResponse; const vd = pr?.videoDetails || null; const title = (titleFromDom || vd?.title || document.title || '').replace(/\s*-\s*YouTube\s*$/i, '').trim(); const author = (authorFromDom || vd?.author || '').trim(); const thumbs = vd?.thumbnail?.thumbnails; const thumb = Array.isArray(thumbs) ? (thumbs[thumbs.length - 1]?.url || '') : ''; return { title, author, thumb }; } catch (e) { return { title: '', author: '', thumb: '' }; } } function ensureContinueWatchingMapLoaded() { const rt = __ytToolsRuntime.continueWatching; if (!rt.map) rt.map = readJsonGM(STORAGE_KEYS_MDCM.CONTINUE_WATCHING, {}); if (typeof rt.map !== 'object' || !rt.map) rt.map = {}; return rt.map; } function pruneContinueWatchingMap(map, maxEntries = 200) { try { const entries = Object.entries(map || {}).filter(([, v]) => v && typeof v === 'object'); entries.sort((a, b) => (Number(b[1].updatedAt) || 0) - (Number(a[1].updatedAt) || 0)); const keep = entries.slice(0, maxEntries); const next = {}; for (const [k, v] of keep) next[k] = v; return next; } catch (e) { return map || {}; } } function scheduleContinueWatchingFlush() { const rt = __ytToolsRuntime.continueWatching; clearTimeout(rt.flushT); rt.flushT = setTimeout(() => { try { if (!rt.map) return; rt.map = pruneContinueWatchingMap(rt.map, 200); writeJsonGM(STORAGE_KEYS_MDCM.CONTINUE_WATCHING, rt.map); } catch (e) { } }, 800); } function clearContinueWatchingForVideo(videoId) { if (!videoId) return; const rt = __ytToolsRuntime.continueWatching; const map = ensureContinueWatchingMapLoaded(); if (map && Object.prototype.hasOwnProperty.call(map, videoId)) { delete map[videoId]; rt.map = map; scheduleContinueWatchingFlush(); } } function setContinueWatchingForVideo(videoId, seconds, durationSec) { if (!videoId) return; const rt = __ytToolsRuntime.continueWatching; const map = ensureContinueWatchingMapLoaded(); const t = Math.max(0, Math.floor(Number(seconds) || 0)); const d = Math.max(0, Math.floor(Number(durationSec) || 0)); const prev = map[videoId] && typeof map[videoId] === 'object' ? map[videoId] : {}; const meta = getCurrentVideoMeta(); map[videoId] = { t, d, updatedAt: Date.now(), title: meta.title || prev.title || '', author: meta.author || prev.author || '', thumb: meta.thumb || prev.thumb || '', }; rt.map = map; scheduleContinueWatchingFlush(); } function getContinueWatchingTime(videoId) { if (!videoId) return null; const map = ensureContinueWatchingMapLoaded(); const entry = map?.[videoId]; const t = Number(entry?.t); return Number.isFinite(t) ? t : null; } function updateContinueWatchingButton() { const rt = __ytToolsRuntime.continueWatching; const enabled = !!rt.enabled; if (!enabled || !isWatchPage()) return; const videoId = getCurrentVideoId(); if (!videoId) return; const v = getMainVideoEl(); const t = getContinueWatchingTime(videoId); const dur = Number(v?.duration); const hasDur = Number.isFinite(dur) && dur > 0; if (!t || t < 5) return; if (hasDur && t >= (dur - 5)) { clearContinueWatchingForVideo(videoId); return; } } function updateContinueWatchingHistoryUi() { const rt = __ytToolsRuntime.continueWatching; const btn = $id('yt-cw-history-toggle'); const panel = $id('yt-continue-watching-panel'); if (!btn || !panel) return; const enabled = !!rt.enabled; if (!enabled || !isWatchPage()) { btn.style.display = 'none'; panel.style.display = 'none'; return; } btn.style.display = 'inline-flex'; panel.style.display = rt.panelOpen ? 'block' : 'none'; } function cssEscapeLite(s) { const str = String(s || ''); if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(str); // minimal escape for attribute selector return str.replace(/["\\]/g, '\\$&'); } function updateContinueWatchingPanelRow(videoId) { try { const rt = __ytToolsRuntime.continueWatching; if (!rt.enabled || !rt.panelOpen || !isWatchPage()) return false; const panel = $id('yt-continue-watching-panel'); if (!panel) return false; const key = cssEscapeLite(videoId); const row = panel.querySelector(`.yt-cw-item[data-video-id="${key}"]`); if (!row) return false; const entry = ensureContinueWatchingMapLoaded()?.[videoId]; const t = Number(entry?.t); if (!Number.isFinite(t)) return false; const meta = row.querySelector('.yt-cw-meta'); if (!meta) return false; const author = String(entry?.author || '').trim(); meta.textContent = `${formatTimeShort(t)}${author ? ` • ${author}` : ''}`; return true; } catch (e) { return false; } } function navigateToWatchSpa(videoId, seconds) { const t = Number(seconds); const url = `/watch?v=${encodeURIComponent(videoId)}${Number.isFinite(t) ? `&t=${Math.max(0, Math.floor(t))}s` : ''}`; try { const a = document.createElement('a'); a.href = url; a.target = '_self'; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); a.remove(); return; } catch (e) { } location.href = url; } function renderContinueWatchingPanel() { const panel = $id('yt-continue-watching-panel'); if (!panel) return; const rt = __ytToolsRuntime.continueWatching; if (!rt.enabled || !rt.panelOpen || !isWatchPage()) { panel.style.display = 'none'; return; } const map = pruneContinueWatchingMap(ensureContinueWatchingMapLoaded(), 200); rt.map = map; const currentVid = getCurrentVideoId(); const entries = Object.entries(map) .map(([videoId, v]) => ({ videoId, ...v })) .filter((e) => e.videoId && Number.isFinite(Number(e.t)) && Number(e.t) >= 5) .sort((a, b) => (Number(b.updatedAt) || 0) - (Number(a.updatedAt) || 0)) .slice(0, 25); panel.replaceChildren(); const header = document.createElement('div'); header.className = 'yt-cw-header'; const hTitle = document.createElement('div'); hTitle.className = 'yt-cw-header-title'; hTitle.textContent = 'Continue watching'; const clearAll = document.createElement('button'); clearAll.type = 'button'; clearAll.className = 'yt-cw-clear'; clearAll.textContent = 'Clear'; clearAll.dataset.cwAction = 'clearAll'; header.appendChild(hTitle); header.appendChild(clearAll); panel.appendChild(header); if (!entries.length) { const empty = document.createElement('div'); empty.className = 'yt-cw-empty'; empty.textContent = 'No history yet. Watch a bit, then reopen any video.'; panel.appendChild(empty); return; } for (const e of entries) { const item = document.createElement('div'); item.className = 'yt-cw-item'; item.dataset.videoId = e.videoId; const thumbWrap = document.createElement('div'); thumbWrap.className = 'yt-cw-thumb-wrap'; const img = document.createElement('img'); img.className = 'yt-cw-thumb'; img.loading = 'lazy'; img.decoding = 'async'; img.alt = ''; const thumbSrc = (e.thumb || '').trim() || `https://i.ytimg.com/vi/${encodeURIComponent(e.videoId)}/hqdefault.jpg`; img.src = thumbSrc; thumbWrap.appendChild(img); const info = document.createElement('div'); info.className = 'yt-cw-info'; const title = document.createElement('div'); title.className = 'yt-cw-title'; const safeTitle = (e.title || '').trim(); title.textContent = safeTitle ? safeTitle : e.videoId; const meta = document.createElement('div'); meta.className = 'yt-cw-meta'; const author = (e.author || '').trim(); meta.textContent = `${formatTimeShort(e.t)}${author ? ` • ${author}` : ''}`; info.appendChild(title); info.appendChild(meta); const actions = document.createElement('div'); actions.className = 'yt-cw-actions'; const tSec = Math.max(0, Math.floor(Number(e.t) || 0)); let go = null; if (currentVid && currentVid === e.videoId) { const seek = document.createElement('button'); seek.type = 'button'; seek.className = 'yt-cw-go'; seek.textContent = 'Resume'; seek.dataset.cwAction = 'seek'; seek.dataset.t = String(tSec); go = seek; } else { const a = document.createElement('a'); a.className = 'yt-simple-endpoint yt-cw-go'; a.textContent = 'Resume'; a.href = `/watch?v=${encodeURIComponent(e.videoId)}&t=${tSec}s`; a.target = '_self'; a.rel = 'noopener'; go = a; } const del = document.createElement('button'); del.type = 'button'; del.className = 'yt-cw-del'; del.textContent = '✕'; del.title = 'Delete'; del.dataset.cwAction = 'del'; del.dataset.videoId = e.videoId; actions.appendChild(go); actions.appendChild(del); item.appendChild(thumbWrap); item.appendChild(info); item.appendChild(actions); panel.appendChild(item); } } function setupContinueWatchingFeature(enabled) { const rt = __ytToolsRuntime.continueWatching; rt.enabled = !!enabled; // Keep UI in sync across YouTube SPA navigations if (!rt.navHandlerInitialized) { rt.navHandlerInitialized = true; const onNav = () => { try { const vid = getCurrentVideoId(); if (rt.lastKnownVideoId !== vid) { rt.lastKnownVideoId = vid; // reset throttles so new video can save properly rt.lastSaveAt = 0; rt.lastSavedTime = -1; rt.boundVideoId = vid; updateContinueWatchingButton(); updateContinueWatchingHistoryUi(); if (rt.panelOpen) renderContinueWatchingPanel(); } else { updateContinueWatchingButton(); updateContinueWatchingHistoryUi(); if (rt.panelOpen) renderContinueWatchingPanel(); } } catch (e) { } }; window.addEventListener('yt-navigate-finish', onNav, true); window.addEventListener('popstate', onNav, true); window.addEventListener('hashchange', onNav, true); } // Ensure click handler only once if (!rt.clickHandlerInitialized) { rt.clickHandlerInitialized = true; document.addEventListener('click', (e) => { const target = e.target; if (!(target instanceof Element)) return; const historyBtn = target.closest('#yt-cw-history-toggle'); const cwActionBtn = target.closest('[data-cw-action]'); if (historyBtn) { e.preventDefault(); e.stopPropagation(); rt.panelOpen = !rt.panelOpen; updateContinueWatchingHistoryUi(); if (rt.panelOpen) renderContinueWatchingPanel(); // render only when opened return; } if (cwActionBtn) { const action = cwActionBtn.getAttribute('data-cw-action'); if (!action) return; e.preventDefault(); e.stopPropagation(); if (action === 'clearAll') { rt.map = {}; writeJsonGM(STORAGE_KEYS_MDCM.CONTINUE_WATCHING, {}); renderContinueWatchingPanel(); updateContinueWatchingButton(); try { Notify('success', 'History cleared'); } catch (e2) { } return; } if (action === 'del') { const vid = cwActionBtn.getAttribute('data-video-id') || ''; if (vid) clearContinueWatchingForVideo(vid); renderContinueWatchingPanel(); updateContinueWatchingButton(); return; } if (action === 'seek') { const t = Number(cwActionBtn.getAttribute('data-t')); const v = getMainVideoEl(); if (!v || !Number.isFinite(t)) return; v.currentTime = Math.max(0, t); v.play?.().catch(() => { }); try { Notify('success', `Resume: ${formatTimeShort(t)}`); } catch (e2) { } updateContinueWatchingButton(); return; } } }, true); } // Save on tab close / navigation (once) if (!rt.pagehideHandlerInitialized) { rt.pagehideHandlerInitialized = true; window.addEventListener('pagehide', () => { try { if (!rt.enabled) return; if (!isWatchPage()) return; const vid = getCurrentVideoId(); const v = getMainVideoEl(); if (!vid || !v) return; const t = Number(v.currentTime); const d = Number(v.duration); if (Number.isFinite(t) && t >= 5) setContinueWatchingForVideo(vid, t, d); // best-effort immediate flush if (rt.flushT) { clearTimeout(rt.flushT); rt.flushT = null; } if (rt.map) writeJsonGM(STORAGE_KEYS_MDCM.CONTINUE_WATCHING, pruneContinueWatchingMap(rt.map, 200)); } catch (e) { } }, { capture: true }); } const historyBtn = $id('yt-cw-history-toggle'); const panel = $id('yt-continue-watching-panel'); if (historyBtn && !rt.enabled) historyBtn.style.display = 'none'; if (panel && !rt.enabled) panel.style.display = 'none'; // Not on watch page: detach video listeners and hide if (!rt.enabled || !isWatchPage()) { try { if (rt.boundVideo && rt.handlers) { rt.boundVideo.removeEventListener('timeupdate', rt.handlers.timeupdate); rt.boundVideo.removeEventListener('pause', rt.handlers.pause); rt.boundVideo.removeEventListener('ended', rt.handlers.ended); rt.boundVideo.removeEventListener('loadedmetadata', rt.handlers.loadedmetadata); rt.boundVideo.removeEventListener('seeked', rt.handlers.seeked); } } catch (e) { } rt.boundVideo = null; rt.boundVideoId = null; rt.handlers = null; updateContinueWatchingButton(); updateContinueWatchingHistoryUi(); return; } const v = getMainVideoEl(); const videoId = getCurrentVideoId(); if (!v || !videoId) { updateContinueWatchingButton(); updateContinueWatchingHistoryUi(); return; } // VideoId can change while YouTube reuses the same <video> element if (rt.boundVideoId !== videoId) { rt.boundVideoId = videoId; rt.lastSaveAt = 0; rt.lastSavedTime = -1; } // If video element changed, rebind listeners (avoid leaks) if (rt.boundVideo && rt.boundVideo !== v && rt.handlers) { try { rt.boundVideo.removeEventListener('timeupdate', rt.handlers.timeupdate); rt.boundVideo.removeEventListener('pause', rt.handlers.pause); rt.boundVideo.removeEventListener('ended', rt.handlers.ended); rt.boundVideo.removeEventListener('loadedmetadata', rt.handlers.loadedmetadata); rt.boundVideo.removeEventListener('seeked', rt.handlers.seeked); } catch (e) { } rt.boundVideo = null; rt.boundVideoId = null; rt.handlers = null; } rt.boundVideo = v; rt.boundVideoId = videoId; if (!rt.handlers) { rt.handlers = { timeupdate: () => { try { if (!rt.enabled) return; const vid = getCurrentVideoId(); if (!vid) return; const now = Date.now(); if (now - rt.lastSaveAt < 5000) return; // throttle if (v.paused) return; const t = Number(v.currentTime); const d = Number(v.duration); if (!Number.isFinite(t)) return; if (Math.abs(t - rt.lastSavedTime) < 2) return; rt.lastSaveAt = now; rt.lastSavedTime = t; if (t < 5) return; setContinueWatchingForVideo(vid, t, d); updateContinueWatchingButton(); // Avoid re-rendering the whole panel every few seconds. // If panel is open and row exists, just update its text. if (rt.panelOpen) { if (!updateContinueWatchingPanelRow(vid)) renderContinueWatchingPanel(); } } catch (e) { } }, pause: () => { try { if (!rt.enabled) return; const vid = getCurrentVideoId(); if (!vid) return; const t = Number(v.currentTime); const d = Number(v.duration); if (!Number.isFinite(t)) return; if (t < 5) { clearContinueWatchingForVideo(vid); } else { setContinueWatchingForVideo(vid, t, d); } updateContinueWatchingButton(); if (rt.panelOpen) { if (!updateContinueWatchingPanelRow(vid)) renderContinueWatchingPanel(); } } catch (e) { } }, ended: () => { try { const vid = getCurrentVideoId(); if (vid) clearContinueWatchingForVideo(vid); updateContinueWatchingButton(); if (rt.panelOpen) renderContinueWatchingPanel(); } catch (e) { } }, loadedmetadata: () => { updateContinueWatchingButton(); if (rt.panelOpen) renderContinueWatchingPanel(); }, seeked: () => { updateContinueWatchingButton(); const vid = getCurrentVideoId(); if (rt.panelOpen && vid) updateContinueWatchingPanelRow(vid); }, }; v.addEventListener('timeupdate', rt.handlers.timeupdate, { passive: true }); v.addEventListener('pause', rt.handlers.pause, { passive: true }); v.addEventListener('ended', rt.handlers.ended, { passive: true }); v.addEventListener('loadedmetadata', rt.handlers.loadedmetadata, { passive: true }); v.addEventListener('seeked', rt.handlers.seeked, { passive: true }); } updateContinueWatchingButton(); updateContinueWatchingHistoryUi(); } // ------------------------------ // Feature: Show channel name on Shorts list (Home / feeds) // Adapted from: @𝖢𝖸 𝖥𝗎𝗇𝗀 // ------------------------------ function setupShortsChannelNameFeature(enabled) { __ytToolsRuntime.shortsChannelName.enabled = !!enabled; document.documentElement.dataset.mdcmShortsChannelName = enabled ? '1' : '0'; // Disable: stop observers to reduce overhead if (!enabled) { try { __ytToolsRuntime.shortsChannelName.observer?.disconnect?.(); } catch (e) { } try { __ytToolsRuntime.shortsChannelName.io?.disconnect?.(); } catch (e) { } __ytToolsRuntime.shortsChannelName.observer = null; __ytToolsRuntime.shortsChannelName.io = null; clearTimeout(__ytToolsRuntime.shortsChannelName.scanT); __ytToolsRuntime.shortsChannelName.scanT = null; return; } const rt = __ytToolsRuntime.shortsChannelName; const getShortsVideoIdFromItem = (item) => { const a = item.querySelector('a[href^="/shorts/"]'); const href = a?.getAttribute('href') || ''; const m = href.match(/\/shorts\/([^/?]+)/); return m?.[1] || null; }; const findSubhead = (item) => { return item.querySelector( '.ShortsLockupViewModelHostOutsideMetadataSubhead,' + ' .shortsLockupViewModelHostOutsideMetadataSubhead,' + ' .ShortsLockupViewModelHostMetadataSubhead,' + ' .shortsLockupViewModelHostMetadataSubhead' ); }; const ensureLabel = (subhead) => { const parent = subhead?.parentElement; if (!parent) return null; let el = parent.querySelector('.yt-tools-shorts-channel-name'); if (!el) { el = document.createElement('div'); el.className = 'yt-tools-shorts-channel-name'; el.textContent = ''; parent.insertBefore(el, subhead); } return el; }; const tryExtractChannelNameFromDom = (item) => { const a = item.querySelector('a[href^="/@"], a[href^="/channel/"]'); const name = (a?.textContent || a?.getAttribute('title') || '').trim(); return name || null; }; const fetchChannelNameFromWatch = (videoId) => { rt.fetchChain = rt.fetchChain.then(async () => { if (rt.cache.has(videoId)) return rt.cache.get(videoId); let res = null; try { res = await fetch(`/watch?v=${videoId}`, { method: 'GET', credentials: 'same-origin', cache: 'force-cache', }); } catch (e) { return ''; } if (!res?.ok) return ''; const html = await res.text(); const idx = html.indexOf('itemprop="author"'); if (idx < 0) return ''; const start = html.lastIndexOf('<span', idx); const end = html.indexOf('</span>', idx); if (start < 0 || end < 0) return ''; const chunk = html.slice(start, end + 7); const doc = new DOMParser().parseFromString(chunk, 'text/html'); const link = doc.querySelector('link[itemprop="name"]'); return (link?.getAttribute('content') || '').trim(); }); return rt.fetchChain; }; const getChannelName = (videoId, item) => { const cached = rt.cache.get(videoId); if (cached) return Promise.resolve(cached); const persisted = getShortsChannelFromPersistedCache(videoId); if (persisted) { rt.cache.set(videoId, persisted); return Promise.resolve(persisted); } const domName = tryExtractChannelNameFromDom(item); if (domName) { rt.cache.set(videoId, domName); setShortsChannelToPersistedCache(videoId, domName); return Promise.resolve(domName); } const inflight = rt.inflight.get(videoId); if (inflight) return inflight; const p = fetchChannelNameFromWatch(videoId) .then((name) => { const finalName = (name || '').trim(); if (finalName) { rt.cache.set(videoId, finalName); setShortsChannelToPersistedCache(videoId, finalName); } return finalName; }) .finally(() => { rt.inflight.delete(videoId); }); rt.inflight.set(videoId, p); return p; }; const processItem = (item) => { if (!(item instanceof Element)) return; if (item.dataset.ytToolsShortsChannelProcessed === '1') return; const subhead = findSubhead(item); if (!subhead) return; const videoId = getShortsVideoIdFromItem(item); if (!videoId) return; item.dataset.ytToolsShortsChannelProcessed = '1'; item.dataset.ytToolsShortsVideoId = videoId; const label = ensureLabel(subhead); if (!label) return; label.textContent = ''; rt.io?.observe(item); }; if (!rt.io) { rt.io = new IntersectionObserver((entries) => { for (const entry of entries) { if (!entry.isIntersecting) continue; const item = entry.target; const videoId = item?.dataset?.ytToolsShortsVideoId; const subhead = findSubhead(item); const label = subhead?.parentElement?.querySelector?.('.yt-tools-shorts-channel-name'); if (!videoId || !label) { rt.io.unobserve(item); continue; } getChannelName(videoId, item) .then((name) => { if (name) label.textContent = name; }) .finally(() => { rt.io.unobserve(item); }); } }, { threshold: 0.15 }); } const scan = () => { clearTimeout(rt.scanT); rt.scanT = setTimeout(() => { document .querySelectorAll('ytm-shorts-lockup-view-model, ytm-shorts-lockup-view-model-v2') .forEach(processItem); }, 120); }; if (!rt.observer) { rt.observer = new MutationObserver(scan); const observeTarget = document.querySelector('#page-manager') || document.body; rt.observer.observe(observeTarget, { childList: true, subtree: true }); } scan(); } // ------------------------------ // Feature: Show cached rating/likes/dislikes on video cards (watch related + home/search) // ------------------------------ function getVideoIdFromLockup(lockup) { const a = lockup.querySelector('a[href*="watch?v="]'); if (a) { const m = (a.getAttribute('href') || '').match(/[?&]v=([^&]+)/); if (m) return m[1]; } const el = lockup.querySelector('[class*="content-id-"]'); if (el) { const m = el.className.match(/content-id-([A-Za-z0-9_-]+)/); if (m) return m[1]; } return null; } function createSvgIconFromString(svgString, sizePx) { const div = document.createElement('div'); div.innerHTML = safeHTML(svgString.trim()); const svg = div.firstElementChild; if (!svg) return null; svg.setAttribute('width', String(sizePx || 14)); svg.setAttribute('height', String(sizePx || 14)); svg.style.display = 'inline-block'; svg.style.verticalAlign = 'middle'; svg.style.marginRight = '2px'; return svg; } const LOCKUP_RATING_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-star"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873l-6.158 -3.245" /></svg>'; const LOCKUP_LIKE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-thumb-up"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3" /></svg>'; const LOCKUP_DISLIKE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-thumb-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 13v-8a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1v7a1 1 0 0 0 1 1h3a4 4 0 0 1 4 4v1a2 2 0 0 0 4 0v-5h3a2 2 0 0 0 2 -2l-1 -5a2 3 0 0 0 -2 -2h-7a3 3 0 0 0 -3 3" /></svg>'; function injectLockupCachedStats() { if (!window.location.href.includes('youtube.com')) return; document.querySelectorAll('yt-lockup-view-model').forEach((lockup) => { if (lockup.hasAttribute('data-yt-tools-lockup-stats')) return; const videoId = getVideoIdFromLockup(lockup); if (!videoId) return; const cached = getLikesDislikesFromPersistedCache(videoId); if (!cached) return; const hasRating = cached.rating != null; const hasLikes = cached.likes != null; const hasDislikes = cached.dislikes != null; if (!hasRating && !hasLikes && !hasDislikes) return; const meta = lockup.querySelector('yt-content-metadata-view-model'); if (!meta) return; const row = document.createElement('div'); row.className = 'yt-content-metadata-view-model__metadata-row'; row.setAttribute('data-yt-tools-lockup-stats-row', '1'); const wrap = document.createElement('span'); wrap.className = 'yt-core-attributed-string yt-content-metadata-view-model__metadata-text yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--link-inherit-color'; wrap.setAttribute('dir', 'auto'); wrap.setAttribute('role', 'text'); const sep = () => { const s = document.createTextNode(' · '); return s; }; if (hasRating) { const ratingIcon = createSvgIconFromString(LOCKUP_RATING_SVG, 14); if (ratingIcon) wrap.appendChild(ratingIcon); wrap.appendChild(document.createTextNode(' ' + cached.rating.toFixed(1))); if (hasLikes || hasDislikes) wrap.appendChild(sep()); } if (hasLikes) { const likeIcon = createSvgIconFromString(LOCKUP_LIKE_SVG, 14); if (likeIcon) wrap.appendChild(likeIcon); wrap.appendChild(document.createTextNode(' ' + FormatterNumber(cached.likes, 0))); if (hasDislikes) wrap.appendChild(sep()); } if (hasDislikes) { const dislikeIcon = createSvgIconFromString(LOCKUP_DISLIKE_SVG, 14); if (dislikeIcon) wrap.appendChild(dislikeIcon); wrap.appendChild(document.createTextNode(' ' + FormatterNumber(cached.dislikes, 0))); } row.appendChild(wrap); meta.appendChild(row); lockup.setAttribute('data-yt-tools-lockup-stats', videoId); }); } function getVideoIdFromShortsLockup(item) { if (item.dataset.ytToolsShortsVideoId) return item.dataset.ytToolsShortsVideoId; var a = item.querySelector('a[href^="/shorts/"]'); if (!a) return null; var m = (a.getAttribute('href') || '').match(/\/shorts\/([^/?]+)/); return m ? m[1] : null; } function injectShortsLockupCachedStats() { if (!window.location.href.includes('youtube.com')) return; // Process inner lockup (has metadata); v2 wraps it and would duplicate document.querySelectorAll('ytm-shorts-lockup-view-model').forEach(function (item) { if (item.hasAttribute('data-yt-tools-shorts-stats')) return; var videoId = getVideoIdFromShortsLockup(item); if (!videoId) return; var cached = getLikesDislikesFromPersistedCache(videoId); if (!cached) return; var hasLikes = cached.likes != null; var hasDislikes = cached.dislikes != null; if (!hasLikes && !hasDislikes) return; var subhead = item.querySelector( '.ShortsLockupViewModelHostOutsideMetadataSubhead,' + '.shortsLockupViewModelHostOutsideMetadataSubhead,' + '.ShortsLockupViewModelHostMetadataSubhead,' + '.shortsLockupViewModelHostMetadataSubhead' ); if (!subhead || !subhead.parentElement) return; var wrap = document.createElement('span'); wrap.className = 'yt-core-attributed-string yt-content-metadata-view-model__metadata-text yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--link-inherit-color yt-tools-shorts-stats-row'; wrap.setAttribute('dir', 'auto'); wrap.setAttribute('role', 'text'); wrap.setAttribute('style', 'color: #aaa !important;'); var sep = function () { return document.createTextNode(' \u00b7 '); }; if (hasLikes) { var likeIcon = createSvgIconFromString(LOCKUP_LIKE_SVG, 12); if (likeIcon) { likeIcon.style.setProperty('color', '#aaa', 'important'); wrap.appendChild(likeIcon); } wrap.appendChild(document.createTextNode(' ' + FormatterNumber(cached.likes, 0))); if (hasDislikes) wrap.appendChild(sep()); } if (hasDislikes) { var dislikeIcon = createSvgIconFromString(LOCKUP_DISLIKE_SVG, 12); if (dislikeIcon) { dislikeIcon.style.setProperty('color', '#aaa', 'important'); wrap.appendChild(dislikeIcon); } wrap.appendChild(document.createTextNode(' ' + FormatterNumber(cached.dislikes, 0))); } var row = document.createElement('div'); row.className = 'yt-tools-shorts-stats-wrap'; row.setAttribute('style', 'color: #aaa !important;'); row.appendChild(wrap); subhead.parentElement.appendChild(row); item.setAttribute('data-yt-tools-shorts-stats', videoId); }); } function createLockupStatsObserver(target) { var lockupStatsDebounceT = null; var lockupStatsScheduled = false; var obs = new MutationObserver(function () { if (lockupStatsScheduled) return; lockupStatsScheduled = true; clearTimeout(lockupStatsDebounceT); lockupStatsDebounceT = setTimeout(function () { lockupStatsScheduled = false; if (!window.location.href.includes('youtube.com')) return; injectLockupCachedStats(); injectShortsLockupCachedStats(); // Single extra pass for late-rendered lockups (reduced from 3 passes) setTimeout(function () { if (!window.location.href.includes('youtube.com')) return; if (!hasUnprocessedLockups()) return; injectLockupCachedStats(); injectShortsLockupCachedStats(); }, 1200); }, 280); }); obs.observe(target, { childList: true, subtree: true }); return obs; } function retargetLockupStatsObserverIfNeeded() { if (!window.location.href.includes('youtube.com/watch')) return; var secondary = document.getElementById('secondary') || document.querySelector('ytd-watch-next-secondary-results-renderer'); if (!secondary || !secondary.parentNode) return; if (__ytToolsRuntime.lockupCachedStatsObserveTarget === secondary) return; var obs = __ytToolsRuntime.lockupCachedStatsObserver; if (!obs) return; obs.disconnect(); __ytToolsRuntime.lockupCachedStatsObserver = createLockupStatsObserver(secondary); __ytToolsRuntime.lockupCachedStatsObserveTarget = secondary; } function hasUnprocessedLockups() { var normal = document.querySelectorAll('yt-lockup-view-model:not([data-yt-tools-lockup-stats])').length > 0; var shorts = document.querySelectorAll('ytm-shorts-lockup-view-model:not([data-yt-tools-shorts-stats])').length > 0; return normal || shorts; } function runLockupCachedStatsCatchUp() { if (!window.location.href.includes('youtube.com')) return; if (document.visibilityState !== 'visible') return; if (!hasUnprocessedLockups()) return; injectLockupCachedStats(); injectShortsLockupCachedStats(); } function setupLockupCachedStats() { if (!window.location.href.includes('youtube.com')) return; injectLockupCachedStats(); injectShortsLockupCachedStats(); var secondary = document.getElementById('secondary') || document.querySelector('ytd-watch-next-secondary-results-renderer'); var observeTarget = secondary && secondary.parentNode ? secondary : document.body; if (__ytToolsRuntime.lockupCachedStatsObserver) { if (observeTarget !== __ytToolsRuntime.lockupCachedStatsObserveTarget) { __ytToolsRuntime.lockupCachedStatsObserver.disconnect(); __ytToolsRuntime.lockupCachedStatsObserver = createLockupStatsObserver(observeTarget); __ytToolsRuntime.lockupCachedStatsObserveTarget = observeTarget; } return; } __ytToolsRuntime.lockupCachedStatsObserver = createLockupStatsObserver(observeTarget); __ytToolsRuntime.lockupCachedStatsObserveTarget = observeTarget; // Catch-up interval: apply stats to any new cards (scroll, filters, SPA) every 1.8s when there are unprocessed lockups if (!__ytToolsRuntime.lockupCachedStatsIntervalId) { __ytToolsRuntime.lockupCachedStatsIntervalId = setInterval(runLockupCachedStatsCatchUp, 1800); } } // ------------------------------ // Feature: Bookmarks per video (persisted) // ------------------------------ function getBookmarksForVideo(videoId) { const all = readJsonGM(STORAGE_KEYS_MDCM.BOOKMARKS, {}); const list = Array.isArray(all[videoId]) ? all[videoId] : []; return { all, list }; } function saveBookmark(videoId, seconds, label) { const { all, list } = getBookmarksForVideo(videoId); const t = Math.max(0, Math.floor(Number(seconds) || 0)); const exists = list.some(b => b && b.t === t); const item = { t, label: (label || formatTimeShort(t)).trim(), createdAt: Date.now() }; const nextList = exists ? list.map(b => (b.t === t ? item : b)) : [...list, item]; nextList.sort((a, b) => a.t - b.t); all[videoId] = nextList; writeJsonGM(STORAGE_KEYS_MDCM.BOOKMARKS, all); } function deleteBookmark(videoId, seconds) { const { all, list } = getBookmarksForVideo(videoId); const t = Math.max(0, Math.floor(Number(seconds) || 0)); all[videoId] = list.filter(b => b && b.t !== t); writeJsonGM(STORAGE_KEYS_MDCM.BOOKMARKS, all); } function renderBookmarksPanel(videoId) { const panel = $id('yt-bookmarks-panel'); if (!panel) return; const { list } = getBookmarksForVideo(videoId); if (!list.length) { panel.innerHTML = safeHTML(`<div class="yt-bm-empty">No bookmarks yet. Click ★ to save one.</div>`); return; } panel.innerHTML = safeHTML(list .map((b) => { const time = formatTimeShort(b.t); const safeLabel = (b.label || time).replace(/</g, '<').replace(/>/g, '>'); return ` <div class="yt-bm-item"> <button type="button" class="yt-bm-go" data-action="go" data-t="${b.t}" title="Go to ${time}">${time}</button> <div class="yt-bm-label" title="${safeLabel}">${safeLabel}</div> <button type="button" class="yt-bm-del" data-action="del" data-t="${b.t}" title="Delete">✕</button> </div> `; }) .join('')); } function applyBookmarksIfEnabled(settings) { const addBtn = $id('yt-bookmark-add'); const toggleBtn = $id('yt-bookmark-toggle'); const panel = $id('yt-bookmarks-panel'); if (!addBtn || !toggleBtn || !panel) return; const enabled = !!settings?.bookmarks; addBtn.style.display = enabled ? 'inline-flex' : 'none'; toggleBtn.style.display = enabled ? 'inline-flex' : 'none'; panel.style.display = enabled && __ytToolsRuntime.bookmarksPanelOpen ? 'block' : 'none'; if (!enabled) return; const videoId = getCurrentVideoId(); if (!videoId) return; renderBookmarksPanel(videoId); if (__ytToolsRuntime.bookmarkClickHandlerInitialized) return; __ytToolsRuntime.bookmarkClickHandlerInitialized = true; document.addEventListener('click', (e) => { const target = e.target; if (!(target instanceof Element)) return; const add = target.closest('#yt-bookmark-add'); const tog = target.closest('#yt-bookmark-toggle'); const actionBtn = target.closest('[data-action][data-t]'); if (add) { e.preventDefault(); e.stopPropagation(); const v = $e('video'); const vid = getCurrentVideoId(); if (!v || !vid) return; const t = Math.floor(v.currentTime || 0); const defaultLabel = formatTimeShort(t); const label = prompt('Bookmark name (optional):', defaultLabel) || defaultLabel; saveBookmark(vid, t, label); __ytToolsRuntime.bookmarksPanelOpen = true; panel.style.display = 'block'; renderBookmarksPanel(vid); Notify('success', `Bookmark saved at ${defaultLabel}`); return; } if (tog) { e.preventDefault(); e.stopPropagation(); __ytToolsRuntime.bookmarksPanelOpen = !__ytToolsRuntime.bookmarksPanelOpen; panel.style.display = __ytToolsRuntime.bookmarksPanelOpen ? 'block' : 'none'; const vid = getCurrentVideoId(); if (vid && __ytToolsRuntime.bookmarksPanelOpen) renderBookmarksPanel(vid); return; } if (actionBtn) { e.preventDefault(); e.stopPropagation(); const action = actionBtn.getAttribute('data-action'); const t = Number(actionBtn.getAttribute('data-t')); const v = $e('video'); const vid = getCurrentVideoId(); if (!v || !vid) return; if (action === 'go') { v.currentTime = Math.max(0, t || 0); v.play?.().catch(() => { }); } else if (action === 'del') { deleteBookmark(vid, t); renderBookmarksPanel(vid); } } }); } // ------------------------------ // Feature: Like vs Dislike bar // ------------------------------ function parseCountText(text) { if (!text) return null; const s0 = String(text).trim().toLowerCase(); if (!s0) return null; let mult = 1; let s = s0.replace(/\s+/g, ''); if (s.includes('mil')) { mult = 1000; s = s.replace('mil', ''); } else if (s.includes('k')) { mult = 1000; s = s.replace('k', ''); } else if (s.includes('m')) { mult = 1000000; s = s.replace('m', ''); } // normalize decimal separators s = s.replace(/[^\d.,]/g, ''); if (!s) return null; // If both separators exist, assume last is decimal const lastDot = s.lastIndexOf('.'); const lastComma = s.lastIndexOf(','); let nStr = s; if (lastDot !== -1 && lastComma !== -1) { const dec = Math.max(lastDot, lastComma); const intPart = s.slice(0, dec).replace(/[.,]/g, ''); const decPart = s.slice(dec + 1); nStr = `${intPart}.${decPart}`; } else { // Use dot as decimal nStr = s.replace(',', '.'); } const num = Number.parseFloat(nStr); if (!Number.isFinite(num)) return null; return Math.round(num * mult); } async function ensureDislikesForCurrentVideo() { const videoId = getCurrentVideoId(); if (!videoId) return null; const now = Date.now(); if (__ytToolsRuntime.dislikesCache.videoId === videoId && __ytToolsRuntime.dislikesCache.dislikes != null && (now - __ytToolsRuntime.dislikesCache.ts) < 10 * 60 * 1000) { return __ytToolsRuntime.dislikesCache; } const persisted = getLikesDislikesFromPersistedCache(videoId); if (persisted && persisted.dislikes != null) { __ytToolsRuntime.dislikesCache = { videoId, likes: persisted.likes, dislikes: persisted.dislikes, viewCount: persisted.viewCount, rating: persisted.rating, ts: now }; return __ytToolsRuntime.dislikesCache; } try { const res = await fetch(`${apiDislikes}${videoId}`); const data = await res.json(); const dislikes = Number(data?.dislikes); const likes = Number(data?.likes); const viewCount = Number(data?.viewCount); const rating = Number(data?.rating); const now = Date.now(); if (Number.isFinite(dislikes)) { __ytToolsRuntime.dislikesCache = { videoId, likes: Number.isFinite(likes) ? likes : getLikesFromDom(), dislikes, viewCount, rating, ts: now }; setLikesDislikesToPersistedCache( videoId, __ytToolsRuntime.dislikesCache.likes, dislikes, Number.isFinite(viewCount) ? viewCount : undefined, (Number.isFinite(rating) && rating >= 0 && rating <= 5) ? rating : undefined ); return __ytToolsRuntime.dislikesCache; } } catch (e) { console.error('[YT Tools] Error fetching dislikes:', e); } return null; } function getLikesFromDom() { // Try grab visible like count (YouTube UI varies a lot) const likeBtn = $e('#top-level-buttons-computed like-button-view-model button-view-model button') || $e('#top-level-buttons-computed like-button-view-model button') || $e('#top-level-buttons-computed ytd-toggle-button-renderer:nth-child(1)') || $e('segmented-like-dislike-button-view-model like-button-view-model'); if (!likeBtn) return null; // Prefer visible counter first; aria-label can include locale thousands separators (17.606) that are ambiguous. const candidates = [ likeBtn.querySelector?.('.yt-spec-button-shape-next__button-text-content')?.textContent, likeBtn.textContent, likeBtn.getAttribute?.('aria-label'), ].filter(Boolean); for (const txt of candidates) { const n = parseCountText(txt); if (n != null) return n; } return null; } function updateLikeDislikeBar(likes, dislikes) { // Prefer placing it above the "copy description" button (as requested). const copyDesc = $id('button_copy_description'); const host = $e('#top-level-buttons-computed') || $e('ytd-watch-metadata #top-level-buttons-computed'); if (!host && !copyDesc) return; let bar = $id('yt-like-dislike-bar-mdcm'); if (!bar) { bar = document.createElement('div'); bar.id = 'yt-like-dislike-bar-mdcm'; bar.innerHTML = safeHTML(`<div class="like"></div><div class="dislike"></div>`); if (copyDesc) { copyDesc.insertAdjacentElement('beforebegin', bar); } else { host.appendChild(bar); } } else if (copyDesc && bar.previousElementSibling !== copyDesc) { // Keep it near the copy description area if the DOM changed try { copyDesc.insertAdjacentElement('beforebegin', bar); } catch (e) { } } if (!Number.isFinite(likes) || !Number.isFinite(dislikes) || likes + dislikes <= 0) { bar.style.display = 'none'; return; } const total = likes + dislikes; const likePct = Math.max(0, Math.min(100, (likes / total) * 100)); const dislikePct = 100 - likePct; bar.style.display = 'block'; const likeEl = bar.querySelector('.like'); const dislikeEl = bar.querySelector('.dislike'); likeEl.style.width = `${likePct}%`; dislikeEl.style.width = `${dislikePct}%`; bar.title = `Likes: ${likes.toLocaleString()} | Dislikes: ${dislikes.toLocaleString()}`; } async function applyLikeDislikeBarIfEnabled(settings) { if (!settings?.likeDislikeBar) { const existing = $id('yt-like-dislike-bar-mdcm'); if (existing) existing.style.display = 'none'; return; } if (!window.location.href.includes('youtube.com/watch')) return; const videoId = getCurrentVideoId(); if (!videoId) return; const data = await ensureDislikesForCurrentVideo(); if (!data || data.likes == null || data.dislikes == null) return; updateLikeDislikeBar(data.likes, data.dislikes); } // Retry helper (YT often renders likes late; keep it lightweight) function scheduleLikeBarUpdate(settings, attempts = 4) { if (!settings?.likeDislikeBar) return; let i = 0; const tick = async () => { i += 1; await applyLikeDislikeBarIfEnabled(settings); const bar = $id('yt-like-dislike-bar-mdcm'); if (bar && bar.style.display !== 'none') return; if (i < attempts) setTimeout(tick, 800); }; setTimeout(tick, 300); } // Dislikes video async function videoDislike() { if (isYTMusic) return; validoUrl = document.location.href; const validoVentana = $e('#below > ytd-watch-metadata > div'); if (validoVentana != undefined && document.location.href.split('?v=')[0].includes('youtube.com/watch')) { let localVideoId = paramsVideoURL(); if (!localVideoId) return; validoUrl = localVideoId; const data = await ensureDislikesForCurrentVideo(); if (!data || data.dislikes == null) return; const dislikes = data.dislikes; const dislikes_btn = $e('#top-level-buttons-computed > segmented-like-dislike-button-view-model > yt-smartimation > div > div > dislike-button-view-model > toggle-button-view-model > button-view-model > button'); if (dislikes_btn != null) { const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); // Find or create our custom count element let textContent = dislikes_btn.querySelector('.yt-tools-dislike-count'); if (!textContent) { textContent = document.createElement('div'); textContent.className = 'ytSpecButtonShapeNextButtonTextContent yt-tools-dislike-count'; // Insert it after the icon container const iconDiv = dislikes_btn.querySelector('.ytSpecButtonShapeNextIcon'); if (iconDiv) { iconDiv.insertAdjacentElement('afterend', textContent); // Convert button style to icon+text layout dislikes_btn.classList.add('ytSpecButtonShapeNextIconLeading'); dislikes_btn.classList.remove('ytSpecButtonShapeNextIconButton'); } else { dislikes_btn.appendChild(textContent); } } if (settings.dislikes) { textContent.textContent = FormatterNumber(dislikes, 0); textContent.style.display = 'block'; showDislikes = true; } else { textContent.style.display = 'none'; showDislikes = false; } const likes_btn = $e('#top-level-buttons-computed like-button-view-model button-view-model button') || $e('#top-level-buttons-computed like-button-view-model button') || $e('#top-level-buttons-computed ytd-toggle-button-renderer:nth-child(1)') || $e('segmented-like-dislike-button-view-model like-button-view-model button'); // Capture current state as "initial" for this data load const currentIsPressed = dislikes_btn.getAttribute('aria-pressed') === 'true'; dislikes_btn.dataset.initialState = currentIsPressed; dislikes_btn.dataset.originalCount = dislikes; if (likes_btn) { likes_btn.dataset.initialState = likes_btn.getAttribute('aria-pressed') === 'true'; likes_btn.dataset.originalCount = data.likes || 0; } const updateCount = () => { // Dislikes calculation const isDislikePressed = dislikes_btn.getAttribute('aria-pressed') === 'true'; const wasDislikePressed = dislikes_btn.dataset.initialState === 'true'; const originalDislikes = Number(dislikes_btn.dataset.originalCount); let dislikeOffset = 0; if (!wasDislikePressed && isDislikePressed) dislikeOffset = 1; else if (wasDislikePressed && !isDislikePressed) dislikeOffset = -1; const newDislikes = Math.max(0, originalDislikes + dislikeOffset); // Likes calculation let newLikes = data.likes || 0; if (likes_btn) { const isLikePressed = likes_btn.getAttribute('aria-pressed') === 'true'; const wasLikePressed = likes_btn.dataset.initialState === 'true'; const originalLikes = Number(likes_btn.dataset.originalCount); let likeOffset = 0; if (!wasLikePressed && isLikePressed) likeOffset = 1; else if (wasLikePressed && !isLikePressed) likeOffset = -1; newLikes = Math.max(0, originalLikes + likeOffset); } // Update run-time cache if (__ytToolsRuntime.dislikesCache.videoId === localVideoId) { __ytToolsRuntime.dislikesCache.dislikes = newDislikes; __ytToolsRuntime.dislikesCache.likes = newLikes; // Also persist it so F5 uses the updated count as "original" setLikesDislikesToPersistedCache( localVideoId, newLikes, newDislikes, __ytToolsRuntime.dislikesCache.viewCount, __ytToolsRuntime.dislikesCache.rating ); } if (settings.dislikes && textContent) { textContent.textContent = FormatterNumber(newDislikes, 0); } // Sync the like/dislike bar immediately if (settings.likeDislikeBar) { updateLikeDislikeBar(newLikes, newDislikes); } }; // Initial update updateCount(); // Use MutationObserver for robust state tracking (handles Like button clicks too) if (!dislikes_btn.dataset.observerInitialized) { dislikes_btn.dataset.observerInitialized = 'true'; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.attributeName === 'aria-pressed') { updateCount(); } } }); observer.observe(dislikes_btn, { attributes: true, attributeFilter: ['aria-pressed'] }); if (likes_btn) observer.observe(likes_btn, { attributes: true, attributeFilter: ['aria-pressed'] }); // Also handle direct clicks just in case dislikes_btn.addEventListener('click', () => { setTimeout(updateCount, 150); }); if (likes_btn) { likes_btn.addEventListener('click', () => { setTimeout(updateCount, 150); }); } } try { scheduleLikeBarUpdate(settings, 5); } catch (e) { } } } } // dislikes shorts + views button (viewCount from Return YouTube Dislike API) async function shortDislike() { validoUrl = document.location.href; const validoVentanaShort = $m( "#button-bar > reel-action-bar-view-model > dislike-button-view-model > toggle-button-view-model > button-view-model > label > div > span" ); if (validoVentanaShort != undefined && document.location.href.split('/')[3] === 'shorts') { validoUrl = document.location.href.split('/')[4]; let dislikes = null; let viewCount = null; let rating = null; const persisted = getLikesDislikesFromPersistedCache(validoUrl); if (persisted && persisted.dislikes != null) { dislikes = persisted.dislikes; viewCount = persisted.viewCount ?? null; rating = persisted.rating ?? null; } else { const urlShorts = `${apiDislikes}${validoUrl}`; try { const respuesta = await fetch(urlShorts); const datosShort = await respuesta.json(); dislikes = Number(datosShort?.dislikes); viewCount = Number(datosShort?.viewCount); rating = Number(datosShort?.rating); if (Number.isFinite(dislikes)) setLikesDislikesToPersistedCache(validoUrl, undefined, dislikes, Number.isFinite(viewCount) ? viewCount : undefined, (Number.isFinite(rating) && rating >= 0 && rating <= 5) ? rating : undefined); } catch (error) { console.log(error); } } if (dislikes != null) { const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); for (let i = 0; i < validoVentanaShort.length; i++) { const el = validoVentanaShort[i]; if (settings.dislikes) { if (!el.dataset.originalLabel) el.dataset.originalLabel = el.textContent; el.textContent = `${FormatterNumber(dislikes, 0)}`; } else { if (el.dataset.originalLabel) { el.textContent = el.dataset.originalLabel; delete el.dataset.originalLabel; } } } } if (__ytToolsRuntime.updateShortsViewsButton) __ytToolsRuntime.updateShortsViewsButton(validoUrl, viewCount); if (__ytToolsRuntime.updateShortsRatingButton) __ytToolsRuntime.updateShortsRatingButton(validoUrl, rating); } } let showDislikes = false; window.addEventListener('yt-navigate-finish', () => { if (isYTMusic) return; // Dislikes UI not available on YTM const svgDislike = $e('.svg-dislike-ico'); if (!svgDislike && showDislikes) { setTimeout(async () => { await videoDislike(); await shortDislike(); }, 1500); } }); // Styles for our enhancement panel const thumbnailVideo = ` <button title="Image video" class="botones_div" type="button" id="imagen"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-photo-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M15 8h.01"></path> <path d="M12.5 21h-6.5a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v6.5"></path> <path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l4 4"></path> <path d="M14 14l1 -1c.653 -.629 1.413 -.815 2.13 -.559"></path> <path d="M19 16v6"></path> <path d="M22 19l-3 3l-3 -3"></path> </svg> </button> `; const repeatVideo = ` <button title="Repeat video" class="botones_div" type="button" id="repeatvideo"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-repeat" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3"></path> <path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3"></path> </svg> </button> `; const downloadMP4Mp3 = ` <button title="MP4" type="button" class="btn1 botones_div"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M14 3v4a1 1 0 0 0 1 1h4"></path> <path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path> <path d="M12 17v-6"></path> <path d="M9.5 14.5l2.5 2.5l2.5 -2.5"></path> </svg> </button> <button title="MP3" type="button" class="btn2 botones_div"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-music" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M14 3v4a1 1 0 0 0 1 1h4"></path> <path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path> <path d="M11 16m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path> <path d="M12 16l0 -5l2 1"></path> </svg> </button> <button title="Close" type="button" class="btn3 botones_div"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-circle-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path> <path d="M10 10l4 4m0 -4l-4 4"></path> </svg> </button> `; const pictureToPicture = ` <button title="Picture to picture" type="button" class="video_picture_to_picture botones_div"> <svg width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v4" /><path d="M14 14m0 1a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z" /></svg> </button> `; const screenShot = ` <button title="Screenshot video" type="button" class="screenshot_video botones_div"> <svg width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M6 13l2.644 -2.644a1.21 1.21 0 0 1 1.712 0l3.644 3.644" /><path d="M13 13l1.644 -1.644a1.21 1.21 0 0 1 1.712 0l1.644 1.644" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /></svg> </button> `; const bookmarkAddBtn = ` <button title="Add bookmark" type="button" id="yt-bookmark-add" class="botones_div"> <svg width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M7 4h10a2 2 0 0 1 2 2v14l-7 -4l-7 4v-14a2 2 0 0 1 2 -2z" /> <path d="M12 7v6" /> <path d="M9 10h6" /> </svg> </button> `; const bookmarkToggleBtn = ` <button title="Show bookmarks" type="button" id="yt-bookmark-toggle" class="botones_div"> <svg width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M9 6h11" /> <path d="M9 12h11" /> <path d="M9 18h11" /> <path d="M5 6h.01" /> <path d="M5 12h.01" /> <path d="M5 18h.01" /> </svg> </button> `; const continueWatchingHistoryBtn = ` <button title="History" type="button" id="yt-cw-history-toggle" class="botones_div" style="display:none;"> <svg width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M12 8v4l3 3" /> <path d="M3 12a9 9 0 1 0 3 -6.7" /> <path d="M3 4v4h4" /> </svg> </button> `; const menuBotones = ` <main class="yt-tools-container"> <div class="container"> <form> <div class="containerButtons"> ${thumbnailVideo} ${!isYTMusic ? repeatVideo : ''} ${bookmarkAddBtn} ${bookmarkToggleBtn} ${continueWatchingHistoryBtn} ${downloadMP4Mp3} ${pictureToPicture} ${screenShot} </div> <div id="yt-bookmarks-panel" class="yt-bookmarks-panel" style="display:none;"></div> <div id="yt-continue-watching-panel" class="yt-continue-watching-panel" style="display:none;"></div> <div> </div> </form> </div> <div class="content_collapsible_colors" style="margin-top: 10px"> <form class="formulariodescarga ocultarframe" action=""> <div class="containerall"> <select class="selectcalidades ocultarframe" required> <option selected disabled>Video Quality</option> <option value="144">144p MP4</option> <option value="240">240p MP4</option> <option value="360">360p MP4</option> <option value="480">480p MP4</option> <option value="720">720p HD MP4 Default</option> <option value="1080">1080p FULL HD MP4</option> <option value="1440">1440p 2K WEBM</option> <option value="4k">2160p 4K WEBM</option> <option value="8k">4320p 8K WEBM</option> </select> <div id="descargando" class="download-container ocultarframe"> <button class="progress-retry-btn" title="Retry" style="display: none;"> <i class="fa-solid fa-rotate-right"></i> </button> <button class="download-again-btn" title="Download again" style="display: none;"> <i class="fa-solid fa-download"></i> </button> <div class="download-info"> <span class="download-text">Download Video And Please Wait...</span> <span class="download-quality"></span> </div> <div class="download-actions"> <button class="download-btn video-btn">Download</button> <button class="retry-btn" style="display: none;">Retry</button> </div> <div class="progress-container" style="display: none;"> <div class="progress-bar"> <div class="progress-fill"></div> </div> <span class="progress-text">0%</span> </div> <div class="download-footer"> <a href="https://github.com/akari310/" target="_blank"> <i class="fa-brands fa-github"></i> by: Akari</a> </div> <h1 class="text-description-download"> <span >Enable pop-ups on YouTube to download audio or video</span> </h1> </div> </div> </form> <form class="formulariodescargaaudio ocultarframe" action=""> <div class="containerall"> <select class="selectcalidadesaudio ocultarframeaudio" required> <option selected disabled>Audio Quality</option> <option value="flac">Audio FLAC UHQ</option> <option value="wav">Audio WAV UHQ</option> <option value="webm">Audio WEBM UHQ</option> <option value="mp3">Audio MP3 Default</option> <option value="m4a">Audio M4A</option> <option value="aac">Audio AAC</option> <option value="opus">Audio OPUS</option> <option value="ogg">Audio OGG</option> </select> <div id="descargandomp3" class="download-container ocultarframeaudio"> <button class="progress-retry-btn" title="Retry" style="display: none;"> <i class="fa-solid fa-rotate-right"></i> </button> <button class="download-again-btn" title="Download again" style="display: none;"> <i class="fa-solid fa-download"></i> </button> <div class="download-info"> <span class="download-text">Download Audio And Please Wait...</span> <span class="download-quality"></span> </div> <div class="download-actions"> <button class="download-btn audio-btn">Download</button> <button class="retry-btn" style="display: none;">Retry</button> </div> <div class="progress-container" style="display: none;"> <div class="progress-bar"> <div class="progress-fill"></div> </div> <span class="progress-text">0%</span> </div> <div class="download-footer"> <a href="https://github.com/akari310/" target="_blank"><i class="fa-brands fa-github"></i> by: Akari</a> </div> <h1 class="text-description-download"> <span >Enable pop-ups on YouTube to download audio or video</span> </h1> </div> </div> </form> </main> `; function Notify(type = 'info', message = '', title = '') { const defaultTitles = { success: 'Success', error: 'Error', info: 'Information', warning: 'Warning', }; if (isYTMusic || (window.trustedTypes && window.trustedTypes.defaultPolicy === null)) { // Avoid iziToast due to innerHTML TrustedTypes violation on strict environments let toast = document.getElementById('yt-tools-custom-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'yt-tools-custom-toast'; toast.style.cssText = 'position:fixed;bottom:20px;left:20px;background:rgba(30,30,30,0.9);color:#fff;padding:12px 20px;border-radius:8px;z-index:99999;font-family:sans-serif;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);border-left:4px solid #ff0000;transition:opacity 0.3s;opacity:0;pointer-events:none;'; document.body.appendChild(toast); } toast.textContent = (title || defaultTitles[type] || 'Notification') + ': ' + message; if (type === 'success') toast.style.borderLeftColor = '#22c55e'; else if (type === 'error') toast.style.borderLeftColor = '#ef4444'; else if (type === 'warning') toast.style.borderLeftColor = '#f59e0b'; else toast.style.borderLeftColor = '#3b82f6'; toast.style.opacity = '1'; if (toast._timer) clearTimeout(toast._timer); toast._timer = setTimeout(() => { toast.style.opacity = '0'; }, 3000); return; } try { iziToast[type]({ title: title || defaultTitles[type] || 'Notification', message: message, position: 'bottomLeft', }); } catch (e) { console.warn('[yt-tools] iziToast failed:', e); } } // Helper: create SVG icon using DOM API (no innerHTML needed) function createSvgIcon(pathsData, size) { const sz = size || 24; const svgNS = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(svgNS, 'svg'); svg.setAttribute('width', String(sz)); svg.setAttribute('height', String(sz)); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('stroke-width', '2'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('fill', 'none'); svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round'); pathsData.forEach(d => { const p = document.createElementNS(svgNS, 'path'); p.setAttribute('d', d); if (d === 'M0 0h24v24H0z') p.setAttribute('fill', 'none'); svg.appendChild(p); }); return svg; } // Helper: create a toolbar button with SVG icon function makeToolBtn(title, id, className, paths) { const btn = document.createElement('button'); btn.title = title; btn.type = 'button'; if (id) btn.id = id; btn.className = (className ? className + ' ' : '') + 'botones_div'; btn.appendChild(createSvgIcon(paths)); return btn; } GM_addStyle(` @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"); @import url("https://cdn.jsdelivr.net/npm/[email protected]/dist/css/iziToast.min.css"); :root { --primary-custom: #ff0000 !important; --bg-dark-custom: #1a1a1a !important; --bg-card-custom: #252525 !important; --text-custom: #ffffff !important; --text-custom-secondary: #9e9e9e !important; --accent-custom: #ff4444 !important; } #panel-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.3); z-index: 9998; backdrop-filter: blur(2px); } #yt-enhancement-panel { z-index: 9999 !important; } body .container-mdcm { font-family: "Inter", -apple-system, sans-serif; color: var(--yt-enhance-menu-text, var(--text-custom)); } #toggle-button:hover { background-color: rgba(255,255,255,0.1); border-radius: 50%; opacity: 1 !important; } .container-mdcm { width: 420px; max-width: 420px; background: rgba(30, 30, 30, 0.6) !important; border-radius: 16px 16px 0 0; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.1); display: flex; flex-direction: column; max-height: 80vh; overflow-y: auto; overflow-x: hidden; height: auto; } #shareDropdown { display: none; position: absolute; top: 50px; right: 100px; background-color: var(--yt-enhance-menu-bg, #252525); border-radius: 6px; padding: 10px; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 12px; z-index: 11; } #shareDropdown a { color: var(--text-custom); text-decoration: none; line-height: 2; font-size: 14px; } #shareDropdown a:hover { color: var(--primary-custom); } .header-mdcm { padding: 12px 16px; border-bottom: 1px solid rgba(255,255,255,0.1); position: sticky; top: 0; background-color: var(--yt-enhance-menu-bg, #252525); border-radius: 16px 16px 0 0; z-index: 10; display: flex; justify-content: space-between; align-items: center; } .header-mdcm h1 { font-size: 16px; margin: 0; font-weight: 600; display: flex; align-items: center; gap: 8px; } .header-mdcm i { color: var(--primary-custom) } .icons-mdcm { display: flex; gap: 4px; } .icons-mdcm i { color: var(--yt-enhance-menu-accent, var(--text-custom)); } .icon-btn-mdcm { background: rgba(255,255,255,0.1); border: none; color: var(--text-custom); width: 28px; height: 28px; border-radius: 6px; cursor: pointer; transition: all 0.3s; } .icon-btn-mdcm:hover { background: rgba(255,255,255,0.2); transform: translateY(-2px); } .icon-btn-mdcm i { color: var(--text-custom); outline: none; text-decoration: none; } .tabs-mdcm { padding: 10px 12px; margin: 10px 0; position: sticky; top: 50px; background-color: var(--yt-enhance-menu-bg, #252525); z-index: 10; display: flex; gap: 8px; -ms-overflow-style: none; padding-bottom: 8px; } .tabs-mdcm::-webkit-scrollbar { height: 0px; background-color: transparent; } .tabs-mdcm:hover::-webkit-scrollbar { height: 6px; } .tabs-mdcm::-webkit-scrollbar-thumb { background-color: rgba(255, 0, 0, 0.5); border-radius: 3px; } .tabs-mdcm::-webkit-scrollbar-track { background-color: transparent; } .tab-mdcm { padding: 6px 10px; border: none; background: rgba(255,255,255,0.05); cursor: pointer; font-size: 12px; color: var(--text-custom-secondary); border-radius: 6px; transition: all 0.3s; flex: 1; display: flex; align-items: center; gap: 6px; flex-shrink: 0; justify-content: center; white-space: nowrap; } .tab-mdcm svg { width: 14px; height: 14px; fill: currentColor; } .tab-mdcm.active { background: var(--yt-enhance-menu-accent, var(--primary-custom)) !important; color: var(--text-custom); font-weight: 500; box-shadow: 0 4px 12px rgba(255,0,0,0.2); } .tab-mdcm:hover:not(.active) { background: rgba(255,255,255,0.1); transform: translateY(-1px); } .options-mdcm { flex: 1; overflow-y: auto; padding: 0 16px 0; scrollbar-width: thin; scrollbar-color: var(--primary-custom) var(--bg-dark-custom); max-height: 300px; display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; } .options-settings-mdcm { flex: 1; overflow-y: auto; padding: 0 16px 0; scrollbar-width: thin; scrollbar-color: var(--primary-custom) var(--bg-dark-custom); max-height: 300px; display: grid; gap: 8px; } .card-items-end { display: flex; justify-content: space-between; align-items: center; width: 175px; } .radio-mdcm { width: 14px; height: 14px; accent-color: var(--primary-custom); } .color-picker-mdcm { width: 50px; height: 24px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.1); border-radius: 4px; cursor: pointer; transition: all 0.3s; } .color-picker-mdcm:hover { background: rgba(255, 255, 255, 0.2); } .options-mdcm::-webkit-scrollbar, .options-settings-mdcm::-webkit-scrollbar { width: 6px; } .options-mdcm::-webkit-scrollbar-track, .options-settings-mdcm::-webkit-scrollbar-track { background: var(--bg-dark-custom); border-radius: 3px; } .options-mdcm::-webkit-scrollbar-thumb, .options-settings-mdcm::-webkit-scrollbar-thumb { background: var(--primary-custom); border-radius: 3px; } .options-mdcm::-webkit-scrollbar-thumb:hover, .options-settings-mdcm::-webkit-scrollbar-thumb:hover { background: var(--accent-custom); } .options-mdcm::after, .options-settings-mdcm::after { content: ''; display: block; } .option-mdcm { display: grid; grid-template-columns: auto 1fr; align-items: center; margin-bottom: 0; padding: 5px; background: rgba(255,255,255,0.05); border-radius: 6px; transition: all 0.3s; border: 1px solid rgba(255,255,255,0.05); color: var(--text-custom); gap: 6px; } .option-mdcm:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.1); } .option-settings-mdcm { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0; padding: 6px; background: rgba(255, 255, 255, 0.05); border-radius: 6px; transition: all 0.3s; border: 1px solid rgba(255, 255, 255, 0.05); gap: 6px; } .option-settings-mdcm:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.1); } /* Themes Grid 2 Columns */ .themes-grid-mdcm { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 8px !important; } .themes-grid-mdcm .theme-option { margin-bottom: 0 !important; } .tab-content { display: none; } .tab-content.active { display: block; margin-bottom: 10px; } .checkbox-mdcm { width: 14px; height: 14px; accent-color: var(--yt-enhance-menu-accent, var(--primary-custom)) !important; } .yt-tools-audio-only-player { background-color: #000 !important; background-repeat: no-repeat !important; background-position: center !important; background-size: cover !important; } .yt-tools-audio-only-video { opacity: 0 !important; } label { font-size: 12px; color: var(--text-custom); } .slider-container-mdcm { background: rgba(255,255,255,0.05); padding: 10px; border-radius: 6px; } .slider-mdcm { width: 100%; height: 3px; accent-color: var(--yt-enhance-menu-accent, var(--primary-custom)) !important; margin: 10px 0; } .reset-btn-mdcm { padding: 5px 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: var(--text-custom); border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.3s; } .reset-btn-mdcm:hover { background: rgba(255,255,255,0.2); } .quality-selector-mdcm select { position: relative; padding: 3px; outline: none; border-radius: 4px; border: 1px solid rgba(255,255,255,0.2); background: var(--yt-enhance-menu-accent, var(--primary-custom)) !important; color: var(--text-custom); width: fit-content; appearance: none; cursor: pointer; font-size: 11px; } .quality-selector-mdcm { background: rgba(255,255,255,0.05); padding: 10px; border-radius: 6px; } .select-wrapper-mdcm { position: relative; display: inline-block; } .select-wrapper-mdcm select { -webkit-appearance: auto; -moz-appearance: auto; } .actions-mdcm { position: sticky; top: 0; padding: 12px 16px; backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); background: rgba(30, 30, 30, 0.6) !important; border-bottom: 1px solid rgba(255, 255, 255, 0.1); display: flex; gap: 6px; width: 390px; border-radius: 0 0 16px 16px; justify-content: space-between; align-items: center; } .action-buttons-mdcm { display: flex; gap: 6px; } .action-btn-mdcm { flex: 1; padding: 8px; border: none; border-radius: 6px; background: var(--primary-custom); color: var(--text-custom); cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 4px; box-shadow: 0 4px 12px rgba(255,0,0,0.2); } .action-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(255,0,0,0.3); } textarea.textarea-mdcm { width: 100%; height: 50px; margin-top: 10px; margin-bottom: 12px; padding: 8px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: var(--text-custom); font-size: 11px; resize: none; transition: all 0.3s; } textarea.textarea-mdcm:focus { outline: none; border-color: var(--primary-custom); background: rgba(255,255,255,0.08); } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .container-mdcm { animation: fadeIn 0.3s ease-out; } .developer-mdcm { font-size: 10px; color: var(--text-custom-secondary); } .developer-mdcm a { color: var(--primary-custom); text-decoration: none; } /* Styles for the import/export area */ #importExportArea { display: none; padding: 16px; margin: 0px; background-color: var(--yt-enhance-menu-bg, #252525); border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } #importExportArea.active { display: block; margin-top: 10px; } /* Style the textarea */ #importExportArea textarea { width: 370px; height: 20px; margin-bottom: 10px; padding: 8px; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; background-color: rgba(255, 255, 255, 0.05); color: var(--text-custom); font-size: 12px; resize: vertical; } /* Style the buttons */ #importExportArea .action-buttons-mdcm { display: flex; justify-content: space-between; gap: 10px; } #importExportArea .action-btn-mdcm { flex: 1; padding: 10px 16px; border: none; border-radius: 6px; background-color: var(--primary-custom); color: var(--text-custom); font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.3s ease; } #importExportArea .action-btn-mdcm:hover { background-color: var(--accent-custom); } .ocultarframe, .ocultarframeaudio { display: none !important; } .yt-tools-container { width: 100% !important; display: flex !important; justify-content: center !important; margin: 0 !important; } .yt-tools-inner-container { width: 100% !important; display: flex !important; flex-direction: column !important; align-items: center !important; } /* Layout & Alignment Fixes */ .yt-tools-form { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; width: 100% !important; gap: 2px !important; } .containerButtons { display: flex !important; justify-content: center !important; align-items: center !important; gap: 12px !important; width: 100% !important; flex-wrap: wrap !important; } .selectcalidades, .selectcalidadesaudio { background: rgba(30, 30, 30, 0.9) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; color: #ffffff !important; padding: 0 20px !important; height: 40px !important; line-height: 40px !important; border-radius: 10px !important; font-family: "Inter", -apple-system, sans-serif !important; font-size: 14px !important; font-weight: 600 !important; cursor: pointer !important; outline: none !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important; transition: all 0.3s ease !important; margin: 0 auto !important; display: block !important; min-width: 280px !important; max-width: 350px !important; appearance: none !important; -webkit-appearance: none !important; text-align: center !important; text-align-last: center !important; /* Support for some browsers */ position: relative; } .selectcalidades:hover, .selectcalidadesaudio:hover { border-color: #ff0000 !important; background: rgba(45, 45, 45, 0.95) !important; transform: translateY(-2px); box-shadow: 0 12px 40px rgba(255, 0, 0, 0.15) !important; } .selectcalidades option, .selectcalidadesaudio option { background: #1e1e1e !important; color: #ffffff !important; padding: 12px !important; text-align: center !important; } .formulariodescarga, .formulariodescargaaudio { width: 100% !important; margin: 0 !important; display: none !important; /* Hidden by default */ justify-content: center !important; align-items: center !important; } .containerall { width: 100% !important; display: flex !important; flex-direction: column !important; align-items: center !important; gap: 2px !important; } /* Regular YouTube specific spacing (VIP) */ ytd-watch-metadata .yt-tools-inner-container { gap: 10px !important; } ytd-watch-metadata .containerall { gap: 0px !important; padding-bottom: 0 !important; } ytd-watch-metadata .yt-tools-container { margin-bottom: -8px !important; } ytd-watch-metadata .content_collapsible_colors { margin-top: 0 !important; } ytd-watch-metadata .download-container { width: 90% !important; max-width: 450px !important; padding: 7px !important; border-radius: 12px !important; margin: 5px auto !important; display: flex !important; flex-direction: column !important; transition: all 0.3s ease; position: relative; } #yt-stats { position: fixed; top: 60px; right: 20px; background: #1a1a1a; color: white; padding: 15px; border-radius: 10px; width: 320px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); font-family: Arial, sans-serif; display: none; } #yt-stats-toggle { font-size: 12px; color: #fff; padding: 12px 20px; border-radius: 5px; cursor: pointer; } .stat-row { margin: 15px 0; } .progress { height: 6px; overflow: hidden; background: #333; border-radius: 3px; margin: 8px 0; } .progress-bar { height: 100%; transition: width 0.3s; } .total-bar { background: #44aaff !important; } .video-bar { background: #00ff88 !important; } .shorts-bar { background: #ff4444 !important; } #cinematics { position: absolute !important; width: 90vw !important; height: 100vh ; } #cinematics div { position: fixed; inset: 0px; pointer-events: none; transform: scale(1.5, 2); } #cinematics > div > div > canvas:nth-child(1), #cinematics > div > div > canvas:nth-child(2) { position: absolute !important; width: 90vw !important; height: 100vh ; } /* .html5-video-player.unstarted-mode { background-image: url('https://avatars.githubusercontent.com/u/54366580?v=4'); background-repeat: no-repeat; background-position: 50% 50%; display: flex; justify-content: center; align-items: center; } */ #yt-enhancement-panel { position: fixed; top: 60px; right: 20px; z-index: 9999; } .color-picker { width: 100%; margin: 0; padding: 0; border: none; background: none; } .slider { width: 100%; } #toggle-panel { z-index: 10000; color: white; padding: 5px; border: none; cursor: pointer; display: flex; justify-content: center; transition: all 0.5s ease; width: 43px; border-radius: 100px; } #icon-menu-settings { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 7px; font-size: 20px; color: var(--yt-spec-icon-inactive); cursor: pointer; user-select: none; filter: drop-shadow(2px 4px 6px black); } .theme-option { margin-bottom: 15px; } .theme-option label { display: flex; align-items: center; } .theme-option { position: relative; width: auto; margin-bottom: 10px; padding: 10px; border-radius: 4px; cursor: pointer; } .theme-preview { position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; border: 1px solid #000; z-index: 1; } .theme-option input[type="radio"] { position: relative; z-index: 2; margin-right: 10px; cursor: pointer; } .theme-name { position: relative; z-index: 2; font-size: 15px; color: #fff; } .theme-option label { display: flex; align-items: center; width: 100%; position: relative; z-index: 2; } .buttons-tranlate, .select-traductor { background: #000; font-size: 10px; border: none; color: #fbf4f4 !important; padding: 3px 0; margin-left: 10px; width: 70px; border-radius: 10px; } .buttons-tranlate:hover { cursor: pointer; background-color: #6b6b6b; } button.botones_div { margin: 0; padding: 0; } button.botones_div:hover { cursor: pointer; color: #6b6b6b !important; } .tab-button:hover { background-color: #ec3203 !important; color: #ffffff !important; cursor: pointer; } .traductor-container { display: inline-block; align-items: center; gap: 8px; margin-top: 4px; } #eyes { opacity: 0; position: absolute; height: 24px; left: 0; width: 24px; } /* width */ .container-mdcm ::-webkit-scrollbar { width: 4px; height: 10px; } /* Track */ .container-mdcm ::-webkit-scrollbar-track { background: #d5d5d5; } /* Handle */ .container-mdcm ::-webkit-scrollbar-thumb { background: #000; } .color-boxes { display: flex; gap: 8px; } .color-box { width: 20px; height: 20px; border: 1px solid rgb(221 221 221 / 60%); border-radius: 4px; cursor: pointer; } .color-box.selected { border: 2px solid var(--primary-custom); filter: drop-shadow(0px 1px 6px red); } .containerButtons { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 10px; } .containerButtons > button:hover { cursor: pointer; } /* Download Container Styles */ .download-container { width: 90% !important; max-width: 450px !important; padding: 16px !important; border-radius: 12px !important; margin: 10px auto !important; display: flex !important; flex-direction: column !important; transition: all 0.3s ease; position: relative; } .download-container.video { background: linear-gradient(135deg, #ff4444, #cc0000); color: white; } .download-container.audio { background: linear-gradient(135deg, #00cc44, #009933); color: white; } .download-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .download-text { font-weight: 600; font-size: 14px; } .download-quality { font-size: 12px; opacity: 0.9; } .progress-container { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; } .progress-bar { flex: 1; height: 6px; background: rgba(255, 255, 255, 0.3); border-radius: 3px; overflow: hidden; } .progress-fill { height: 100%; background: rgba(255, 255, 255, 0.8); border-radius: 3px; width: 0%; transition: width 0.3s ease; } .progress-text { font-size: 12px; font-weight: 500; min-width: 30px; } .download-footer { font-size: 10px; opacity: 0.7; text-align: center; } .download-footer a { text-decoration: none; color: #fff; } .download-container.completed { color: #fff; background: linear-gradient(135deg, #00cc44, #009933) !important; } .download-container.completed .download-text { font-weight: 700; } /* Bookmarks panel (under video buttons) */ .yt-bookmarks-panel { margin-top: 10px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; padding: 8px; } .yt-bm-empty { font-size: 12px; color: var(--text-custom-secondary); } .yt-bm-item { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: center; padding: 6px; border-radius: 8px; } .yt-bm-item:hover { background: rgba(255,255,255,0.06); } .yt-bm-go { border: none; border-radius: 6px; padding: 4px 8px; background: rgba(34,197,94,0.2); color: #fff; cursor: pointer; font-size: 12px; white-space: nowrap; } .yt-bm-label { font-size: 12px; color: var(--text-custom); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .yt-bm-del { border: none; border-radius: 6px; padding: 4px 8px; background: rgba(239,68,68,0.2); color: #fff; cursor: pointer; font-size: 12px; } /* Continue watching panel (under video buttons) */ .yt-continue-watching-panel { margin-top: 10px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; padding: 8px; } .yt-cw-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } .yt-cw-header-title { font-size: 12px; font-weight: 600; color: var(--text-custom, #fff); } .yt-cw-clear { border: none; border-radius: 6px; padding: 4px 8px; background: rgba(239,68,68,0.18); color: #fff; cursor: pointer; font-size: 12px; } .yt-cw-empty { font-size: 12px; color: var(--text-custom-secondary, #aaa); } .yt-cw-item { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center; padding: 8px; border-radius: 10px; } .yt-cw-item:hover { background: rgba(255,255,255,0.06); } .yt-cw-thumb-wrap { width: 72px; height: 40px; border-radius: 8px; overflow: hidden; background: rgba(255,255,255,0.08); flex: none; } .yt-cw-thumb { width: 100%; height: 100%; object-fit: cover; display: block; } .yt-cw-title { font-size: 12px; font-weight: 600; color: var(--text-custom, #fff); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; } .yt-cw-meta { font-size: 12px; color: var(--text-custom-secondary, #aaa); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .yt-cw-actions { display: flex; align-items: center; gap: 8px; } .yt-cw-go { border: none; border-radius: 6px; padding: 4px 8px; background: rgba(34,197,94,0.2); color: #fff; cursor: pointer; font-size: 12px; white-space: nowrap; } .yt-cw-del { border: none; border-radius: 6px; padding: 4px 8px; background: rgba(239,68,68,0.2); color: #fff; cursor: pointer; font-size: 12px; } /* Shorts channel name label (Home/feed Shorts lockups) */ html:not([data-mdcm-shorts-channel-name="1"]) .yt-tools-shorts-channel-name { display: none !important; } .yt-tools-shorts-channel-name { font-size: 12px; line-height: 1.2; color: var(--yt-spec-text-secondary, #aaa); margin-bottom: 2px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .yt-tools-shorts-stats-wrap { margin-top: 4px; font-size: 11px; line-height: 1.2; color: var(--yt-spec-text-secondary, #aaa); } .yt-tools-shorts-stats-wrap .yt-tools-shorts-stats-row { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 2px; } /* Like vs dislike bar (under likes/dislikes) */ #yt-like-dislike-bar-mdcm { height: 6px; border-radius: 999px; overflow: hidden; margin-top: 6px; background: rgba(255,255,255,0.12); max-width: 305px; } #yt-like-dislike-bar-mdcm .like { height: 100%; background: #22c55e; float: left; } #yt-like-dislike-bar-mdcm .dislike { height: 100%; background: #ef4444; float: left; } .progress-retry-btn { position: absolute; top: 8px; left: 8px; right: auto; width: 24px; height: 24px; border: none; border-radius: 50%; background: rgba(255, 255, 255, 0.2); color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; transition: all 0.3s ease; } .progress-retry-btn:hover { background: rgba(255, 255, 255, 0.3); transform: scale(1.1); } .download-again-btn { position: absolute; top: 8px; left: 8px; right: auto; width: 24px; height: 24px; border: none; border-radius: 50%; background: rgba(34, 197, 94, 0.35); color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; transition: all 0.3s ease; } .download-again-btn:hover { background: rgba(34, 197, 94, 0.5); transform: scale(1.1); } .download-container { position: relative; } .download-actions { display: flex; gap: 8px; margin-bottom: 8px; } .download-btn { flex: 1; padding: 8px 16px; border: none; border-radius: 4px; font-weight: 600; font-size: 12px; cursor: pointer; transition: all 0.3s ease; color: white; } .download-btn.video-btn { background: linear-gradient(135deg, #ff6666, #ff4444); } .download-btn.audio-btn { background: linear-gradient(135deg, #00dd55, #00cc44); } .download-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); } .download-info { padding-left: 28px !important; /* Space for buttons on the left */ } .download-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .retry-btn { padding: 8px 16px; border: none; border-radius: 4px; font-weight: 600; font-size: 12px; cursor: pointer; transition: all 0.3s ease; background: linear-gradient(135deg, #ffaa00, #ff8800); color: white; } .retry-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); } body { padding: 0; margin: 0; overflow-y: scroll; overflow-x: hidden; } .style-scope.ytd-comments { overflow-y: auto; overflow-x: hidden; height: auto; } ytd-comment-view-model[is-reply] #author-thumbnail.ytd-comment-view-model yt-img-shadow.ytd-comment-view-model, ytd-comment-view-model[is-creator-reply] #author-thumbnail.ytd-comment-view-model yt-img-shadow.ytd-comment-view-model { width: 40px; height: 40px; border-radius: 50%; } #author-thumbnail img.yt-img-shadow { border-radius: 50% !important; } #author-thumbnail.ytd-comment-view-model yt-img-shadow.ytd-comment-view-model { width: 40px; height: 40px; border-radius: 50%; overflow: visible; } ytd-item-section-renderer.ytd-watch-next-secondary-results-renderer { --ytd-item-section-item-margin: 8px; overflow-y: auto; overflow-x: hidden; height: auto; } .right-section.ytcp-header { display: flex; flex: 1; align-items: center; gap: 45px; justify-content: end; } #meta.ytd-playlist-panel-video-renderer { min-width: 0; padding: 0 8px; /* display: flexbox; */ display: flex; flex-direction: column-reverse; flex: 1; flex-basis: 0.000000001px; } .containerall { display: flex; align-items: center; justify-content: center; flex-direction: column; padding-bottom: 30px; max-width: 800px; margin: auto; } .container .botoncalidades { margin: 3px 2px; width: 24.6%; } .botoncalidades:first-child { background-color: #0af; } .botoncalidades:last-child { background-color: red; width: 100px; } .selectcalidades, .botoncalidades, .selectcalidadesaudio { width: 50%; height: 27.8px; background-color: #fff; color: #000; font-size: 25px; text-align: center; border: 1px solid black; border-radius: 10px; border: none; font-size: 20px; margin: 2px 2px; } .botoncalidades { width: 70px; height: 30px; background-color: rgb(4, 156, 22); border: 0px solid #000; color: #fff; font-size: 20px; border-radius: 10px; margin: 2px 2px; } .botoncalidades:hover, .bntcontainer:hover { cursor: pointer; } .ocultarframe, .ocultarframeaudio { display: none; } .checked_updates { cursor: pointer; } #export-config, #import-config { width: 100%; display: flex; align-items: center; justify-content: center; gap: 10px; background-color: var(--yt-enhance-menu-accent, var(--primary-custom)) !important; color: #ffffff; border: none; padding: 5px; } #export-config:hover, #import-config:hover { background-color: #ff0000; color: #ffffff; cursor: pointer; } .yt-image-avatar-download { position: absolute; bottom: -10px; right: -14px; border: none; z-index: 1000; background: transparent; filter: drop-shadow(1px 0 6px red); color: var(--ytcp-text-primary); cursor: pointer; } .custom-classic-btn { display: flex; align-items: center; justify-content: center; background-color: rgba(255,255,255,0.1); border-radius: 50%; border: none; width: 48px; height: 48px; color: var(--yt-spec-icon-inactive); font-size: 24px; margin: 0px 8px; cursor: pointer; } .custom-classic-btn:hover { background-color: rgba(255,255,255,0.2); } .background-image-container { display: flex; flex-direction: column; align-items: center; gap: 8px; margin: 10px 0; } .background-image-preview { width: 160px; height: 90px; border-radius: 10px; background-size: cover; background-position: center; border: 2px solid #444; position: relative; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: box-shadow 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; } .background-image-preview:hover .background-image-overlay { opacity: 1; } .background-image-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; background: rgba(0,0,0,0.35); font-size: 18px; opacity: 0; transition: opacity 0.2s; pointer-events: none; } .background-image-preview:hover .background-image-overlay, .background-image-preview:focus .background-image-overlay { opacity: 1; } .background-image-overlay i { font-size: 28px; margin-bottom: 4px; } .background-image-text { font-size: 13px; font-weight: 500; text-shadow: 0 1px 4px #000; } .remove-background-image { position: absolute; top: 6px; right: 6px; background: #e74c3c; color: #fff; border: none; border-radius: 50%; width: 26px; height: 26px; font-size: 18px; cursor: pointer; z-index: 2; display: none; align-items: center; justify-content: center; padding: 0; line-height: 1; box-shadow: 0 2px 8px rgba(0,0,0,0.15); transition: background 0.2s; } .remove-background-image:hover { background: #c0392b; } .background-image-preview.has-image .remove-background-image { display: flex; } ytd-feed-filter-chip-bar-renderer[not-sticky] #chips-wrapper.ytd-feed-filter-chip-bar-renderer { padding: 10px; } .text-description-download { font-size: 12px; text-align: center; margin-top: 10px; } /* === FIX: Căn giữa thanh công cụ khi bật Cinematic Mode === */ ytd-watch-flexy[cinematic-container-initialized] #primary-inner { display: flex; flex-direction: column; align-items: center; } ytd-watch-flexy[cinematic-container-initialized] .yt-tools-container { /* Đảm bảo thanh công cụ không bị quá rộng so với video */ width: 100%; max-width: var(--ytd-watch-flexy-max-player-width, 1280px); } /* === YouTube Music specific styles === */ #ytm-side-panel-wrapper { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; width: 100% !important; margin: 0 0 12px 0 !important; padding: 4px 0 !important; box-sizing: border-box !important; overflow: hidden !important; border-radius: 16px !important; } ytmusic-player-page #side-panel { margin-left: 16px !important; margin-bottom: 24px !important; padding: 0 !important; background: transparent !important; border: none !important; box-shadow: none !important; } .ytm-side-panel-divider { width: 92%; height: 1px; background: rgba(255, 255, 255, 0.1); margin: 4px 0; } /* Blur Mode (Standard with subtle blur) */ body.ytm-style-blur #ytm-side-panel-wrapper, body.ytm-style-blur ytmusic-player-page #side-panel ytmusic-tab-renderer { background: rgba(20, 20, 20, 0.6) !important; backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4) !important; border-radius: 16px !important; } /* Liquid Mode (Fixed Apple Style) */ body.ytm-style-liquid #ytm-side-panel-wrapper, body.ytm-style-liquid ytmusic-player-page #side-panel ytmusic-tab-renderer { background: rgba(25, 25, 25, 0.45) !important; backdrop-filter: blur(24px) saturate(180%) !important; -webkit-backdrop-filter: blur(24px) saturate(180%) !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-top: 1px solid rgba(255, 255, 255, 0.25) !important; box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.1) !important; border-radius: 16px !important; } /* Transparent Mode */ body.ytm-style-transparent #ytm-side-panel-wrapper, body.ytm-style-transparent ytmusic-player-page #side-panel ytmusic-tab-renderer { background: transparent !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; border: none !important; box-shadow: none !important; } /* YouTube watch playlist panel: reuse the same 3 style choices as YTM. */ body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist { background: rgba(20, 20, 20, 0.62) !important; backdrop-filter: blur(20px) saturate(140%) !important; -webkit-backdrop-filter: blur(20px) saturate(140%) !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.38) !important; border-radius: 16px !important; overflow: hidden !important; } body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist { background: rgba(25, 25, 25, 0.45) !important; backdrop-filter: blur(24px) saturate(180%) !important; -webkit-backdrop-filter: blur(24px) saturate(180%) !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-top-color: rgba(255, 255, 255, 0.25) !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.1) !important; border-radius: 16px !important; overflow: hidden !important; } body.ytm-style-transparent ytd-watch-flexy ytd-playlist-panel-renderer#playlist { background: transparent !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; border: none !important; box-shadow: none !important; } body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist > #container, body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist > #container, body.ytm-style-transparent ytd-watch-flexy ytd-playlist-panel-renderer#playlist > #container, body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist #items, body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist #items, body.ytm-style-transparent ytd-watch-flexy ytd-playlist-panel-renderer#playlist #items { background: transparent !important; } body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist .header, body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist .header { background: rgba(255, 255, 255, 0.04) !important; border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important; } body.ytm-style-transparent ytd-watch-flexy ytd-playlist-panel-renderer#playlist .header { background: transparent !important; border-bottom: none !important; } body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist ytd-playlist-panel-video-renderer, body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist ytd-playlist-panel-video-renderer { background: transparent !important; } body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist ytd-playlist-panel-video-renderer:hover #container, body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist ytd-playlist-panel-video-renderer:hover #container { background: rgba(255, 255, 255, 0.08) !important; } body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist ytd-thumbnail, body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist ytd-thumbnail { border-radius: 8px !important; overflow: hidden !important; } body.ytm-style-blur ytd-watch-flexy ytd-playlist-panel-renderer#playlist #meta, body.ytm-style-liquid ytd-watch-flexy ytd-playlist-panel-renderer#playlist #meta, body.ytm-style-transparent ytd-watch-flexy ytd-playlist-panel-renderer#playlist #meta { min-width: 0 !important; } /* Inner Container Resets */ html[dark] .yt-tools-container, ytmusic-app .yt-tools-container, ytmusic-app #side-panel ytmusic-tab-renderer, ytmusic-app ytmusic-search-box, ytmusic-app #side-panel > .tab-header-container, tp-yt-paper-tabs.tab-header-container, #tabsContainer.tp-yt-paper-tabs, tp-yt-paper-tab.tab-header, .tab-content.tp-yt-paper-tab { background: transparent !important; background-color: transparent !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; border: none !important; box-shadow: none !important; margin: 0 !important; } .yt-tools-container { width: 100% !important; display: flex !important; align-items: center !important; justify-content: center !important; padding: 0 !important; } .tab-header-container { width: 100% !important; display: flex !important; align-items: center !important; justify-content: center !important; padding: 4px 0 !important; } /* Fix YTM side-panel queue layout */ ytmusic-app #side-panel { padding: 8px 12px; box-sizing: border-box; } ytmusic-app #side-panel ytmusic-queue-header-renderer .container-name { padding: 0 8px; } ytmusic-app #side-panel ytmusic-player-queue-item .song-info { padding-right: 4px; } ytmusic-app #side-panel ytmusic-tab-renderer { border-radius: 16px !important; overflow-x: hidden !important; overflow-y: auto !important; width: 100% !important; box-sizing: border-box !important; padding: 12px 0 12px 12px !important; margin-bottom: 12px !important; } /* Make the YTM Search Box premium and glassmorphic (Feature requested by user) */ ytmusic-app ytmusic-search-box { border-radius: 16px !important; } html[dark] ytmusic-app ytmusic-search-box #input-box, ytmusic-app ytmusic-search-box #input-box { background: transparent !important; } /* Tab headers — rounded corners & spacing */ ytmusic-app #side-panel > .tab-header-container { border-radius: 16px !important; overflow: hidden !important; padding: 4px 8px; margin: 0 0 8px 0 !important; width: 100% !important; box-sizing: border-box !important; } /* Compact button layout for narrow YTM side panel */ ytmusic-app .containerButtons { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 4px; } ytmusic-app .botones_div { background: rgba(255, 255, 255, 0.08) !important; border: 1px solid rgba(255, 255, 255, 0.12) !important; color: #fff !important; border-radius: 6px !important; padding: 5px 7px !important; transition: all 0.2s ease !important; line-height: 1 !important; } ytmusic-app .botones_div svg { width: 18px !important; height: 18px !important; } ytmusic-app .botones_div:hover { background: rgba(255, 0, 0, 0.25) !important; border-color: rgba(255, 0, 0, 0.4) !important; transform: translateY(-1px); } ytmusic-player-page #side-panel select, ytmusic-player-page #side-panel select option { background: #282828 !important; color: #fff !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; } ytmusic-app .selectcalidades, ytmusic-app .selectcalidadesaudio { background: rgba(255, 255, 255, 0.1) !important; color: #fff !important; border: 1px solid rgba(255, 255, 255, 0.2) !important; border-radius: 6px !important; padding: 8px 8px 6px !important; /* Asymmetric padding for vertical centering */ font-size: 12px !important; width: 100% !important; cursor: pointer !important; outline: none !important; height: 32px !important; line-height: normal !important; } ytmusic-app .download-container { width: 100% !important; background: rgba(0, 0, 0, 0.3) !important; border-radius: 6px !important; position: relative !important; /* Fix absolute positioned buttons (Retry/Again) */ padding: 10px !important; box-sizing: border-box !important; margin-bottom: 0 !important; } ytmusic-app .content_collapsible_colors { margin-top: 8px !important; } .ytm-side-panel-divider { margin: 0 !important; border-bottom: 1px solid rgba(255, 255, 255, 0.1); height: 0; width: 100%; } ytmusic-app .containerall { padding-bottom: 10px !important; } ytmusic-app #toggle-button { display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 8px; margin-right: 4px; } ytmusic-app #icon-menu-settings { color: #fff; font-size: 20px; transition: transform 0.3s ease; } ytmusic-app #icon-menu-settings:hover { transform: rotate(90deg); color: #ff4444; } `); function buildYTMToolbar() { const main = document.createElement('main'); main.className = 'yt-tools-container'; const container = document.createElement('div'); container.className = 'yt-tools-inner-container'; const form = document.createElement('form'); form.className = 'yt-tools-form'; const btnsDiv = document.createElement('div'); btnsDiv.className = 'containerButtons'; // Thumbnail (Image download) btnsDiv.appendChild(makeToolBtn('Image video', 'imagen', '', [ 'M0 0h24v24H0z', 'M15 8h.01', 'M12.5 21h-6.5a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v6.5', 'M3 16l5 -5c.928 -.893 2.072 -.893 3 0l4 4', 'M14 14l1 -1c.653 -.629 1.413 -.815 2.13 -.559', 'M19 16v6', 'M22 19l-3 3l-3 -3' ])); // Repeat if (!isYTMusic) { btnsDiv.appendChild(makeToolBtn('Repeat video', 'repeatvideo', '', [ 'M0 0h24v24H0z', 'M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3', 'M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3' ])); } // Download MP4 btnsDiv.appendChild(makeToolBtn('MP4', null, 'btn1', [ 'M0 0h24v24H0z', 'M14 3v4a1 1 0 0 0 1 1h4', 'M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z', 'M12 17v-6', 'M9.5 14.5l2.5 2.5l2.5 -2.5' ])); // Download MP3 btnsDiv.appendChild(makeToolBtn('MP3', null, 'btn2', [ 'M0 0h24v24H0z', 'M14 3v4a1 1 0 0 0 1 1h4', 'M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z', 'M11 16m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0', 'M12 16l0 -5l2 1' ])); // Close btnsDiv.appendChild(makeToolBtn('Close', null, 'btn3', [ 'M0 0h24v24H0z', 'M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0', 'M10 10l4 4m0 -4l-4 4' ])); if (!isYTMusic) { // Add Bookmark btnsDiv.appendChild(makeToolBtn('Add bookmark', 'yt-bookmark-add', '', [ 'M0 0h24v24H0z', 'M7 4h10a2 2 0 0 1 2 2v14l-7 -4l-7 4v-14a2 2 0 0 1 2 -2z', 'M12 7v6', 'M9 10h6' ])); // Show Bookmarks btnsDiv.appendChild(makeToolBtn('Show bookmarks', 'yt-bookmark-toggle', '', [ 'M0 0h24v24H0z', 'M9 6h11', 'M9 12h11', 'M9 18h11', 'M5 6h.01', 'M5 12h.01', 'M5 18h.01' ])); // History (Continue Watching) btnsDiv.appendChild(makeToolBtn('History', 'yt-cw-history-toggle', '', [ 'M0 0h24v24H0z', 'M12 8v4l3 3', 'M3 12a9 9 0 1 0 3 -6.7', 'M3 4v4h4' ])); } // Picture-in-Picture btnsDiv.appendChild(makeToolBtn('Picture to picture', null, 'video_picture_to_picture', [ 'M0 0h24v24H0z', 'M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v4', 'M14 14m0 1a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z' ])); // Screenshot btnsDiv.appendChild(makeToolBtn('Screenshot video', null, 'screenshot_video', [ 'M0 0h24v24H0z', 'M15 8h.01', 'M6 13l2.644 -2.644a1.21 1.21 0 0 1 1.712 0l3.644 3.644', 'M13 13l1.644 -1.644a1.21 1.21 0 0 1 1.712 0l1.644 1.644', 'M4 8v-2a2 2 0 0 1 2 -2h2', 'M4 16v2a2 2 0 0 0 2 2h2', 'M16 4h2a2 2 0 0 1 2 2v2', 'M16 20h2a2 2 0 0 0 2 -2v-2' ])); form.appendChild(btnsDiv); if (!isYTMusic) { const bookmarksPanel = document.createElement('div'); bookmarksPanel.id = 'yt-bookmarks-panel'; bookmarksPanel.className = 'yt-bookmarks-panel'; bookmarksPanel.style.display = 'none'; form.appendChild(bookmarksPanel); const historyPanel = document.createElement('div'); historyPanel.id = 'yt-continue-watching-panel'; historyPanel.className = 'yt-continue-watching-panel'; historyPanel.style.display = 'none'; form.appendChild(historyPanel); } // Download video quality select const videoForm = document.createElement('form'); videoForm.className = 'formulariodescarga ocultarframe'; const videoSelectDiv = document.createElement('div'); videoSelectDiv.className = 'containerall'; const videoSelect = document.createElement('select'); videoSelect.className = 'selectcalidades ocultarframe'; videoSelect.required = true; [['', 'Video Quality', true], ['144', '144p MP4'], ['240', '240p MP4'], ['360', '360p MP4'], ['480', '480p MP4'], ['720', '720p HD MP4 Default'], ['1080', '1080p FULL HD MP4'], ['1440', '1440p 2K WEBM'], ['4k', '2160p 4K WEBM'], ['8k', '4320p 8K WEBM'] ].forEach(([val, text, dis]) => { const opt = document.createElement('option'); opt.value = val; opt.textContent = text; if (dis) { opt.selected = true; opt.disabled = true; } videoSelect.appendChild(opt); }); videoSelectDiv.appendChild(videoSelect); // Download video container const dlVideoContainer = document.createElement('div'); dlVideoContainer.id = 'descargando'; dlVideoContainer.className = 'download-container ocultarframe'; const dlInfo = document.createElement('div'); dlInfo.className = 'download-info'; const dlText = document.createElement('span'); dlText.className = 'download-text'; dlText.textContent = 'Download Video And Please Wait...'; const dlQuality = document.createElement('span'); dlQuality.className = 'download-quality'; dlInfo.appendChild(dlText); dlInfo.appendChild(dlQuality); const dlActions = document.createElement('div'); dlActions.className = 'download-actions'; const dlBtn = document.createElement('button'); dlBtn.className = 'download-btn video-btn'; dlBtn.textContent = 'Download'; const retryBtn = document.createElement('button'); retryBtn.className = 'retry-btn'; retryBtn.style.display = 'none'; retryBtn.textContent = 'Retry'; dlActions.appendChild(dlBtn); dlActions.appendChild(retryBtn); const progressC = document.createElement('div'); progressC.className = 'progress-container'; progressC.style.display = 'none'; const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; const progressFill = document.createElement('div'); progressFill.className = 'progress-fill'; progressBar.appendChild(progressFill); const progressText = document.createElement('span'); progressText.className = 'progress-text'; progressText.textContent = '0%'; progressC.appendChild(progressBar); progressC.appendChild(progressText); // progress-retry-btn and download-again-btn (required by startDownloadVideoOrAudio) const progRetryBtn = document.createElement('button'); progRetryBtn.className = 'progress-retry-btn'; progRetryBtn.title = 'Retry'; progRetryBtn.style.display = 'none'; progRetryBtn.textContent = '↻'; const dlAgainBtn = document.createElement('button'); dlAgainBtn.className = 'download-again-btn'; dlAgainBtn.title = 'Download again'; dlAgainBtn.style.display = 'none'; dlAgainBtn.textContent = '⬇'; dlVideoContainer.appendChild(progRetryBtn); dlVideoContainer.appendChild(dlAgainBtn); dlVideoContainer.appendChild(dlInfo); dlVideoContainer.appendChild(dlActions); dlVideoContainer.appendChild(progressC); videoSelectDiv.appendChild(dlVideoContainer); videoForm.appendChild(videoSelectDiv); // Download audio quality select const audioForm = document.createElement('form'); audioForm.className = 'formulariodescargaaudio ocultarframe'; const audioSelectDiv = document.createElement('div'); audioSelectDiv.className = 'containerall'; const audioSelect = document.createElement('select'); audioSelect.className = 'selectcalidadesaudio ocultarframeaudio'; audioSelect.required = true; [['', 'Audio Quality', true], ['flac', 'Audio FLAC UHQ'], ['wav', 'Audio WAV UHQ'], ['webm', 'Audio WEBM UHQ'], ['mp3', 'Audio MP3 Default'], ['m4a', 'Audio M4A'], ['aac', 'Audio AAC'], ['opus', 'Audio OPUS'], ['ogg', 'Audio OGG'] ].forEach(([val, text, dis]) => { const opt = document.createElement('option'); opt.value = val; opt.textContent = text; if (dis) { opt.selected = true; opt.disabled = true; } audioSelect.appendChild(opt); }); audioSelectDiv.appendChild(audioSelect); // Download audio container const dlAudioContainer = document.createElement('div'); dlAudioContainer.id = 'descargandomp3'; dlAudioContainer.className = 'download-container ocultarframeaudio'; const dlInfoA = document.createElement('div'); dlInfoA.className = 'download-info'; const dlTextA = document.createElement('span'); dlTextA.className = 'download-text'; dlTextA.textContent = 'Download Audio And Please Wait...'; const dlQualityA = document.createElement('span'); dlQualityA.className = 'download-quality'; dlInfoA.appendChild(dlTextA); dlInfoA.appendChild(dlQualityA); const dlActionsA = document.createElement('div'); dlActionsA.className = 'download-actions'; const dlBtnA = document.createElement('button'); dlBtnA.className = 'download-btn audio-btn'; dlBtnA.textContent = 'Download'; const retryBtnA = document.createElement('button'); retryBtnA.className = 'retry-btn'; retryBtnA.style.display = 'none'; retryBtnA.textContent = 'Retry'; dlActionsA.appendChild(dlBtnA); dlActionsA.appendChild(retryBtnA); const progressCA = document.createElement('div'); progressCA.className = 'progress-container'; progressCA.style.display = 'none'; const progressBarA = document.createElement('div'); progressBarA.className = 'progress-bar'; const progressFillA = document.createElement('div'); progressFillA.className = 'progress-fill'; progressBarA.appendChild(progressFillA); const progressTextA = document.createElement('span'); progressTextA.className = 'progress-text'; progressTextA.textContent = '0%'; progressCA.appendChild(progressBarA); progressCA.appendChild(progressTextA); // progress-retry-btn and download-again-btn for audio const progRetryBtnA = document.createElement('button'); progRetryBtnA.className = 'progress-retry-btn'; progRetryBtnA.title = 'Retry'; progRetryBtnA.style.display = 'none'; progRetryBtnA.textContent = '↻'; const dlAgainBtnA = document.createElement('button'); dlAgainBtnA.className = 'download-again-btn'; dlAgainBtnA.title = 'Download again'; dlAgainBtnA.style.display = 'none'; dlAgainBtnA.textContent = '⬇'; dlAudioContainer.appendChild(progRetryBtnA); dlAudioContainer.appendChild(dlAgainBtnA); dlAudioContainer.appendChild(dlInfoA); dlAudioContainer.appendChild(dlActionsA); dlAudioContainer.appendChild(progressCA); audioSelectDiv.appendChild(dlAudioContainer); audioForm.appendChild(audioSelectDiv); const collapsible = document.createElement('div'); collapsible.className = 'content_collapsible_colors'; collapsible.style.marginTop = '2px'; collapsible.appendChild(videoForm); collapsible.appendChild(audioForm); container.appendChild(form); container.appendChild(collapsible); main.appendChild(container); return main; } function renderizarButtons() { if (isYTMusic) { // YouTube Music: inject ABOVE the tab header container (Tiếp theo/Lời nhạc/Liên quan) const sidePanel = document.querySelector('#player-page #side-panel'); const tabHeaders = sidePanel && sidePanel.querySelector('.tab-header-container'); const addButton = tabHeaders || document.querySelector('#tab-renderer'); // YTM loads lazily - if element not found, retry if (!addButton && validoBotones) { if (!renderizarButtons._ytmRetries) renderizarButtons._ytmRetries = 0; if (renderizarButtons._ytmRetries < 30) { renderizarButtons._ytmRetries++; setTimeout(renderizarButtons, 500); } return; } renderizarButtons._ytmRetries = 0; if (addButton && validoBotones) { validoBotones = false; const sidePanel = document.querySelector('ytmusic-player-page #side-panel'); if (sidePanel) { // CREATE A COMMON WRAPPER FOR TOOLS AND TAB HEADERS (Top Box) let sideWrapper = $id('ytm-side-panel-wrapper'); if (!sideWrapper) { sideWrapper = document.createElement('div'); sideWrapper.id = 'ytm-side-panel-wrapper'; sidePanel.insertBefore(sideWrapper, addButton); } const toolbar = buildYTMToolbar(); sideWrapper.appendChild(toolbar); // ADD A LINE SEPARATOR const line = document.createElement('div'); line.className = 'ytm-side-panel-divider'; sideWrapper.appendChild(line); // MOVE THE TAB HEADER INTO THE TOP BOX sideWrapper.appendChild(addButton); } } } else { // Regular YouTube const addButton = document.querySelector('.style-scope .ytd-watch-metadata'); const addButton2 = document.querySelector('#contents'); if (addButton && validoBotones) { const isVisible = addButton.offsetParent !== null; if (isVisible || addButton2) { validoBotones = false; const toolbar = buildYTMToolbar(); // Insert before metadata to be above descriptions/comments addButton.parentNode.insertBefore(toolbar, addButton); } } } const formulariodescarga = $e('.formulariodescarga'); const formulariodescargaaudio = $e('.formulariodescargaaudio'); const btn1mp4 = $e('.btn1'); const btn2mp3 = $e('.btn2'); const btn3cancel = $e('.btn3'); const selectcalidades = $e('.selectcalidades'); const selectcalidadesaudio = $e('.selectcalidadesaudio'); [formulariodescarga, formulariodescargaaudio].forEach(form => { if (!form) return; if (form.dataset.ytToolsPreventDefault === '1') return; form.addEventListener('click', e => e.preventDefault()); form.dataset.ytToolsPreventDefault = '1'; }); if (selectcalidades && selectcalidades.dataset.ytToolsBound !== '1') { selectcalidades.dataset.ytToolsBound = '1'; selectcalidades.addEventListener('change', e => { const quality = e.target.value; if (!quality) return; // Don't proceed if no quality selected const downloadContainer = $id('descargando'); const downloadText = downloadContainer.querySelector('.download-text'); const downloadQuality = downloadContainer.querySelector('.download-quality'); const downloadBtn = downloadContainer.querySelector('.download-btn'); const retryBtn = downloadContainer.querySelector('.retry-btn'); const progressContainer = downloadContainer.querySelector('.progress-container'); // Update UI downloadContainer.classList.add('video'); downloadContainer.classList.remove('ocultarframe'); downloadText.textContent = `Download ${quality.toUpperCase()} And Please Wait...`; downloadQuality.textContent = `${quality}p`; // Show download button, hide progress downloadBtn.style.display = 'block'; retryBtn.style.display = 'none'; progressContainer.style.display = 'none'; // Store quality for later use downloadContainer.dataset.quality = quality; downloadContainer.dataset.type = 'video'; }); } if (selectcalidadesaudio && selectcalidadesaudio.dataset.ytToolsBound !== '1') { selectcalidadesaudio.dataset.ytToolsBound = '1'; selectcalidadesaudio.addEventListener('change', e => { const format = e.target.value; if (!format) return; // Don't proceed if no format selected const downloadContainer = $id('descargandomp3'); const downloadText = downloadContainer.querySelector('.download-text'); const downloadQuality = downloadContainer.querySelector('.download-quality'); const downloadBtn = downloadContainer.querySelector('.download-btn'); const retryBtn = downloadContainer.querySelector('.retry-btn'); const progressContainer = downloadContainer.querySelector('.progress-container'); // Update UI downloadContainer.classList.add('audio'); downloadContainer.classList.remove('ocultarframeaudio'); downloadText.textContent = `Download ${format.toUpperCase()} And Please Wait...`; downloadQuality.textContent = format.toUpperCase(); // Show download button, hide progress downloadBtn.style.display = 'block'; retryBtn.style.display = 'none'; progressContainer.style.display = 'none'; // Store format for later use downloadContainer.dataset.quality = format; downloadContainer.dataset.type = 'audio'; }); } if (btn3cancel && btn3cancel.dataset.ytToolsBound !== '1') { btn3cancel.dataset.ytToolsBound = '1'; btn3cancel.addEventListener('click', () => { // Hide all selects selectcalidades?.classList.add('ocultarframe'); selectcalidadesaudio?.classList.add('ocultarframeaudio'); // Hide all download containers const videoContainer = $id('descargando'); const audioContainer = $id('descargandomp3'); if (videoContainer) { videoContainer.classList.add('ocultarframe'); videoContainer.classList.remove('video', 'audio', 'completed'); videoContainer.removeAttribute('data-quality'); videoContainer.removeAttribute('data-type'); videoContainer.removeAttribute('data-downloading'); videoContainer.removeAttribute('data-url-opened'); videoContainer.removeAttribute('data-last-download-url'); videoContainer.querySelector?.('.download-again-btn')?.style && (videoContainer.querySelector('.download-again-btn').style.display = 'none'); } if (audioContainer) { audioContainer.classList.add('ocultarframeaudio'); audioContainer.classList.remove('video', 'audio', 'completed'); audioContainer.removeAttribute('data-quality'); audioContainer.removeAttribute('data-type'); audioContainer.removeAttribute('data-downloading'); audioContainer.removeAttribute('data-url-opened'); audioContainer.removeAttribute('data-last-download-url'); audioContainer.querySelector?.('.download-again-btn')?.style && (audioContainer.querySelector('.download-again-btn').style.display = 'none'); } // Hide all forms if (formulariodescarga) formulariodescarga.style.setProperty('display', 'none', 'important'); if (formulariodescargaaudio) formulariodescargaaudio.style.setProperty('display', 'none', 'important'); // Reset forms formulariodescarga?.reset(); formulariodescargaaudio?.reset(); }); } // Add event listeners for download buttons (only once) if (!__ytToolsRuntime.downloadClickHandlerInitialized) { __ytToolsRuntime.downloadClickHandlerInitialized = true; document.addEventListener('click', (e) => { const target = e.target; if (!(target instanceof Element)) return; const clicked = target.closest('.download-btn') || target.closest('.retry-btn') || target.closest('.progress-retry-btn') || target.closest('.download-again-btn'); if (!clicked) return; const container = clicked.closest('.download-container'); if (!container) return; const quality = container.dataset.quality; const type = container.dataset.type; // download-again just re-opens the last URL (no restart) if (clicked.classList.contains('download-again-btn')) { const url = container.dataset.lastDownloadUrl; if (url) window.open(url); return; } if (!quality || !type) return; if (clicked.classList.contains('progress-retry-btn')) { container.dataset.downloading = 'false'; container.dataset.urlOpened = 'false'; container.dataset.lastDownloadUrl = ''; container.querySelector?.('.download-again-btn')?.style && (container.querySelector('.download-again-btn').style.display = 'none'); } startDownloadVideoOrAudio(quality, container); }); } if (btn1mp4 && btn1mp4.dataset.ytToolsBound !== '1') { btn1mp4.dataset.ytToolsBound = '1'; btn1mp4.addEventListener('click', () => { // Show video select, hide audio select selectcalidades?.classList.remove('ocultarframe'); selectcalidadesaudio?.classList.add('ocultarframeaudio'); // Hide all download containers const videoContainer = $id('descargando'); const audioContainer = $id('descargandomp3'); if (videoContainer) { videoContainer.classList.add('ocultarframe'); videoContainer.classList.remove('video', 'audio', 'completed'); videoContainer.removeAttribute('data-quality'); videoContainer.removeAttribute('data-type'); videoContainer.removeAttribute('data-downloading'); videoContainer.removeAttribute('data-url-opened'); } if (audioContainer) { audioContainer.classList.add('ocultarframeaudio'); audioContainer.classList.remove('video', 'audio', 'completed'); audioContainer.removeAttribute('data-quality'); audioContainer.removeAttribute('data-type'); audioContainer.removeAttribute('data-downloading'); audioContainer.removeAttribute('data-url-opened'); } // Show video form, hide audio form if (formulariodescarga) formulariodescarga.style.setProperty('display', 'flex', 'important'); if (formulariodescargaaudio) formulariodescargaaudio.style.setProperty('display', 'none', 'important'); // Reset forms formulariodescarga?.reset(); formulariodescargaaudio?.reset(); // On YTM: auto-select 720p and show download button immediately if (isYTMusic && selectcalidades) { selectcalidades.value = '720'; selectcalidades.dispatchEvent(new Event('change', { bubbles: true })); } }); } if (btn2mp3 && btn2mp3.dataset.ytToolsBound !== '1') { btn2mp3.dataset.ytToolsBound = '1'; btn2mp3.addEventListener('click', () => { // Show audio select, hide video select selectcalidadesaudio?.classList.remove('ocultarframeaudio'); selectcalidades?.classList.add('ocultarframe'); // Hide all download containers const videoContainer = $id('descargando'); const audioContainer = $id('descargandomp3'); if (videoContainer) { videoContainer.classList.add('ocultarframe'); videoContainer.classList.remove('video', 'audio', 'completed'); videoContainer.removeAttribute('data-quality'); videoContainer.removeAttribute('data-type'); videoContainer.removeAttribute('data-downloading'); videoContainer.removeAttribute('data-url-opened'); } if (audioContainer) { audioContainer.classList.add('ocultarframeaudio'); audioContainer.classList.remove('video', 'audio', 'completed'); audioContainer.removeAttribute('data-quality'); audioContainer.removeAttribute('data-type'); audioContainer.removeAttribute('data-downloading'); audioContainer.removeAttribute('data-url-opened'); } // Show audio form, hide video form if (formulariodescargaaudio) formulariodescargaaudio.style.setProperty('display', 'flex', 'important'); if (formulariodescarga) formulariodescarga.style.setProperty('display', 'none', 'important'); // Reset forms formulariodescargaaudio?.reset(); formulariodescarga?.reset(); // On YTM: auto-select MP3 and show download button immediately if (isYTMusic && selectcalidadesaudio) { selectcalidadesaudio.value = 'mp3'; selectcalidadesaudio.dispatchEvent(new Event('change', { bubbles: true })); } }); } // Invertir contenido const btnImagen = $e('#imagen'); // valido modo oscuro y venta de video // Repeat video button let countRepeat = 0; // count const repeat = $e('#repeatvideo'); // Repeat button const videoFull = isYTMusic ? $e('video') : $e('#movie_player > div.html5-video-container > video'); if (repeat != undefined) { repeat.onclick = () => { if ( (isYTMusic ? videoFull : $e('#cinematics > div')) != undefined || videoFull != undefined ) { countRepeat += 1; switch (countRepeat) { case 1: const videoEl = isYTMusic ? $e('video') : document .querySelector('#movie_player > div.html5-video-container > video'); videoEl?.setAttribute('loop', 'true'); if (isYTMusic) { // On YTM, replace SVG icon using DOM API const newSvg = createSvgIcon([ 'M0 0h24v24H0z', 'M4 12v-3c0 -1.336 .873 -2.468 2.08 -2.856m3.92 -.144h10m-3 -3l3 3l-3 3', 'M20 12v3a3 3 0 0 1 -.133 .886m-1.99 1.984a3 3 0 0 1 -.877 .13h-13m3 3l-3 -3l3 -3', 'M3 3l18 18' ]); repeat.replaceChildren(newSvg); } else { const imarepeat = $e('.icon-tabler-repeat'); if (imarepeat) imarepeat.innerHTML = safeHTML(` <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-repeat-off" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 12v-3c0 -1.336 .873 -2.468 2.08 -2.856m3.92 -.144h10m-3 -3l3 3l-3 3"></path> <path d="M20 12v3a3 3 0 0 1 -.133 .886m-1.99 1.984a3 3 0 0 1 -.877 .13h-13m3 3l-3 -3l3 -3"></path> <path d="M3 3l18 18"></path> </svg> `); } break; case 2: countRepeat = 0; const videoEl2 = isYTMusic ? $e('video') : document .querySelector('#movie_player > div.html5-video-container > video'); videoEl2?.removeAttribute('loop'); if (isYTMusic) { const newSvg2 = createSvgIcon([ 'M0 0h24v24H0z', 'M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3', 'M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3' ]); repeat.replaceChildren(newSvg2); } else { const imarepeat2 = $e('.icon-tabler-repeat'); if (imarepeat2) imarepeat2.innerHTML = safeHTML(` <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-repeat" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3"></path> <path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3"></path> </svg>`); } break; } } } } // Background transparent const cinematica = $e('#cinematics > div'); if (cinematica != undefined) { cinematica.style.cssText = 'position: fixed; inset: 0px; pointer-events: none; transform: scale(1.5, 2)'; } if (btnImagen != undefined) { btnImagen.onclick = () => { if ( $e('#cinematics > div') != undefined || videoFull != undefined ) { const parametrosURL = new URLSearchParams(window.location.search); let enlace = parametrosURL.get('v'); const imageUrl = `https://i.ytimg.com/vi/${enlace}/maxresdefault.jpg`; fetch(imageUrl) .then((response) => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } return response.blob(); }) .then((blob) => { const imageSizeKB = blob.size / 1024; if (imageSizeKB >= 20) { window.open( `https://i.ytimg.com/vi/${enlace}/maxresdefault.jpg`, 'popUpWindow', 'height=500,width=400,left=100,top=100,resizable=yes,scrollbars=yes,toolbar=yes,menubar=no,location=no,directories=no, status=yes' ); const imageUrlObject = URL.createObjectURL(blob); const enlaceDescarga = $cl('a'); enlaceDescarga.href = imageUrlObject; const titleVideo = isYTMusic ? ($e('ytmusic-player-bar .title')?.textContent?.trim() || 'YouTube Music') : ($e('h1.style-scope.ytd-watch-metadata')?.innerText || 'video'); enlaceDescarga.download = `${titleVideo}_maxresdefault.jpg`; enlaceDescarga.click(); URL.revokeObjectURL(imageUrlObject); } else { console.log( 'La imagen no excede los 20 KB. No se descargará.' ); } }) .catch((error) => { alert('No found image'); console.error('Error al obtener la imagen:', error); }); } }; } // [REMOVED] Duplicate background image handler — handled at end of script (line ~6648+). const viewPictureToPicture = $e( '.video_picture_to_picture' ); if (viewPictureToPicture != undefined) { viewPictureToPicture.onclick = () => { const video = $e('video'); if ('pictureInPictureEnabled' in document) { if (!document.pictureInPictureElement) { video .requestPictureInPicture() .then(() => { }) .catch((error) => { console.error( 'Error al activar el modo Picture-in-Picture:', error ); }); } else { // video picture } } else { alert('Picture-in-Picture not supported'); } }; } const screenShotVideo = $e('.screenshot_video'); if (screenShotVideo != undefined) { screenShotVideo.onclick = () => { const video = $e('video'); const canvas = $cl('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const context = canvas.getContext('2d'); context.drawImage(video, 0, 0, canvas.width, canvas.height); const imagenURL = canvas.toDataURL('image/png'); const enlaceDescarga = $cl('a'); enlaceDescarga.href = imagenURL; const titleVideo = isYTMusic ? ($e('ytmusic-player-bar .title')?.textContent?.trim() || 'YouTube Music') : ($e('h1.style-scope.ytd-watch-metadata')?.innerText || 'video'); enlaceDescarga.download = `${video.currentTime.toFixed( 0 )}s_${titleVideo}.png`; enlaceDescarga.click(); }; } else { const containerButtons = $e('.containerButtons'); if (containerButtons != undefined) { containerButtons.innerHTML = safeHTML(''); } } // [REMOVED] clearInterval(renderizarButtons) — was passing a function, not an interval ID. } function hideCanvas() { const canvas = $id('wave-visualizer-canvas'); if (canvas) { canvas.style.opacity = '0'; if (controlPanel) { controlPanel.style.opacity = '0'; } } } function showCanvas() { const canvas = $id('wave-visualizer-canvas'); if (audioCtx && audioCtx.state === 'suspended') { audioCtx.resume(); } if (canvas) { canvas.style.opacity = '1'; if (controlPanel) controlPanel.style.opacity = '1'; } } async function startDownloadVideoOrAudio(format, container) { const videoURL = window.location.href; // Notify('info', 'Starting download...'); // Check if already downloading if (container.dataset.downloading === 'true') { return; } // Stop any previous poller (avoid leaks on retry) try { if (container.__ytDownloadPoll) { clearInterval(container.__ytDownloadPoll); container.__ytDownloadPoll = null; } } catch (e) { } // Get UI elements from the container const downloadBtn = container.querySelector('.download-btn'); const retryBtn = container.querySelector('.retry-btn'); const progressRetryBtn = container.querySelector('.progress-retry-btn'); const downloadAgainBtn = container.querySelector('.download-again-btn'); const progressContainer = container.querySelector('.progress-container'); const progressFill = container.querySelector('.progress-fill'); const progressText = container.querySelector('.progress-text'); const downloadText = container.querySelector('.download-text'); // Set downloading flag container.dataset.downloading = 'true'; container.dataset.urlOpened = 'false'; container.dataset.lastDownloadUrl = ''; // Update UI to show progress downloadBtn.style.display = 'none'; retryBtn.style.display = 'none'; progressRetryBtn.style.display = 'block'; if (downloadAgainBtn) downloadAgainBtn.style.display = 'none'; progressContainer.style.display = 'flex'; progressFill.style.width = '0%'; progressText.textContent = '0%'; const fetchJsonWithTimeout = async (url, timeoutMs = 20000) => { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeoutMs); try { const res = await fetch(url, { signal: ctrl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } finally { clearTimeout(t); } }; const setErrorState = () => { retryBtn.style.display = 'block'; progressContainer.style.display = 'none'; progressRetryBtn.style.display = 'none'; if (downloadAgainBtn) downloadAgainBtn.style.display = 'none'; container.dataset.downloading = 'false'; container.dataset.urlOpened = 'false'; container.dataset.lastDownloadUrl = ''; }; const markCompleteAndOpen = (downloadUrl) => { if (!downloadUrl) { setErrorState(); return; } // Save for the \"download again\" button container.dataset.lastDownloadUrl = String(downloadUrl); // Check if URL was already opened if (container.dataset.urlOpened === 'true') return; // Mark URL as opened container.dataset.urlOpened = 'true'; // Update UI to show completion container.classList.add('completed'); container.classList.remove('video', 'audio'); downloadText.textContent = 'Download Complete!'; progressFill.style.width = '100%'; progressText.textContent = '100%'; progressRetryBtn.style.display = 'none'; if (downloadAgainBtn) downloadAgainBtn.style.display = 'flex'; container.dataset.downloading = 'false'; try { window.open(downloadUrl); } catch (e) { console.warn('Could not open download URL:', e); } }; const pollProgressUrl = (progressURL) => { container.__ytDownloadPoll = setInterval(async () => { try { const progressData = await fetchJsonWithTimeout(progressURL, 15000); const progress = Math.min((Number(progressData.progress) || 0) / 10, 100); progressFill.style.width = `${progress}%`; progressText.textContent = `${Math.round(progress)}%`; if (Number(progressData.progress) >= 1000 && progressData.download_url) { clearInterval(container.__ytDownloadPoll); container.__ytDownloadPoll = null; markCompleteAndOpen(progressData.download_url); } } catch (e) { console.error('Error in progress:', e); clearInterval(container.__ytDownloadPoll); container.__ytDownloadPoll = null; setErrorState(); } }, 3000); }; const trySaveNowProvider = async (baseUrl) => { const url = new URL('/ajax/download.php', baseUrl); url.searchParams.set('copyright', '0'); url.searchParams.set('allow_extended_duration', '1'); url.searchParams.set('format', String(format)); url.searchParams.set('url', videoURL); url.searchParams.set('api', API_KEY_DEVELOPERMDCM); const data = await fetchJsonWithTimeout(url.toString(), 25000); if (!data?.success || !data?.progress_url) { throw new Error('SaveNow provider did not return success/progress_url'); } return data; }; const tryDubsProvider = async () => { const videoId = paramsVideoURL(); if (!videoId) throw new Error('Missing videoId'); const startUrl = new URL(DUBS_START_ENDPOINT); startUrl.searchParams.set('id', videoId); startUrl.searchParams.set('format', String(format)); const startData = await fetchJsonWithTimeout(startUrl.toString(), 25000); if (!startData?.success || !startData?.progressId) { throw new Error('Dubs provider did not return success/progressId'); } const statusUrl = new URL(DUBS_STATUS_ENDPOINT); statusUrl.searchParams.set('id', startData.progressId); container.__ytDownloadPoll = setInterval(async () => { try { const st = await fetchJsonWithTimeout(statusUrl.toString(), 20000); const rawProgress = Number(st?.progress) || 0; // 0..1000 const progress = Math.min(rawProgress / 10, 100); progressFill.style.width = `${progress}%`; progressText.textContent = `${Math.round(progress)}%`; if (st?.finished && st?.downloadUrl) { clearInterval(container.__ytDownloadPoll); container.__ytDownloadPoll = null; markCompleteAndOpen(st.downloadUrl); } } catch (e) { console.error('❌ Error polling dubs status:', e); clearInterval(container.__ytDownloadPoll); container.__ytDownloadPoll = null; setErrorState(); } }, 3000); }; try { let started = null; let lastErr = null; for (const base of DOWNLOAD_API_FALLBACK_BASES) { try { started = await trySaveNowProvider(base); break; } catch (e) { lastErr = e; } } if (started?.success && started?.progress_url) { pollProgressUrl(started.progress_url); return; } console.warn('SaveNow providers failed, falling back to dubs.io', lastErr); await tryDubsProvider(); } catch (error) { setErrorState(); console.error('❌ Error starting download:', error); } } // Define themes const themes = [{ name: 'Default / Reload', gradient: '', textColor: '', raised: '', btnTranslate: '', CurrentProgressVideo: '', videoDuration: '', colorIcons: '', textLogo: '', primaryColor: '', secondaryColor: '', }, { name: 'Midnight Blue', gradient: 'linear-gradient(135deg, #1e3a8a, #3b82f6)', textColor: '#ffffff', raised: '#1e3a8a', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Forest Green', gradient: 'linear-gradient(135deg, #14532d, #22c55e)', textColor: '#ffffff', raised: '#14532d', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Sunset Orange', gradient: 'linear-gradient(135deg, #7c2d12, #f97316)', textColor: '#ffffff', raised: '#7c2d12', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Royal Purple', gradient: 'linear-gradient(135deg, #2e1065, #4c1d95)', textColor: '#ffffff', raised: '#4c1d95', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Cherry Blossom', gradient: 'linear-gradient(135deg, #a9005c, #fc008f)', textColor: '#ffffff', raised: '#fc008f', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Red Dark', gradient: 'linear-gradient(135deg, #790909, #f70131)', textColor: '#ffffff', raised: '#790909', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Raind ', gradient: 'linear-gradient(90deg, #3f5efb 0%, #fc466b 100%)', textColor: '#ffffff', raised: '#3f5efb', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Neon', gradient: 'linear-gradient(273deg, #ee49fd 0%, #6175ff 100%)', textColor: '#ffffff', raised: '#ee49fd', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Azure', gradient: 'linear-gradient(273deg, #0172af 0%, #74febd 100%)', textColor: '#ffffff', raised: '#0172af', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Butterfly', gradient: 'linear-gradient(273deg, #ff4060 0%, #fff16a 100%)', textColor: '#ffffff', raised: '#ff4060', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', }, { name: 'Colombia', gradient: 'linear-gradient(174deg, #fbf63f 0%, #0000bb 45%, #ff0000 99%)', textColor: '#ffffff', raised: '#0000bb', btnTranslate: '#000', CurrentProgressVideo: '#0f0', videoDuration: '#fff', colorIcons: '#fff', textLogo: '#f00', },]; // Create our enhancement panel const panel = $cl('div'); panel.id = 'yt-enhancement-panel'; const panelOverlay = $cl('div'); panelOverlay.id = 'panel-overlay'; $ap(panelOverlay); // Generate theme options HTML const themeOptionsHTML = themes .map( (theme, index) => ` <label > <div class="theme-option"> <div class="theme-preview" style="background: ${theme.gradient};"></div> <input type="radio" name="theme" value="${index}" ${index === 0 ? 'checked' : '' }> <span style="${theme.name === 'Default / Reload Page' ? 'color: red; ' : ''}" class="theme-name">${theme.name}</span> </div> </label> ` ) .join(''); const languageOptionsHTML = Object.entries(languagesTranslate) .map(([code, name]) => { const selected = code === 'en' ? 'selected' : ''; return `<option value="${code}" ${selected}>${name}</option>`; }) .join(''); function checkDarkModeActive() { // YTM is always dark mode if (isYTMusic) return 'dark'; const prefCookie = document.cookie.split('; ').find(c => c.startsWith('PREF=')); if (!prefCookie) return 'light'; const prefValue = prefCookie.substring(5); const params = new URLSearchParams(prefValue); const f6Value = params.get('f6'); const darkModes = ['400', '4000000', '40000400', '40000000']; return darkModes.includes(f6Value) ? 'dark' : 'light'; } let isDarkModeActive = checkDarkModeActive(); // Use Trusted Types to set innerHTML const menuHTML = ` <div class="container-mdcm"> <div class="header-mdcm"> <h1> <i class="fa-brands fa-youtube"></i> YouTube Tools</h1> <div class="icons-mdcm"> <a href="https://update.greasyfork.org/scripts/576162/YouTube%20Ultimate%20Tools.user.js" target="_blank"> <button class="icon-btn-mdcm"> <i class="fa-solid fa-arrows-rotate"></i> </button> </a> <a href="https://github.com/akari310" target="_blank"> <button class="icon-btn-mdcm"> <i class="fa-brands fa-github"></i> </button> </a> <button class="icon-btn-mdcm" id="shareBtn-mdcm"> <i class="fa-solid fa-share-alt"></i> </button> <button class="icon-btn-mdcm" id="importExportBtn"> <i class="fa-solid fa-file-import"></i> </button> <button id="menu-settings-icon" class="icon-btn-mdcm tab-mdcm" data-tab="menu-settings"> <i class="fa-solid fa-gear"></i> </button> <button class="icon-btn-mdcm close_menu_settings"> <i class="fa-solid fa-xmark"></i> </button> </div> </div> <div class="tabs-mdcm"> <button class="tab-mdcm active" data-tab="general"> <i class="fa-solid fa-shield-halved"></i> General </button> <button class="tab-mdcm" data-tab="themes"> <i class="fa-solid fa-palette"></i> Themes </button> <button class="tab-mdcm" data-tab="stats"> <i class="fa-solid fa-square-poll-vertical"></i> Stats </button> <button class="tab-mdcm" data-tab="headers"> <i class="fa-regular fa-newspaper"></i> Header </button> </div> <div id="general" class="tab-content active"> <div class="options-mdcm"> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="hide-comments-toggle"> Hide Comments </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="hide-sidebar-toggle"> Hide Sidebar </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="autoplay-toggle"> Disable Autoplay </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="subtitles-toggle"> Disable Subtitles </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" checked id="dislikes-toggle"> Show Dislikes </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="like-dislike-bar-toggle"> Like vs Dislike bar </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="bookmarks-toggle"> Bookmarks (timestamps) </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="continue-watching-toggle"> Continue watching </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="shorts-channel-name-toggle"> Shorts: show channel name </div> </label> <label> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" checked id="nonstop-playback-toggle"> Nonstop playback </div> </label> <label> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="audio-only-toggle"> Audio-only mode </div> </label> <label> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="audio-only-tab-toggle"> Audio-only this tab </div> </label> <label> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="themes-toggle"> Active Themes </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="translation-toggle"> Translate comments </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="avatars-toggle"> Download avatars </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="reverse-mode-toggle"> Reverse mode </div> </label> <label> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="cinematic-lighting-toggle"> ${isYTMusic ? 'Ambient Mode' : 'Cinematic Mode'} </div> </label> <label> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" checked id="wave-visualizer-toggle"> Wave visualizer Beta </div> </label> <label ${!isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="custom-timeline-color-toggle"> Royal Purple Timeline </div> </label> <label ${isYTMusic ? 'style="display:none"' : ''}> <div class="option-mdcm"> <input type="checkbox" class="checkbox-mdcm" id="sync-cinematic-toggle"> Sync Ambient Mode YT </div> </label> <div class="quality-selector-mdcm" style="grid-column: span 2;"> <div class="select-wrapper-mdcm"> <label>Side/Playlist Panel Style: <select class="tab-button-active" id="side-panel-style-select"> <option value="blur">Blur</option> <option value="liquid">Liquid Glass</option> <option value="transparent">Transparent</option> </select> </label> </div> </div> <div class="quality-selector-mdcm" style="grid-column: span 2;"> <div class="select-wrapper-mdcm"> <label>Effect wave visualizer: <select class="tab-button-active" id="select-wave-visualizer-select"> <option value="linea">Line smooth</option> <option value="barras">Vertical bars</option> <option value="curva">Curved</option> <option value="picos">Smooth peaks</option> <option value="solida">Solid wave</option> <option value="dinamica">Dynamic wave</option> <option value="montana">Smooth mountain</option> </select> </label> </div> </div> <div class="quality-selector-mdcm" style="grid-column: span 2;${isYTMusic ? ' display:none;' : ''}"> <div class="select-wrapper-mdcm"> <label>Default video player quality: <select class="tab-button-active" id="select-video-qualitys-select"> <option value="user">User Default</option> <option value="">Auto</option> <option value="144">144</option> <option value="240">240</option> <option value="360">360</option> <option value="480">480</option> <option value="720">720</option> <option value="1080">1080</option> <option value="1440">1440</option> <option value="2160">2160</option> <option value="4320">4320</option> </select> </label> </div> </div> <div class="quality-selector-mdcm" style="grid-column: span 2;${isYTMusic ? ' display:none;' : ''}"> <div class="select-wrapper-mdcm"> <label>Language for translate comments: <select class="tab-button-active" id="select-languages-comments-select"> ${languageOptionsHTML} </select> </label> </div> </div> <div class="slider-container-mdcm" style="grid-column: span 2;"> <label>Video Player Size: <span id="player-size-value">100</span>%</label> <input type="range" id="player-size-slider" class="slider-mdcm" min="50" max="150" value="100"> <button class="reset-btn-mdcm" id="reset-player-size">Reset video size</button> </div> </div> </div> <div id="themes" class="tab-content"> <div id="background-image-container" class="background-image-container"> <h4>Background Image</h4> <input type="file" id="background_image" accept="image/png, image/jpeg" style="display:none;" /> <div id="background-image-preview" class="background-image-preview"> <span class="background-image-overlay"> <i class="fa fa-camera"></i> <span class="background-image-text">Select image</span> </span> <button id="remove-background-image" class="remove-background-image" title="Quitar fondo">×</button> </div> </div> <div class="themes-hidden"> <div class="options-mdcm" style="margin-bottom: 10px;"> <div> <h4>Choose a Theme</h4> <p>Disable Mode Cinematic on General</p> ${isDarkModeActive === 'dark' ? '' : '<p style="color: red; margin: 10px 0;font-size: 11px;">Activate dark mode to use this option</p>'} </div> </div> <div class="options-mdcm"> <label> <div class="theme-option option-mdcm"> <input type="radio" class="radio-mdcm" name="theme" value="custom" checked> <span class="theme-name">Custom</span> </div> </label> <label> <div class="theme-option option-mdcm theme-selected-normal"> <input type="radio" class="radio-mdcm" name="theme" value="normal"> <span class="theme-name">Selected Themes</span> </div> </label> </div> <div class="themes-options"> <div class="options-mdcm themes-grid-mdcm"> ${themeOptionsHTML} </div> </div> <div class="theme-custom-options"> <div class="options-mdcm"> <div class="option-mdcm"> <div class="card-items-end"> <label>Progressbar Video:</label> <input type="color" id="progressbar-color-picker" class="color-picker-mdcm" value="#ff0000"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Background Color:</label> <input type="color" id="bg-color-picker" class="color-picker-mdcm" value="#000000"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Primary Color:</label> <input type="color" id="primary-color-picker" class="color-picker-mdcm" value="#ffffff"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Secondary Color:</label> <input type="color" id="secondary-color-picker" class="color-picker-mdcm" value="#ffffff"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Header Color:</label> <input type="color" id="header-color-picker" class="color-picker-mdcm" value="#000000"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Icons Color:</label> <input type="color" id="icons-color-picker" class="color-picker-mdcm" value="#ffffff"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Menu Color:</label> <input type="color" id="menu-color-picker" class="color-picker-mdcm" value="#000000"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Line Color Preview:</label> <input type="color" id="line-color-picker" class="color-picker-mdcm" value="#ff0000"> </div> </div> <div class="option-mdcm"> <div class="card-items-end"> <label>Time Color Preview:</label> <input type="color" id="time-color-picker" class="color-picker-mdcm" value="#ffffff"> </div> </div> </div> </div> </div> </div> <div id="stats" class="tab-content"> <div id="yt-stats-toggle"> <div class="stat-row"> <div>Foreground Time</div> <div class="progress"> <div class="progress-bar total-bar" id="usage-bar"></div> </div> <div id="total-time">0h 0m 0s</div> </div> <div class="stat-row"> <div>Video Time</div> <div class="progress"> <div class="progress-bar video-bar" id="video-bar"></div> </div> <div id="video-time">0h 0m 0s</div> </div> <div class="stat-row"> <div>Shorts Time</div> <div class="progress"> <div class="progress-bar shorts-bar" id="shorts-bar"></div> </div> <div id="shorts-time">0h 0m 0s</div> </div> </div> </div> <div id="headers" class="tab-content"> <div class="options-mdcm"> <label>Available in next update</label> </div> </div> <div id="menu-settings" class="tab-content"> <div class="options-mdcm"> <h4 style="margin: 10px 0">Menu Appearance</h4> </div> <div class="options-settings-mdcm"> <div class="option-settings-mdcm"> <label>Backgrounds:</label> <div class="color-boxes" id="bg-color-options"> <div class="color-box" data-type="bg" data-value="#252525" style="background-color: #252525;"></div> <div class="color-box" data-type="bg" data-value="#1e1e1e" style="background-color: #1e1e1e;"></div> <div class="color-box" data-type="bg" data-value="#3a3a3a" style="background-color: #3a3a3a;"></div> <div class="color-box" data-type="bg" data-value="#4a4a4a" style="background-color: #4a4a4a;"></div> <div class="color-box" data-type="bg" data-value="#000000" style="background-color: #000000;"></div> <div class="color-box" data-type="bg" data-value="#00000000" style="background-color: #00000000;"></div> <div class="color-box" data-type="bg" data-value="#2d2d2d" style="background-color: #2d2d2d;"></div> <div class="color-box" data-type="bg" data-value="#444" style="background-color: #444;"></div> </div> </div> <div class="option-settings-mdcm"> <label>Accent Colors:</label> <div class="color-boxes" id="bg-accent-color-options"> <div class="color-box" data-type="accent" data-value="#ff0000" style="background-color: #ff0000;"></div> <div class="color-box" data-type="accent" data-value="#000000" style="background-color: #000000;"></div> <div class="color-box" data-type="accent" data-value="#009c37 " style="background-color: #009c37 ;"></div> <div class="color-box" data-type="accent" data-value="#0c02a0 " style="background-color: #0c02a0 ;"></div> </div> </div> <div class="option-settings-mdcm"> <label>Titles Colors:</label> <div class="color-boxes" id="text-color-options"> <div class="color-box" data-type="color" data-value="#ffffff" style="background-color: #ffffff;"></div> <div class="color-box" data-type="color" data-value="#cccccc" style="background-color: #cccccc;"></div> <div class="color-box" data-type="color" data-value="#b3b3b3" style="background-color: #b3b3b3;"></div> <div class="color-box" data-type="color" data-value="#00ffff" style="background-color: #00ffff;"></div> <div class="color-box" data-type="color" data-value="#00ff00" style="background-color: #00ff00;"></div> <div class="color-box" data-type="color" data-value="#ffff00" style="background-color: #ffff00;"></div> <div class="color-box" data-type="color" data-value="#ffcc00" style="background-color: #ffcc00;"></div> <div class="color-box" data-type="color" data-value="#ff66cc" style="background-color: #ff66cc;"></div> </div> </div> </div> </div> <div id="importExportArea"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <h3>Import / Export Settings</h3> <button class="icon-btn-mdcm" id="closeImportExportBtn"> <i class="fa-solid fa-xmark"></i> </button> </div> <textarea id="config-data" placeholder="Paste configuration here to import"></textarea> <div class="action-buttons-mdcm"> <button id="export-config" class="action-btn-mdcm">Export</button> <button id="import-config" class="action-btn-mdcm">Import</button> </div> </div> <div id="shareDropdown"> <a href="https://www.facebook.com/sharer/sharer.php?u=${urlSharedCode}" target="_blank" data-network="facebook" class="share-link"><i class="fa-brands fa-facebook"></i> Facebook</a><br> <a href="https://twitter.com/intent/tweet?url=${urlSharedCode}" target="_blank" data-network="twitter" class="share-link"><i class="fa-brands fa-twitter"></i> Twitter</a><br> <a href="https://api.whatsapp.com/send?text=${urlSharedCode}" target="_blank" data-network="whatsapp" class="share-link"><i class="fa-brands fa-whatsapp"></i> WhatsApp</a><br> <a href="https://www.linkedin.com/sharing/share-offsite/?url=${urlSharedCode}" target="_blank" data-network="linkedin" class="share-link"><i class="fa-brands fa-linkedin"></i> LinkedIn</a><br> </div> </div> <div class="actions-mdcm"> <div class="developer-mdcm"> <div style="font-size: 11px; opacity: 0.9; margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; line-height: 1.6;"> Developed by <a href="https://github.com/akari310" target="_blank" style="color: #ff4444; text-decoration: none;"><i class="fa-brands fa-github"></i> Akari</a>. Base by <a href="https://github.com/DeveloperMDCM" target="_blank" style="color: #00aaff; text-decoration: none;"><i class="fa-brands fa-github"></i> MDCM</a>. Features from <a href="https://github.com/nvbangg" target="_blank" style="color: #00ffaa; text-decoration: none;"><i class="fa-brands fa-github"></i> nvbangg</a>. </div> </div> <span style="color: #fff" ;>v${GM_info.script.version}</span> </div> `; panel.innerHTML = safeHTML(menuHTML); $ap(panel); let headerObserver = null; function setupHeaderObserver() { if (headerObserver) return; const target = $e('#masthead-container') || $e('ytd-masthead') || document.body; headerObserver = new MutationObserver(() => { const icon = $id('icon-menu-settings'); if (!icon || !document.body.contains(icon)) { addIcon(); } }); headerObserver.observe(target, { childList: true, subtree: true }); } function addIcon() { const existing = $id('icon-menu-settings'); if (existing && document.body.contains(existing)) return; if (existing) existing.closest('#toggle-button')?.remove(); let anchor; if (isYTMusic) { anchor = $e('#right-content'); } else { anchor = $e('ytd-topbar-menu-button-renderer') || $e('#buttons') || $e('#end'); } if (!anchor) return; const toggleButton = $cl('div'); toggleButton.id = 'toggle-button'; toggleButton.style.display = 'flex'; toggleButton.style.alignItems = 'center'; toggleButton.style.justifyContent = 'center'; toggleButton.style.cursor = 'pointer'; toggleButton.style.marginRight = '8px'; const icon = $cl('i'); icon.id = 'icon-menu-settings'; icon.classList.add('fa-solid', 'fa-gear'); icon.style.fontSize = '20px'; toggleButton.appendChild(icon); if (isYTMusic) { anchor.insertBefore(toggleButton, anchor.firstChild); } else { anchor.parentElement.insertBefore(toggleButton, anchor); } toggleButton.addEventListener('click', (e) => { e.stopPropagation(); toggleMenu(); }); setupHeaderObserver(); } let openMenu = false; function toggleMenu() { openMenu = !openMenu; panel.style.display = openMenu ? 'block' : 'none'; panelOverlay.style.display = openMenu ? 'block' : 'none'; } // Close panel when clicking the overlay panelOverlay.addEventListener('click', () => { if (openMenu) { toggleMenu(); } }); addIcon(); const close_menu_settings = $e('.close_menu_settings'); if (close_menu_settings) { close_menu_settings.addEventListener('click', () => { toggleMenu(); }); } // $ap(toggleButton); // Add change listener to the entire panel to save/apply settings immediately panel.addEventListener('change', (e) => { if (e.target.classList.contains('checkbox-mdcm') || e.target.tagName === 'SELECT' || e.target.tagName === 'INPUT') { saveSettings(); if (typeof applySettings === 'function') { applySettings(); } } }); // Specific listeners for live updates of certain features $id('dislikes-toggle')?.addEventListener('change', () => { const st = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); if (!st.dislikes) { // Hide dislikes if turned off const dislikes_content = $e('#top-level-buttons-computed > segmented-like-dislike-button-view-model > yt-smartimation > div > div > dislike-button-view-model > toggle-button-view-model > button-view-model > button'); if (dislikes_content) { // We don't have the original SVG easily, but we can at least hide our custom one or clear it // For now, let's just trigger a reload message or try to refresh the component // Actually, the user just wants it to go away. dislikes_content.style.width = ''; // We'll let applySettings handle the bar, but for the button, we might need a refresh or more complex logic. // But let's try to at least call applySettings. } } }); // Tab functionality const tabButtons = $m('.tab-mdcm'); const tabContents = $m('.tab-content'); tabButtons.forEach((button) => { button.addEventListener('click', () => { const tabName = button.getAttribute('data-tab'); tabButtons.forEach((btn) => btn.classList.remove('active')); tabContents.forEach((content) => content.classList.remove('active')); button.classList.add('active'); $id(tabName).classList.add('active'); }); }); // Function to save settings function saveSettings() { const settings = { theme: $e('input[name="theme"]:checked').value, bgColorPicker: $id('bg-color-picker').value, progressbarColorPicker: $id('progressbar-color-picker').value, primaryColorPicker: $id('primary-color-picker').value, secondaryColorPicker: $id('secondary-color-picker').value, headerColorPicker: $id('header-color-picker').value, iconsColorPicker: $id('icons-color-picker').value, menuColorPicker: $id('menu-color-picker').value, lineColorPicker: $id('line-color-picker').value, timeColorPicker: $id('time-color-picker').value, dislikes: $id('dislikes-toggle').checked, likeDislikeBar: $id('like-dislike-bar-toggle').checked, bookmarks: $id('bookmarks-toggle').checked, continueWatching: $id('continue-watching-toggle').checked, shortsChannelName: $id('shorts-channel-name-toggle').checked, nonstopPlayback: $id('nonstop-playback-toggle') ? $id('nonstop-playback-toggle').checked : true, audioOnly: $id('audio-only-toggle') ? $id('audio-only-toggle').checked : false, themes: $id('themes-toggle').checked, translation: $id('translation-toggle').checked, avatars: $id('avatars-toggle').checked, reverseMode: $id('reverse-mode-toggle').checked, waveVisualizer: $id('wave-visualizer-toggle').checked, waveVisualizerSelected: $id('select-wave-visualizer-select').value, hideComments: $id('hide-comments-toggle').checked, hideSidebar: $id('hide-sidebar-toggle').checked, disableAutoplay: $id('autoplay-toggle').checked, cinematicLighting: $id('cinematic-lighting-toggle').checked, syncCinematic: $id('sync-cinematic-toggle') ? $id('sync-cinematic-toggle').checked : false, // NUEVO SETTING sidePanelStyle: $id('side-panel-style-select') ? $id('side-panel-style-select').value : 'normal', customTimelineColor: $id('custom-timeline-color-toggle') ? $id('custom-timeline-color-toggle').checked : false, disableSubtitles: $id('subtitles-toggle') ? $id('subtitles-toggle').checked : false, // fontSize: $id('font-size-slider').value, playerSize: $id('player-size-slider').value, selectVideoQuality: $id('select-video-qualitys-select').value, languagesComments: $id('select-languages-comments-select').value, // menuBgColor: $id('menu-bg-color-picker').value, // menuTextColor: $id('menu-text-color-picker').value, menu_akari: { bg: selectedBgColor, color: selectedTextColor, accent: selectedBgAccentColor } // menuFontSize: $id('menu-font-size-slider').value, }; GM_setValue(SETTINGS_KEY, JSON.stringify(settings)); } // Function to load settings function loadSettings() { const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); // Mark as loaded early so applySettings/saveSettings don't overwrite persisted values with defaults. __ytToolsRuntime.settingsLoaded = true; if (settings.theme) { $e(`input[name="theme"][value="${settings.theme}"]`).checked = true; } const menuData = settings.menu_akari || settings.menu_developermdcm || { bg: "#252525", color: "#ffffff", accent: "#ff0000" }; $id('bg-color-picker').value = settings.bgColorPicker || '#000000'; $id('progressbar-color-picker').value = settings.progressbarColorPicker || '#ff0000'; $id('primary-color-picker').value = settings.primaryColorPicker || '#ffffff'; $id('secondary-color-picker').value = settings.secondaryColorPicker || '#ffffff'; $id('header-color-picker').value = settings.headerColorPicker || '#000'; $id('icons-color-picker').value = settings.iconsColorPicker || '#ffffff'; $id('menu-color-picker').value = settings.menuColorPicker || '#000'; $id('line-color-picker').value = settings.lineColorPicker || '#ff0000'; $id('time-color-picker').value = settings.timeColorPicker || '#ffffff'; $id('dislikes-toggle').checked = settings.dislikes || false; $id('like-dislike-bar-toggle').checked = settings.likeDislikeBar || false; $id('bookmarks-toggle').checked = settings.bookmarks || false; $id('continue-watching-toggle').checked = settings.continueWatching || false; $id('shorts-channel-name-toggle').checked = settings.shortsChannelName || false; if ($id('nonstop-playback-toggle')) $id('nonstop-playback-toggle').checked = settings.nonstopPlayback !== false; if ($id('audio-only-toggle')) $id('audio-only-toggle').checked = settings.audioOnly || false; syncAudioOnlyTabCheckbox(settings); $id('themes-toggle').checked = settings.themes || false; $id('translation-toggle').checked = settings.translation || false; $id('avatars-toggle').checked = settings.avatars || false; $id('reverse-mode-toggle').checked = settings.reverseMode || false; $id('wave-visualizer-toggle').checked = settings.waveVisualizer || false; $id('select-wave-visualizer-select').value = settings.waveVisualizerSelected || 'dinamica'; $id('hide-comments-toggle').checked = settings.hideComments || false; $id('hide-sidebar-toggle').checked = settings.hideSidebar || false; $id('autoplay-toggle').checked = settings.disableAutoplay || false; $id('cinematic-lighting-toggle').checked = settings.cinematicLighting || false; if ($id('sync-cinematic-toggle')) $id('sync-cinematic-toggle').checked = settings.syncCinematic || false; if ($id('side-panel-style-select')) $id('side-panel-style-select').value = settings.sidePanelStyle || 'blur'; if ($id('custom-timeline-color-toggle')) $id('custom-timeline-color-toggle').checked = settings.customTimelineColor || false; if ($id('subtitles-toggle')) $id('subtitles-toggle').checked = settings.disableSubtitles || false; $id('player-size-slider').value = settings.playerSize || 100; $id('select-video-qualitys-select').value = settings.selectVideoQuality || 'user'; $id('select-languages-comments-select').value = settings.languagesComments || 'en'; selectedBgColor = menuData.bg; selectedTextColor = menuData.color; selectedBgAccentColor = menuData.accent; $m('#bg-color-options .color-box').forEach(el => { el.classList.toggle('selected', el.dataset.value === selectedBgColor); }); $m('#text-color-options .color-box').forEach(el => { el.classList.toggle('selected', el.dataset.value === selectedTextColor); }); $m('#bg-accent-color-options .color-box').forEach(el => { el.classList.toggle('selected', el.dataset.value === selectedBgAccentColor); }); // Apply menu colors $sp('--yt-enhance-menu-bg', selectedBgColor); $sp('--yt-enhance-menu-text', selectedTextColor); $sp('--yt-enhance-menu-accent', selectedBgAccentColor); updateSliderValues(); setTimeout(() => { applySettings(); if (settings.dislikes && !isYTMusic) { videoDislike(); shortDislike(); showDislikes = true; } if (!isYTMusic && window.location.href.includes('youtube.com/watch?v=')) { detectInitialCinematicState(); } }, 500); } // Check if the video is in cinematic mode async function detectInitialCinematicState() { return new Promise((resolve) => { const waitForVideo = () => { const video = $e('video'); const cinematicDiv = $id('cinematics'); if (!video || !cinematicDiv || isNaN(video.duration) || video.duration === 0) { setTimeout(waitForVideo, 500); return; } const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); if (!settings.syncCinematic) { // apply cinematic toggle const cinematicToggle = $id('cinematic-lighting-toggle'); if (cinematicToggle && cinematicDiv) { cinematicDiv.style.display = cinematicToggle.checked ? 'block' : 'none'; } resolve(false); return; } const startTime = video.currentTime; const checkPlayback = () => { if (video.currentTime >= startTime + 1) { const isActive = isCinematicActive(); const cinematicToggle = $id('cinematic-lighting-toggle'); if (cinematicToggle && cinematicToggle.checked !== isActive) { cinematicToggle.checked = isActive; saveSettings(); } resolve(isActive); } else { setTimeout(checkPlayback, 300); } }; checkPlayback(); }; waitForVideo(); }); } $m('.color-box').forEach(box => { box.addEventListener('click', () => { const type = box.dataset.type; const value = box.dataset.value; if (type === 'bg') { selectedBgColor = value; $sp('--yt-enhance-menu-bg', value); $m('#bg-color-options .color-box').forEach(el => { el.classList.remove('selected'); }); box.classList.add('selected'); } else if (type === 'color') { selectedTextColor = value; $sp('--yt-enhance-menu-text', value); $m('#text-color-options .color-box').forEach(el => { el.classList.remove('selected'); }); box.classList.add('selected'); } else if (type === 'accent') { selectedBgAccentColor = value; $sp('--yt-enhance-menu-accent', value); $m('#bg-accent-color-options .color-box').forEach(el => { el.classList.remove('selected'); }); box.classList.add('selected'); } saveSettings(); }); }); function updateSliderValues() { $id('player-size-value').textContent = $id('player-size-slider').value; } $id('reset-player-size').addEventListener('click', () => { $id('player-size-slider').value = 100; updateSliderValues(); applySettings(); }); // Initialize header buttons once function initializeHeaderButtons() { const shareBtn = $id('shareBtn-mdcm'); const importExportBtn = $id('importExportBtn'); const closeImportExportBtn = $id('closeImportExportBtn'); if (shareBtn && !shareBtn.dataset.initialized) { shareBtn.dataset.initialized = 'true'; shareBtn.addEventListener('click', function (event) { event.stopPropagation(); const dropdown = $id('shareDropdown'); if (dropdown) { dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block'; } }); } if (importExportBtn && !importExportBtn.dataset.initialized) { importExportBtn.dataset.initialized = 'true'; importExportBtn.addEventListener('click', function () { const importExportArea = $id('importExportArea'); if (importExportArea) { importExportArea.classList.toggle('active'); } }); } if (closeImportExportBtn && !closeImportExportBtn.dataset.initialized) { closeImportExportBtn.dataset.initialized = 'true'; closeImportExportBtn.addEventListener('click', function () { const importExportArea = $id('importExportArea'); if (importExportArea) { importExportArea.classList.remove('active'); } }); } } // Persistent check to ensure the gear icon survives YouTube's dynamic UI updates // setupHeaderObserver() is now called inside addIcon() // ------------------------------ // YTM Ambient Mode — CSS background-image blur approach // Uses album art or video poster as a blurred full-screen background glow // Elements stay persistent for smooth transitions — only .active class toggles // ------------------------------ const ytmAmbientMode = { active: false, _initialized: false, // true once DOM elements are created glowEl: null, styleEl: null, videoEl: null, _lastSrc: '', _pollId: null, // Find album art image URL from YTM player page _getArtUrl() { // 1. Rock-solid way: Get from YouTube Player API directly try { const mp = document.getElementById('movie_player'); if (mp && typeof mp.getVideoData === 'function') { const vData = mp.getVideoData(); if (vData && vData.video_id) { return `https://i.ytimg.com/vi/${vData.video_id}/sddefault.jpg`; } } } catch (e) { } // 2. Fallbacks const selectors = [ '#song-image yt-img-shadow img', '#song-image img', 'ytmusic-player-page #thumbnail img', '#player-page .thumbnail img', 'ytmusic-player-bar .image img', 'ytmusic-player-bar img' ]; for (const sel of selectors) { const img = document.querySelector(sel); if (img && img.src && img.src.startsWith('http')) { return img.src.replace(/=w\d+-h\d+/, '=w640-h640').replace(/=s\d+/, '=s640'); } } const video = $e('video'); if (video && video.poster) return video.poster; return null; }, // Create DOM elements once (called only once, persists across show/hide) _ensureInit() { if (this._initialized) return; this._initialized = true; // Create the glow div this.glowEl = document.createElement('div'); this.glowEl.id = 'ytm-ambient-glow'; document.body.appendChild(this.glowEl); // Create the custom sidebar divider that perfectly fits the top/bottom bars this.dividerEl = document.createElement('div'); this.dividerEl.id = 'ytm-custom-divider'; document.body.appendChild(this.dividerEl); // Create style element this.styleEl = document.createElement('style'); this.styleEl.id = 'ytm-ambient-style'; this.styleEl.textContent = ` #ytm-ambient-glow { position: fixed; top: -200px; left: -200px; width: calc(100vw + 400px); height: calc(100vh + 400px); pointer-events: none; z-index: -1; opacity: 0; background-size: cover; background-position: center; background-repeat: no-repeat; filter: blur(140px) saturate(2.2) brightness(0.9); transition: opacity 1.2s ease; } #ytm-ambient-glow.active { opacity: 0.7; } #ytm-custom-divider { position: fixed; width: 1px; background: rgba(255, 255, 255, 0.15); pointer-events: none; z-index: 2000; opacity: 0; transition: opacity 0.3s ease; } body.ytm-ambient-active #ytm-custom-divider.active { opacity: 1; } /* Make player page backgrounds transparent so glow shows through */ body.ytm-ambient-active ytmusic-app, body.ytm-ambient-active ytmusic-app-layout, body.ytm-ambient-active #layout { background-color: transparent !important; background: transparent !important; transition: background-color 0.6s ease; } body.ytm-ambient-active ytmusic-player-page, body.ytm-ambient-active #player-page, body.ytm-ambient-active ytmusic-player-page #main-panel, body.ytm-ambient-active .background-gradient { background-color: transparent !important; background: transparent !important; background-image: none !important; } /* Make nav bar, player bar, and side drawer transparent so the aura blends behind them smoothly */ body.ytm-ambient-active #nav-bar-background, body.ytm-ambient-active #player-bar-background, body.ytm-ambient-active ytmusic-nav-bar, body.ytm-ambient-active ytmusic-player-bar, body.ytm-ambient-active tp-yt-app-drawer, body.ytm-ambient-active tp-yt-app-drawer #contentContainer, body.ytm-ambient-active #guide-wrapper, body.ytm-ambient-active #guide-content, body.ytm-ambient-active ytmusic-guide-renderer, body.ytm-ambient-active #mini-guide-background, body.ytm-ambient-active #mini-guide { background: transparent !important; background-color: transparent !important; background-image: none !important; } /* Remove borders that piece through the player bar when transparent */ body.ytm-ambient-active tp-yt-app-drawer, body.ytm-ambient-active tp-yt-app-drawer #contentContainer, body.ytm-ambient-active #guide-wrapper, body.ytm-ambient-active #guide-content, body.ytm-ambient-active ytmusic-guide-renderer, body.ytm-ambient-active #mini-guide-background { border: none !important; border-right: none !important; box-shadow: none !important; } /* Hide home/browse pages when player is open so they don't bleed through or block the glow */ body.ytm-ambient-active ytmusic-browse-response { visibility: hidden !important; opacity: 0 !important; } `; document.head.appendChild(this.styleEl); }, // Show ambient (fast — just toggle class + update art) show() { if (!isYTMusic) return; if (this.active) return; if (!window.location.href.includes('/watch')) return; this._ensureInit(); this.active = true; // Update video reference this.videoEl = document.querySelector('video'); if (this.glowEl) { this.glowEl.classList.add('active'); document.body.classList.add('ytm-ambient-active'); } this._updateArt(); this._startPoll(); this._startTracker(); // Listen for play events (for art updates on song change) if (this.videoEl) { this.videoEl.removeEventListener('play', this._onPlay); this.videoEl.addEventListener('play', this._onPlay); } }, // Hide ambient (fast — just toggle class, keep elements) hide() { this.active = false; if (this._pollId) { clearInterval(this._pollId); this._pollId = null; } if (this._trackerId) { cancelAnimationFrame(this._trackerId); this._trackerId = null; } if (this.glowEl) { this.glowEl.classList.remove('active'); document.body.classList.remove('ytm-ambient-active'); } if (this.dividerEl) { this.dividerEl.classList.remove('active'); } if (this.videoEl) { this.videoEl.removeEventListener('play', this._onPlay); this.videoEl = null; } }, // Full cleanup — remove all DOM elements (only when disabling feature) destroy() { this.hide(); this._lastSrc = ''; this._initialized = false; if (this.glowEl) { if (this.glowEl.parentNode) this.glowEl.parentNode.removeChild(this.glowEl); this.glowEl = null; } if (this.dividerEl) { if (this.dividerEl.parentNode) this.dividerEl.parentNode.removeChild(this.dividerEl); this.dividerEl = null; } if (this.styleEl) { if (this.styleEl.parentNode) this.styleEl.parentNode.removeChild(this.styleEl); this.styleEl = null; } }, _startTracker() { if (this._trackerId) cancelAnimationFrame(this._trackerId); const self = this; function track() { if (!self.active) { self._trackerId = null; return; } const nav = document.querySelector('ytmusic-nav-bar'); const player = document.querySelector('ytmusic-player-bar'); const drawer = document.querySelector('tp-yt-app-drawer'); const wrapper = document.querySelector('#guide-wrapper') || document.querySelector('#mini-guide-background'); if (nav && player && drawer && wrapper && self.dividerEl) { const navRect = nav.getBoundingClientRect(); const playerRect = player.getBoundingClientRect(); const wrapperRect = wrapper.getBoundingClientRect(); let leftPos = wrapperRect.right; // Minor correction if right bound goes missing if (leftPos <= 0 || !leftPos) leftPos = drawer.hasAttribute('opened') ? 240 : 72; self.dividerEl.style.top = navRect.bottom + 'px'; self.dividerEl.style.height = (playerRect.top - navRect.bottom) + 'px'; self.dividerEl.style.left = leftPos + 'px'; self.dividerEl.classList.add('active'); } self._trackerId = requestAnimationFrame(track); } this._trackerId = requestAnimationFrame(track); }, // Legacy aliases for compatibility setup() { this.show(); }, cleanup() { this.hide(); }, _updateArt() { const url = this._getArtUrl(); if (url && url !== this._lastSrc) { this._lastSrc = url; if (this.glowEl) { this.glowEl.style.backgroundImage = `url("${url}")`; } } }, _startPoll() { if (this._pollId) clearInterval(this._pollId); const self = this; this._pollId = setInterval(() => { if (!self.active) { clearInterval(self._pollId); self._pollId = null; return; } if (!window.location.href.includes('/watch')) { self.hide(); return; } self._updateArt(); }, 2000); }, _onPlay: function () { if (!window.location.href.includes('/watch')) return; const g = document.getElementById('ytm-ambient-glow'); if (g) { g.classList.add('active'); document.body.classList.add('ytm-ambient-active'); } ytmAmbientMode._updateArt(); }, }; // Persistent ambient watcher — fast URL monitoring for smooth transitions if (isYTMusic) { let _ambientWatcherId = null; function startAmbientWatcher() { if (_ambientWatcherId) return; _ambientWatcherId = setInterval(() => { if (document.visibilityState !== 'visible') return; const s = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); const onWatch = window.location.href.includes('/watch'); if (!s.cinematicLighting) { if (ytmAmbientMode.active) ytmAmbientMode.hide(); return; } if (onWatch && !ytmAmbientMode.active) { ytmAmbientMode.show(); } else if (!onWatch && ytmAmbientMode.active) { ytmAmbientMode.hide(); } }, 1500); // Reduced frequency from 800ms to 1500ms } setTimeout(startAmbientWatcher, 1500); // Also respond to YTM-specific events immediately document.addEventListener('yt-page-data-updated', () => { const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); if (!settings.cinematicLighting) return; if (window.location.href.includes('/watch')) { if (!ytmAmbientMode.active) ytmAmbientMode.show(); else ytmAmbientMode._updateArt(); // might be a new song } else if (ytmAmbientMode.active) { ytmAmbientMode.hide(); } }); } // Cinematic Lighting Control Functions function isWatchPage() { return window.location.href.includes('youtube.com/watch'); } function isCinematicActive() { const cinematicDiv = document.getElementById('cinematics'); if (!cinematicDiv) { return false; } const hasContent = cinematicDiv.innerHTML.trim() !== ''; const hasCanvas = cinematicDiv.querySelector('canvas') !== null; const hasChildren = cinematicDiv.children.length > 0; const hasCinematicElements = cinematicDiv.querySelector('div[style*="position: fixed"]') !== null; return hasContent || hasCanvas || hasChildren || hasCinematicElements; } function toggleCinematicLighting() { const settingsButton = $e('.ytp-button.ytp-settings-button'); if (!settingsButton) { console.log('[YT Tools] Settings button not found'); return; } settingsButton.click(); // Cinematic/ambient keywords in multiple languages for robust detection const cinematicKeywords = [ 'cinematic', 'lighting', 'cinema', 'ambient', 'ch\u1EBF \u0111\u1ED9 \u0111i\u1EC7n \u1EA3nh', // Vietnamese: Chế độ điện ảnh '\u0111i\u1EC7n \u1EA3nh', // Vietnamese: điện ảnh 'atmosph', 'ambiante', 'cin\u00E9ma', // French '\u30A2\u30F3\u30D3\u30A8\u30F3\u30C8', // Japanese '\uC2DC\uB124\uB9C8\uD2F1', // Korean ]; const findAndClickCinematic = () => { const menuItems = $m('.ytp-menuitem'); if (!menuItems || menuItems.length === 0) return false; for (let item of menuItems) { // Method 1: Look for toggle checkbox (cinematic is the only toggle-type menu item) const toggleCheckbox = item.querySelector('.ytp-menuitem-toggle-checkbox'); if (toggleCheckbox) { console.log('[YT Tools] Found cinematic/ambient toggle item (by checkbox)'); item.click(); return true; } } // Method 2: Match by localized text keywords for (let item of menuItems) { const text = (item.textContent || '').toLowerCase(); for (const kw of cinematicKeywords) { if (text.includes(kw)) { console.log('[YT Tools] Found cinematic option by keyword:', kw); item.click(); return true; } } } // Method 3: Match by SVG icon path for (let item of menuItems) { const icon = item.querySelector('.ytp-menuitem-icon svg path'); if (icon && (icon.getAttribute('d')?.includes('M21 7v10H3V7') || icon.getAttribute('d')?.includes('M12 2C6.48 2 2 6.48 2 12'))) { console.log('[YT Tools] Found cinematic option by SVG path'); item.click(); return true; } } return false; }; const closeMenu = () => { const menu = $e('.ytp-settings-menu'); if (menu) document.body.click(); }; // Use polling instead of MutationObserver for more reliable detection let attempts = 0; const maxAttempts = 20; const pollInterval = setInterval(() => { attempts++; if (findAndClickCinematic()) { clearInterval(pollInterval); setTimeout(closeMenu, 150); return; } if (attempts >= maxAttempts) { clearInterval(pollInterval); console.warn('[YT Tools] Could not find cinematic/ambient toggle after', maxAttempts, 'attempts'); closeMenu(); } }, 200); } // Function to apply settings function applySettings() { const formulariodescarga = $e('.formulariodescarga'); const formulariodescargaaudio = $e('.formulariodescargaaudio'); if (formulariodescarga != undefined) { formulariodescarga.classList.add('ocultarframe'); formulariodescargaaudio.classList.add('ocultarframe'); } const settings = { theme: $e('input[name="theme"]:checked').value, bgColorPicker: $id('bg-color-picker').value, progressbarColorPicker: $id('progressbar-color-picker').value, primaryColorPicker: $id('primary-color-picker').value, secondaryColorPicker: $id('secondary-color-picker').value, headerColorPicker: $id('header-color-picker').value, iconsColorPicker: $id('icons-color-picker').value, menuColorPicker: $id('menu-color-picker').value, lineColorPicker: $id('line-color-picker').value, timeColorPicker: $id('time-color-picker').value, dislikes: $id('dislikes-toggle').checked, likeDislikeBar: $id('like-dislike-bar-toggle').checked, bookmarks: $id('bookmarks-toggle').checked, continueWatching: $id('continue-watching-toggle').checked, shortsChannelName: $id('shorts-channel-name-toggle').checked, nonstopPlayback: $id('nonstop-playback-toggle') ? $id('nonstop-playback-toggle').checked : true, audioOnly: $id('audio-only-toggle') ? $id('audio-only-toggle').checked : false, themes: $id('themes-toggle').checked, translation: $id('translation-toggle').checked, avatars: $id('avatars-toggle').checked, reverseMode: $id('reverse-mode-toggle').checked, waveVisualizer: $id('wave-visualizer-toggle').checked, waveVisualizerSelected: $id('select-wave-visualizer-select').value, hideComments: $id('hide-comments-toggle').checked, hideSidebar: $id('hide-sidebar-toggle').checked, disableAutoplay: $id('autoplay-toggle').checked, cinematicLighting: $id('cinematic-lighting-toggle') ? $id('cinematic-lighting-toggle').checked : false, syncCinematic: $id('sync-cinematic-toggle') ? $id('sync-cinematic-toggle').checked : false, // NUEVO SETTING sidePanelStyle: $id('side-panel-style-select') ? $id('side-panel-style-select').value : 'blur', customTimelineColor: $id('custom-timeline-color-toggle') ? $id('custom-timeline-color-toggle').checked : false, disableSubtitles: $id('subtitles-toggle') ? $id('subtitles-toggle').checked : false, // fontSize: $id('font-size-slider').value, playerSize: $id('player-size-slider').value, selectVideoQuality: $id('select-video-qualitys-select').value, languagesComments: $id('select-languages-comments-select').value, // menuBgColor: $id('menu-bg-color-picker').value, // menuTextColor: $id('menu-text-color-picker').value, menu_developermdcm: { bg: selectedBgColor, color: selectedTextColor, accent: selectedBgAccentColor } // menuFontSize: $id('menu-font-size-slider').value, }; $sp('--yt-enhance-menu-bg', settings.menu_developermdcm.bg); $sp('--yt-enhance-menu-text', settings.menu_developermdcm.color); $sp('--yt-enhance-menu-accent', settings.menu_developermdcm.accent); renderizarButtons(); applyNonstopPlayback(settings.nonstopPlayback); syncAudioOnlyTabCheckbox(settings); applyAudioOnlyMode(getEffectiveAudioOnly(settings)); // Handle Side Panel Style classes document.body.classList.remove('ytm-style-blur', 'ytm-style-liquid', 'ytm-style-transparent'); if (settings.sidePanelStyle === 'liquid') { document.body.classList.add('ytm-style-liquid'); } else if (settings.sidePanelStyle === 'transparent') { document.body.classList.add('ytm-style-transparent'); } else { document.body.classList.add('ytm-style-blur'); } // Initialize header buttons initializeHeaderButtons(); // Hide comments (YT only) if (!isYTMusic) { const commentsSection = $id('comments'); if (commentsSection) { commentsSection.style.display = settings.hideComments ? 'none' : 'block'; } // Like vs Dislike Bar (YT only) if (typeof applyLikeDislikeBarIfEnabled === 'function') { applyLikeDislikeBarIfEnabled(settings); } // Show Dislikes count (YT only) if (typeof videoDislike === 'function') videoDislike(); if (typeof shortDislike === 'function') shortDislike(); showDislikes = !!settings.dislikes; } // Active inactive Themes const themesMenuSection = $e('.themes-hidden'); if (themesMenuSection) { themesMenuSection.style.display = settings.themes ? 'block' : 'none'; } // Hide sidebar (YT only) if (!isYTMusic) { const sidebarSection = $e('#secondary > #secondary-inner'); if (sidebarSection) { sidebarSection.classList.add('side-moi'); const sidebarSection2 = $e('.side-moi'); sidebarSection2.style.display = settings.hideSidebar ? 'none' : 'block'; } } // Disable autoplay (YT only - uses .ytp-autonav-toggle-button) if (!isYTMusic) { const autoplayToggle = $e('.ytp-autonav-toggle-button'); if (autoplayToggle) { const isCurrentlyOn = autoplayToggle.getAttribute('aria-checked') === 'true'; if (settings.disableAutoplay && isCurrentlyOn) { autoplayToggle.click(); } else if (!settings.disableAutoplay && !isCurrentlyOn) { autoplayToggle.click(); } } } // Disable subtitles (YT only - uses .ytp-subtitles-button) if (!isYTMusic) { const subtitleToggle = $e('.ytp-subtitles-button'); if (subtitleToggle) { const isCurrentlyOn = subtitleToggle.getAttribute('aria-pressed') === 'true'; if (settings.disableSubtitles && isCurrentlyOn) { subtitleToggle.click(); } else if (!settings.disableSubtitles && !isCurrentlyOn) { subtitleToggle.click(); } } } // Apply cinematic/ambient lighting setting if (isYTMusic) { // YTM: custom ambient mode if (settings.cinematicLighting && isWatchPage()) { setTimeout(() => { ytmAmbientMode.setup(); }, 800); } else { ytmAmbientMode.cleanup(); } } else if (isWatchPage()) { setTimeout(() => { const isCurrentlyActive = isCinematicActive(); if (settings.syncCinematic) { if (settings.cinematicLighting && !isCurrentlyActive) { toggleCinematicLighting(); } else if (!settings.cinematicLighting && isCurrentlyActive) { toggleCinematicLighting(); } } else { const cinematicDiv = $id('cinematics'); if (cinematicDiv) { cinematicDiv.style.display = settings.cinematicLighting ? 'block' : 'none'; } } }, 1000); } // Adjust font size // $e('body').style.fontSize = `${settings.fontSize}px`; // Adjust player size const player = $e('video'); if (player) { player.style.transform = `scale(${settings.playerSize / 100})`; } // selected video quality (YT only - uses div#movie_player) if (!isYTMusic) { const video = $e('div#movie_player'); let ytPlayerQuality = localStorage.getItem('yt-player-quality'); if (video != undefined && settings.selectVideoQuality !== "user") { if (ytPlayerQuality) { let qualitySettings = JSON.parse(ytPlayerQuality); qualitySettings.data = JSON.stringify({ quality: settings.selectVideoQuality, previousQuality: 240 }); localStorage.setItem('yt-player-quality', JSON.stringify(qualitySettings)); } else { let defaultQualitySettings = { data: JSON.stringify({ quality: 720, previousQuality: 240 }), expiration: Date.now() + (365 * 24 * 60 * 60 * 1000), creation: Date.now() }; localStorage.setItem('yt-player-quality', JSON.stringify(defaultQualitySettings)); } } } // Apply menu appearance settings // $sp('--yt-enhance-menu-bg', settings.menuBgColor); // $sp('--yt-enhance-menu-text', settings.menuTextColor); // $sp('--yt-enhance-menu-font-size', `${settings.menuFontSize}px`); // Apply theme const selectedTheme = themes[settings.theme] || themes[0] || {}; const isThemeCustom = $e(`input[name="theme"][value="custom"]`).checked; const isThemeNormal = $e(`input[name="theme"][value="normal"]`).checked; const themeCustomOptions = $e('.theme-custom-options'); const themeNormal = $e('.theme-selected-normal'); // Tối ưu: Dùng mảng để gom CSS, chỉ tiêm vào trang 1 lần duy nhất ở cuối let dynamicCssArray = []; const addDynamicCss = (css) => { if (css) dynamicCssArray.push(css); }; // CHANGE DEFAULT TIMELINE TO ROYAL PURPLE AESTHETIC if (settings.customTimelineColor) { addDynamicCss(` .ytp-swatch-background-color { background: linear-gradient(135deg, #4c1d95, #8b5cf6) !important; } `); if (isYTMusic) { addDynamicCss(` #progress-bar { --paper-slider-active-color: #8b5cf6 !important; --paper-slider-knob-color: #8b5cf6 !important; --paper-slider-secondary-color: #8b5cf680 !important; } `); } } if (isThemeCustom) { themeNormal.style.display = "flex" themeCustomOptions.style.display = "flex"; $e('.themes-options').style.display = "none"; } if (isThemeNormal) { $e(`input[name="theme"][value="custom"]`).checked = false; } // Helper: apply YTM-specific CSS variables for theming function applyYTMThemeVars(bgColor, textColor, secondaryText, menuBg, iconColor, raisedBg, progressColor, progressSecondary) { // YTM-specific variables $sp('--ytmusic-general-background', bgColor); $sp('--ytmusic-background', bgColor); $sp('--ytmusic-color-white1', textColor); $sp('--ytmusic-color-white2', secondaryText || textColor); $sp('--ytmusic-color-white3', secondaryText || textColor); $sp('--ytmusic-color-white4', secondaryText || textColor); $sp('--ytmusic-player-bar-background', raisedBg || bgColor); $sp('--ytmusic-nav-bar-background', bgColor); $sp('--ytmusic-nav-bar', bgColor); $sp('--ytmusic-search-background', menuBg || bgColor); // Shared YT/YTM variables $sp('--yt-spec-general-background-a', bgColor); $sp('--yt-spec-general-background-b', bgColor); $sp('--yt-spec-general-background-c', bgColor); // YTM Slider Progress bar if (progressColor) { $sp('--paper-slider-active-color', progressColor); $sp('--paper-slider-knob-color', progressColor); $sp('--paper-progress-active-color', progressColor); } if (progressSecondary) { $sp('--paper-slider-secondary-color', progressSecondary); $sp('--paper-progress-secondary-color', progressSecondary); } // Force colors via CSS to prevent YTM's native ambient mode from overriding the variables // Only add this if not cleared (bgColor is truthy) if (bgColor) { addDynamicCss(` #nav-bar-background.ytmusic-app-layout { background: ${bgColor} !important; background-image: ${bgColor.includes('gradient') ? bgColor : 'none'} !important; } ytmusic-app-layout.content-scrolled #nav-bar-background.ytmusic-app-layout { background: ${bgColor} !important; } #player-bar-background.ytmusic-app-layout { background: ${raisedBg || bgColor} !important; background-image: ${(raisedBg || bgColor).includes('gradient') ? (raisedBg || bgColor) : 'none'} !important; } `); } } if (isYTMusic) { initYTMHeaderScroll(); } function checkDarkMode() { if (settings.themes) { if (isDarkModeActive === 'dark' && !isThemeCustom) { // Apply theme const themesOpts = $e('.themes-options'); if (themesOpts) themesOpts.style.display = "block"; if (themeNormal) themeNormal.style.display = "none"; if (themeCustomOptions) themeCustomOptions.style.display = "none"; if (settings.theme === '0') { // Clean up CSS vars from previous theme const props = [ '--ytmusic-general-background', '--ytmusic-background', '--ytmusic-color-white1', '--ytmusic-color-white2', '--ytmusic-color-white3', '--ytmusic-color-white4', '--ytmusic-player-bar-background', '--ytmusic-nav-bar-background', '--ytmusic-search-background', '--yt-spec-general-background-a', '--yt-spec-general-background-b', '--yt-spec-general-background-c', '--yt-spec-base-background', '--yt-spec-text-primary', '--yt-spec-text-secondary', '--yt-spec-menu-background', '--yt-spec-icon-inactive', '--yt-spec-brand-icon-inactive', '--yt-spec-brand-icon-active', '--yt-spec-static-brand-red', '--yt-spec-raised-background', '--yt-spec-static-brand-white', '--ytd-searchbox-background', '--ytd-searchbox-text-color', '--ytcp-text-primary', '--ytmusic-nav-bar', '--paper-slider-active-color', '--paper-slider-knob-color', '--paper-progress-active-color', '--paper-slider-secondary-color', '--paper-progress-secondary-color' ]; props.forEach(p => document.documentElement.style.removeProperty(p)); addDynamicCss(` .botones_div { background-color: transparent; border: none; color: #ccc !important; user-select: none; } `); if (localStorage.getItem('backgroundImage')) { applyPageBackground(localStorage.getItem('backgroundImage'), null); } else { applyPageBackground(null); } return; } // Standard YT CSS variables $sp('--yt-spec-base-background', selectedTheme.gradient); $sp('--yt-spec-text-primary', selectedTheme.textColor); $sp('--yt-spec-text-secondary', selectedTheme.textColor); $sp('--yt-spec-menu-background', selectedTheme.gradient); $sp('--yt-spec-icon-inactive', selectedTheme.textColor); $sp('--yt-spec-brand-icon-inactive', selectedTheme.textColor); $sp('--yt-spec-brand-icon-active', selectedTheme.gradient); $sp('--yt-spec-static-brand-red', selectedTheme.gradient); // line current time $sp('--yt-spec-raised-background', selectedTheme.raised); $sp('--yt-spec-static-brand-red', selectedTheme.CurrentProgressVideo); $sp('--yt-spec-static-brand-white', selectedTheme.textColor); $sp('--ytd-searchbox-background', selectedTheme.gradient); $sp('--ytd-searchbox-text-color', selectedTheme.textColor); $sp('--ytcp-text-primary', selectedTheme.textColor); // YTM-specific CSS variables let ytmSliderSolidColor = selectedTheme.CurrentProgressVideo; if (selectedTheme.gradient) { let colors = selectedTheme.gradient.match(/#[0-9a-fA-F]{3,6}/g); if (colors && colors.length > 0) { ytmSliderSolidColor = colors[colors.length - 1]; // Use the most vibrant gradient color } } if (isYTMusic) { applyYTMThemeVars( selectedTheme.gradient, selectedTheme.textColor, selectedTheme.textColor, selectedTheme.gradient, selectedTheme.colorIcons || selectedTheme.textColor, selectedTheme.raised === '#303131' ? selectedTheme.gradient : selectedTheme.raised, ytmSliderSolidColor, ytmSliderSolidColor + '80' // Add 50% opacity in hex for secondary 'buffer' slider ); } addDynamicCss(` .botones_div { background-color: transparent; border: none; color: #999999; user-select: none; } .ytp-menuitem[aria-checked=true] .ytp-menuitem-toggle-checkbox { background: ${selectedTheme.gradient} !important; } #background.ytd-masthead { background: ${selectedTheme.gradient} !important; } .ytp-swatch-background-color { background: ${selectedTheme.gradient} !important; } html, body { background-color: #0f0f0f !important; } `); /* Apply background image with theme overlay if exists */ if (localStorage.getItem('backgroundImage')) { applyPageBackground(localStorage.getItem('backgroundImage'), selectedTheme.gradient); } else { // Apply only theme gradient addDynamicCss(` html, body { background-image: ${selectedTheme.gradient} !important; background-size: cover !important; background-position: center !important; background-attachment: fixed !important; } `); } /* Minimal transparency to allow background to show */ addDynamicCss(` ytd-app, #content.ytd-app, #page-manager.ytd-app, ytd-browse, ytd-watch-flexy, ytd-two-column-browse-results-renderer, #primary.ytd-two-column-browse-results-renderer, #secondary.ytd-two-column-browse-results-renderer, ytd-rich-grid-renderer, #contents.ytd-rich-grid-renderer, ytd-item-section-renderer, ytd-comments-header-renderer, ytd-comment-simplebox-renderer, ytd-comment-thread-renderer, ytd-comment-renderer, #header.ytd-item-section-renderer, #body.ytd-comment-renderer, #author-thumbnail.ytd-comment-simplebox-renderer, #cinematic-shorts-scrim.ytd-shorts, ytd-comment-view-model, ytd-comment-engagement-bar, ytd-comment-replies-renderer, #anchored-panel.ytd-shorts, #cinematic-container.ytd-reel-video-renderer, #shorts-cinematic-container, .short-video-container.ytd-reel-video-renderer, ytd-reel-video-renderer, .navigation-container.ytd-shorts, .navigation-button.ytd-shorts { background: transparent !important; } /* Completely hide the cinematic glow in shorts if it's causing black blocks */ #cinematic-container.ytd-reel-video-renderer, #shorts-cinematic-container, #cinematic-shorts-scrim.ytd-shorts { display: none !important; opacity: 0 !important; visibility: hidden !important; } /* Use theme colors on major components without breaking layout */ #masthead-container.ytd-app, #background.ytd-masthead { background: ${selectedTheme.gradient} !important; } /* Revert chip bar to near-native but themed */ #header.ytd-rich-grid-renderer, ytd-feed-filter-chip-bar-renderer, #chips-wrapper.ytd-feed-filter-chip-bar-renderer { background: transparent !important; } /* Improve Shorts Navigation: Center 'Next' button if it's the first Short */ ytd-shorts[is-watch-while-mode] .navigation-container.ytd-shorts, .navigation-container.ytd-shorts { display: flex !important; flex-direction: column !important; justify-content: center !important; gap: 12px !important; height: 100% !important; top: 0 !important; bottom: 0 !important; margin: 0 !important; background: transparent !important; background-color: transparent !important; } .navigation-button.ytd-shorts { margin: 0 !important; } /* Ensure hidden buttons don't take up space in the flex container */ #navigation-button-up[aria-hidden="true"], #navigation-button-up[aria-hidden=""], #navigation-button-up[hidden], #navigation-button-down[aria-hidden="true"], #navigation-button-down[aria-hidden=""], #navigation-button-down[hidden] { display: none !important; } /* Restore the 'frosted-glass' look but with the theme gradient */ #frosted-glass.ytd-app { background: ${selectedTheme.gradient} !important; opacity: 0.8 !important; } ytd-engagement-panel-section-list-renderer { background: ${selectedTheme.gradient} !important; backdrop-filter: blur(12px) !important; } ytd-engagement-panel-title-header-renderer[shorts-panel] #header.ytd-engagement-panel-title-header-renderer { background: ${selectedTheme.gradient} !important;} .buttons-tranlate { background: ${selectedTheme.btnTranslate} !important; } .badge-shape-wiz--thumbnail-default { color: ${selectedTheme.videoDuration} !important; background: ${selectedTheme.gradient} !important; } #logo-icon { color: ${selectedTheme.textLogo} !important; } .yt-spec-button-shape-next--overlay.yt-spec-button-shape-next--text { color: ${selectedTheme.colorIcons} !important; } .ytd-topbar-menu-button-renderer #button.ytd-topbar-menu-button-renderer { color: ${selectedTheme.colorIcons} !important; } .yt-spec-icon-badge-shape--style-overlay .yt-spec-icon-badge-shape__icon { color: ${selectedTheme.colorIcons} !important; } .ytp-svg-fill { fill: ${selectedTheme.colorIcons} !important; } #ytp-id-30,#ytp-id-17,#ytp-id-19,#ytp-id-20{ fill: ${selectedTheme.colorIcons} !important; } `); // YTM-specific element selectors if (isYTMusic) { addDynamicCss(` html, body, ytmusic-app { background-color: ${localStorage.getItem('backgroundImage') ? 'transparent' : '#030303'} !important; background-image: ${localStorage.getItem('backgroundImage') ? 'none' : selectedTheme.gradient} !important; background-size: cover !important; background-position: center !important; background-attachment: fixed !important; } ytmusic-player-bar { background: ${localStorage.getItem('backgroundImage') ? 'transparent' : selectedTheme.gradient} !important; ${localStorage.getItem('backgroundImage') ? 'backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important;' : ''} } ytmusic-nav-bar { background: transparent !important; transition: background 0.4s ease-in-out !important; } ytmusic-nav-bar.scrolled, ytmusic-nav-bar[opened], body[player-page-open] ytmusic-nav-bar { background: ${selectedTheme.gradient} !important; ${localStorage.getItem('backgroundImage') ? 'backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important;' : ''} } ytmusic-search-box #input-box { background: ${selectedTheme.gradient} !important; } ytmusic-browse-response, ytmusic-header-renderer, ytmusic-tabbed-browse-renderer, ytmusic-detail-header-renderer, ytmusic-section-list-renderer, ytmusic-carousel-shelf-renderer, ytmusic-grid-renderer, ytmusic-item-section-renderer, #content.ytmusic-app, #shorts-container, ytd-shorts, #shorts-inner-container, ytd-reel-player-overlay, #overlay.ytd-reel-video-renderer, ytmusic-app-layout, #mini-guide-background, #guide-wrapper.ytmusic-app-layout, ytmusic-browse-response #background, ytmusic-browse-response .background, ytmusic-app-layout #background, ytmusic-immersive-header-renderer, ytmusic-card-shelf-renderer, ytmusic-chip-cloud-chip-renderer, ytmusic-chip-cloud-renderer, ytmusic-player-page, ytmusic-player-page #background { background: transparent !important; } /* Neutralize default YTM gradients */ ytmusic-browse-response #background, ytmusic-header-renderer #background, ytmusic-tabbed-browse-renderer #background, ytmusic-player-page #background, ytmusic-player-page .background, .background-gradient.ytmusic-browse-response, #background.style-scope.ytmusic-browse-response, #header.style-scope.ytmusic-browse-response, ytmusic-browse-response [id="background"], ytmusic-header-renderer [id="background"], #mini-guide-background, #guide-spacer, .immersive-background, ytmusic-fullbleed-thumbnail-renderer[is-background] { background: transparent !important; background-image: none !important; } .immersive-background, ytmusic-fullbleed-thumbnail-renderer[is-background] { display: none !important; } #layout { background: transparent !important; } .content.ytmusic-player-page { background: transparent !important; } ytmusic-player-bar .title, ytmusic-player-bar .byline { color: ${selectedTheme.textColor} !important; } .ytmusic-player-bar .yt-spec-icon-shape, .ytmusic-player-bar svg { color: ${selectedTheme.colorIcons || selectedTheme.textColor} !important; fill: ${selectedTheme.colorIcons || selectedTheme.textColor} !important; } #progress-bar { --paper-slider-active-color: ${ytmSliderSolidColor} !important; --paper-slider-knob-color: ${ytmSliderSolidColor} !important; --paper-slider-secondary-color: ${ytmSliderSolidColor}80 !important; } `); initYTMHeaderScroll(); } } else if (isDarkModeActive === 'dark' && isThemeCustom) { $sp('--yt-spec-base-background', settings.bgColorPicker); $sp('--yt-spec-text-primary', settings.primaryColorPicker); $sp('--yt-spec-text-secondary', settings.secondaryColorPicker); $sp('--yt-spec-menu-background', settings.menuColorPicker); $sp('--yt-spec-icon-inactive', settings.iconsColorPicker); $sp('--yt-spec-brand-icon-inactive', settings.primaryColorPicker); $sp('--yt-spec-brand-icon-active', settings.primaryColorPicker); $sp('--yt-spec-raised-background', settings.headerColorPicker); $sp('--yt-spec-static-brand-red', settings.lineColorPicker); $sp('--yt-spec-static-brand-white', settings.timeColorPicker); $sp('--ytd-searchbox-background', settings.primaryColorPicker); $sp('--ytd-searchbox-text-color', settings.secondaryColorPicker); $sp('--ytcp-text-primary', settings.primaryColorPicker); // YTM-specific CSS variables for custom theme if (isYTMusic) { applyYTMThemeVars( settings.bgColorPicker, settings.primaryColorPicker, settings.secondaryColorPicker, settings.menuColorPicker, settings.iconsColorPicker, settings.headerColorPicker, settings.progressbarColorPicker, settings.progressbarColorPicker + '80' // Add 50% opacity in hex ); addDynamicCss(` #progress-bar { --paper-slider-active-color: ${settings.progressbarColorPicker} !important; --paper-slider-knob-color: ${settings.progressbarColorPicker} !important; --paper-slider-secondary-color: ${settings.progressbarColorPicker}80 !important; } `); } addDynamicCss(` .html5-video-player { color: ${settings.primaryColorPicker} !important; } .ytProgressBarLineProgressBarPlayed { background: linear-gradient(to right, ${settings.progressbarColorPicker} 80%, ${settings.progressbarColorPicker} 100%); } .ytp-menuitem .ytp-menuitem-icon svg path{ fill: ${settings.iconsColorPicker} !important; } .ytThumbnailOverlayProgressBarHostWatchedProgressBarSegment { background: linear-gradient(to right, ${settings.lineColorPicker} 80%, ${settings.lineColorPicker} 100%) !important; } .yt-badge-shape--thumbnail-default { color: ${settings.timeColorPicker} !important; } a svg > path, .ytp-button svg path { fill: ${settings.iconsColorPicker} !important; } svg.path{ fill: ${settings.iconsColorPicker} !important; } svg { color: ${settings.iconsColorPicker} !important; } .ytp-volume-slider-handle:before, .ytp-volume-slider-handle, .ytp-tooltip.ytp-preview:not(.ytp-text-detail) { background-color: ${settings.iconsColorPicker} !important; } .ytp-autonav-toggle-button[aria-checked=true] { background-color: ${settings.iconsColorPicker} !important; } .tp-yt-iron-icon { fill: ${settings.iconsColorPicker} !important; } .botones_div { background-color: transparent; border: none; color: ${settings.iconsColorPicker} !important; user-select: none; } #container.ytd-searchbox { color: red !important; } .ytp-menuitem[aria-checked=true] .ytp-menuitem-toggle-checkbox { background: ${settings.primaryColorPicker} !important; } .yt-spec-icon-shape { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: ${settings.iconsColorPicker} !important; } .ytp-time-current, .ytp-time-separator, .ytp-time-duration { color: ${settings.iconsColorPicker} !important; } #background.ytd-masthead { background: ${settings.headerColorPicker} !important; } .ytp-swatch-background-color { background: ${settings.progressbarColorPicker } !important; } #shorts-container, ytd-shorts, #shorts-inner-container, #page-manager.ytd-app, #cinematic-container.ytd-reel-video-renderer, #shorts-cinematic-container, .short-video-container.ytd-reel-video-renderer, ytd-reel-video-renderer, ytd-reel-player-overlay, #overlay.ytd-reel-video-renderer { background: transparent !important; } #cinematic-container.ytd-reel-video-renderer, #shorts-cinematic-container, #cinematic-shorts-scrim.ytd-shorts { display: none !important; opacity: 0 !important; visibility: hidden !important; } ytd-engagement-panel-title-header-renderer[shorts-panel] #header.ytd-engagement-panel-title-header-renderer { background: ${settings.bgColorPicker} !important;} .badge-shape-wiz--thumbnail-default { color: ${settings.timeColorPicker} !important; background: ${settings.secondaryColorPicker} !important; } #logo-icon { color: ${settings.primaryColorPicker} !important; } .yt-spec-button-shape-next--overlay.yt-spec-button-shape-next--text { color: ${settings.iconsColorPicker} !important; } .ytd-topbar-menu-button-renderer #button.ytd-topbar-menu-button-renderer { color: ${settings.iconsColorPicker} !important; } .yt-spec-icon-badge-shape--style-overlay .yt-spec-icon-badge-shape__icon { color: ${settings.iconsColorPicker} !important; } .ytp-svg-fill { fill: ${settings.iconsColorPicker} !important; } #ytp-id-30,#ytp-id-17,#ytp-id-19,#ytp-id-20{ fill: ${settings.iconsColorPicker} !important; } `); // YTM-specific element selectors for custom theme if (isYTMusic) { addDynamicCss(` html, body, ytmusic-app { background-image: none !important; background-color: ${localStorage.getItem('backgroundImage') ? 'transparent' : settings.bgColorPicker} !important; background-size: cover !important; background-position: center !important; background-attachment: fixed !important; } ytmusic-player-bar { background: ${localStorage.getItem('backgroundImage') ? 'transparent' : (settings.headerColorPicker || settings.bgColorPicker)} !important; ${localStorage.getItem('backgroundImage') ? 'backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important;' : ''} } ytmusic-nav-bar { background: transparent !important; transition: background 0.4s ease-in-out !important; } ytmusic-nav-bar.scrolled, ytmusic-nav-bar[opened], body[player-page-open] ytmusic-nav-bar { background: ${(settings.headerColorPicker || settings.bgColorPicker)} !important; ${localStorage.getItem('backgroundImage') ? 'backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important;' : ''} } ytmusic-search-box #input-box { background: ${settings.menuColorPicker || settings.bgColorPicker} !important; } ytmusic-browse-response, ytmusic-header-renderer, ytmusic-tabbed-browse-renderer, ytmusic-detail-header-renderer, ytmusic-section-list-renderer, ytmusic-carousel-shelf-renderer, ytmusic-grid-renderer, ytmusic-item-section-renderer, #content.ytmusic-app, #shorts-container, ytd-shorts, #shorts-inner-container, ytd-reel-player-overlay, #overlay.ytd-reel-video-renderer, ytmusic-app-layout, ytmusic-app-layout.content-scrolled, #mini-guide-background, #guide-wrapper.ytmusic-app-layout, ytmusic-browse-response #background, ytmusic-browse-response .background, ytmusic-app-layout #background, ytmusic-app-layout #guide-background, ytmusic-app-layout #nav-bar-background, ytmusic-app-layout #player-bar-background, tp-yt-app-drawer, tp-yt-app-drawer #contentContainer, #mini-guide, #mini-guide-renderer, ytmusic-guide-renderer, #guide-wrapper, #guide-content, #guide-spacer, ytmusic-guide-section-renderer, ytmusic-guide-entry-renderer, tp-yt-paper-item.ytmusic-guide-entry-renderer, ytmusic-immersive-header-renderer, ytmusic-card-shelf-renderer, ytmusic-chip-cloud-chip-renderer, ytmusic-chip-cloud-renderer, ytmusic-player-page, ytmusic-player-page #background { background: transparent !important; --ytmusic-guide-background: transparent !important; --iron-drawer-background-color: transparent !important; } /* Neutralize default YTM gradients */ ytmusic-browse-response #background, ytmusic-header-renderer #background, ytmusic-tabbed-browse-renderer #background, ytmusic-player-page #background, ytmusic-player-page .background, .background-gradient.ytmusic-browse-response, #background.style-scope.ytmusic-browse-response, #header.style-scope.ytmusic-browse-response, ytmusic-browse-response [id="background"], ytmusic-header-renderer [id="background"], #mini-guide-background, #guide-spacer, .immersive-background, ytmusic-fullbleed-thumbnail-renderer[is-background] { background: transparent !important; background-image: none !important; } body:not(.ytm-ambient-active) #mini-guide-background, .immersive-background, ytmusic-fullbleed-thumbnail-renderer[is-background] { opacity: 0 !important; pointer-events: none !important; } #layout { background: transparent !important; } .content.ytmusic-player-page { background: transparent !important; } ytmusic-player-bar .title, ytmusic-player-bar .byline { color: ${settings.primaryColorPicker} !important; } .ytmusic-player-bar .yt-spec-icon-shape, .ytmusic-player-bar svg { color: ${settings.iconsColorPicker} !important; fill: ${settings.iconsColorPicker} !important; } `); initYTMHeaderScroll(); } } else { addDynamicCss(` .botones_div { background-color: transparent; border: none; color: #000 !important; user-select: none; } `); } } else { if (localStorage.getItem('backgroundImage')) { applyPageBackground(localStorage.getItem('backgroundImage'), null); } else { applyPageBackground(null); } // Cleanup theme vars when toggled off to fix stuck colors on side-panel const props = [ '--ytmusic-general-background', '--ytmusic-background', '--ytmusic-color-white1', '--ytmusic-color-white2', '--ytmusic-color-white3', '--ytmusic-color-white4', '--ytmusic-player-bar-background', '--ytmusic-search-background', '--yt-spec-general-background-a', '--yt-spec-general-background-b', '--yt-spec-general-background-c', '--yt-spec-base-background', '--yt-spec-text-primary', '--yt-spec-text-secondary', '--yt-spec-menu-background', '--yt-spec-icon-inactive', '--yt-spec-brand-icon-inactive', '--yt-spec-brand-icon-active', '--yt-spec-static-brand-red', '--yt-spec-raised-background', '--yt-spec-static-brand-white', '--ytd-searchbox-background', '--ytd-searchbox-text-color', '--ytcp-text-primary', '--ytmusic-nav-bar', '--ytmusic-nav-bar-background', '--paper-slider-active-color', '--paper-slider-knob-color', '--paper-progress-active-color', '--paper-slider-secondary-color', '--paper-progress-secondary-color' ]; props.forEach(p => document.documentElement.style.removeProperty(p)); // Restore YTM native defaults instead of leaving vars undefined if (isYTMusic) { const hasBgImage = !!localStorage.getItem('backgroundImage'); if (hasBgImage) { // When bgImage is active, force transparent vars so YTM CSS doesn't override our blur document.documentElement.style.setProperty('--ytmusic-nav-bar-background', 'transparent'); document.documentElement.style.setProperty('--ytmusic-player-bar-background', 'transparent'); } else { // No bgImage → restore YTM native defaults completely document.documentElement.style.removeProperty('--ytmusic-nav-bar-background'); document.documentElement.style.removeProperty('--ytmusic-player-bar-background'); document.documentElement.style.removeProperty('--ytmusic-nav-bar'); } // Cleanup carousel/container backgrounds that were made transparent by theme // But DON'T reset nav-bar/player-bar when backgroundImage is active (applyPageBackground handles those) addDynamicCss(` ytmusic-carousel-shelf-renderer, ytmusic-section-list-renderer, ytmusic-grid-renderer, ytmusic-card-shelf-renderer, ytmusic-chip-cloud-renderer, ytmusic-chip-cloud-chip-renderer, ytmusic-header-renderer, ytmusic-tabbed-browse-renderer, ytmusic-detail-header-renderer, ytmusic-item-section-renderer, ytmusic-immersive-header-renderer { background: ${hasBgImage ? 'transparent' : 'initial'} !important; } .immersive-background, ytmusic-fullbleed-thumbnail-renderer[is-background] { display: initial !important; } ${hasBgImage ? ` /* Theme OFF + bgImage: nav-bar/player-bar need blur background */ #nav-bar-background.ytmusic-app-layout { background: transparent !important; transition: background 0.3s ease, backdrop-filter 0.3s ease !important; } /* High specificity transparency for YTM Guide/Sidebar */ tp-yt-app-drawer, tp-yt-app-drawer #contentContainer, tp-yt-app-drawer #contentContainer.tp-yt-app-drawer, #guide-wrapper.ytmusic-app, #guide-content.ytmusic-app, #guide-spacer.ytmusic-app, #guide-renderer.ytmusic-app, ytmusic-guide-renderer, #sections.ytmusic-guide-renderer, ytmusic-guide-section-renderer, #items.ytmusic-guide-section-renderer, #divider.ytmusic-guide-section-renderer, ytmusic-app-layout.content-scrolled, ytmusic-app-layout #background, ytmusic-app-layout #guide-background, ytmusic-app-layout #player-bar-background, ytmusic-app-layout #nav-bar-background:not(.scrolled) { background: transparent !important; background-color: transparent !important; --ytmusic-guide-background: transparent !important; --iron-drawer-background-color: transparent !important; } ytmusic-nav-bar, #nav-bar-divider { background: transparent !important; border: none !important; } /* High specificity rules for scrolled state - targeting both to ensure coverage */ ytmusic-nav-bar.scrolled, #nav-bar-background.scrolled, ytmusic-nav-bar[opened], body[player-page-open] ytmusic-nav-bar, body[player-page-open] #nav-bar-background { background: rgba(10, 10, 10, 0.4) !important; backdrop-filter: blur(25px) !important; -webkit-backdrop-filter: blur(25px) !important; } ytmusic-player-bar { background: rgba(0, 0, 0, 0.2) !important; backdrop-filter: blur(30px) !important; -webkit-backdrop-filter: blur(30px) !important; border-top: 1px solid rgba(255, 255, 255, 0.05) !important; } /* Standardized YTM Glass Buttons (Edit, Menu, Play, etc.) */ button.ytSpecButtonShapeNextHost, yt-button-shape button, yt-icon-button#guide-button #button, .history-button #button { background: rgba(255, 255, 255, 0.15) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; color: #fff !important; } /* Play button specific fixes */ ytmusic-play-button-renderer { background: transparent !important; --ytmusic-play-button-background-color: transparent !important; --ytmusic-play-button-active-background-color: rgba(255, 255, 255, 0.25) !important; } ytmusic-play-button-renderer .content-wrapper { background: rgba(255, 255, 255, 0.15) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; border-radius: 50% !important; } ytmusic-play-button-renderer yt-icon, ytmusic-play-button-renderer #icon, ytmusic-play-button-renderer .icon { background: transparent !important; background-color: transparent !important; color: #fff !important; --ytmusic-play-button-icon-color: #fff !important; } /* Ensure SVGs inside are visible */ ytmusic-play-button-renderer svg { fill: #fff !important; } ` : ''} `); } else { // Remove nav-bar var only on YT (not relevant there anyway) document.documentElement.style.removeProperty('--ytmusic-nav-bar-background'); } addDynamicCss(` .botones_div { background-color: transparent; border: none; color: #ccc !important; user-select: none; } `); } } // Reverse mode and sidebar CSS (YT only selectors) if (!isYTMusic) { addDynamicCss(` #columns.style-scope.ytd-watch-flexy { flex-direction: ${settings.reverseMode ? 'row-reverse' : 'row'} !important; padding-left: ${settings.reverseMode ? '20px' : '0'} !important; } #secondary.style-scope.ytd-watch-flexy {display: ${settings.hideSidebar ? 'none' : 'block'} !important;} `); } addDynamicCss(` #icon-menu-settings { color: ${settings.iconsColorPicker} !important; } `); checkDarkMode(); // Apply dynamic CSS once per settings update setDynamicCss(dynamicCssArray.join('\n')); // Gộp tất cả CSS trong mảng và tiêm vào trang // Apply new features (safe, no heavy loops) applyBookmarksIfEnabled(settings); setupContinueWatchingFeature(settings.continueWatching); if (!isYTMusic) { scheduleLikeBarUpdate(settings, 5); setupShortsChannelNameFeature(settings.shortsChannelName); setupLockupCachedStats(); } checkForVideo(); downloadDescriptionVideo(); traductor(); function checkForVideo() { if (!settings.waveVisualizer) { cleanup(true); // Limpieza completa return; } const video = $e('video'); const miniPlayer = $e('.ytp-miniplayer-ui'); if ((video && document.location.href.includes('watch')) || miniPlayer) { // Solo si el video cambió o no está configurado if (video !== currentVideo || !isSetup) { cleanup(true); // Limpieza completa antes de crear uno nuevo setupAudioAnalyzer(video); } else if (controlPanel && video.paused === false) { showCanvas(); } } } function downloadDescriptionVideo() { if (isYTMusic) return; // YTM has no description row if (!window.location.href.includes('youtube.com/watch')) return; if ($e('#button_copy_description')) return; const containerDescription = $e('#bottom-row.style-scope.ytd-watch-metadata'); if (!containerDescription) return; const buttomHTML = ` <div id="button_copy_description" style="display: flex; justify-content: end; align-items: center;margin-top: 10px;" > <button id="copy-description" title="Copy description" class="botones_div" type="button" style="cursor: pointer;"> <i style="font-size: 20px;" class="fa-solid fa-copy"></i> </button> </div> `; containerDescription.insertAdjacentHTML('beforebegin', safeHTML(buttomHTML)); $id('copy-description').addEventListener('click', () => { const ldJson = [...$m('script[type="application/ld+json"]')]; for (let script of ldJson) { try { const data = JSON.parse(script.innerText); if (data['@type'] === 'VideoObject') { const description = `📅 Date published: ${data.uploadDate || 'No disponible'}\n` + `Author: ${data.author || 'No disponible'}\n` + `🎬 Name video: ${data.name || 'No disponible'}\n` + `🖼️ Thumbnail: ${Array.isArray(data.thumbnailUrl) ? data.thumbnailUrl.join(', ') : data.thumbnailUrl || 'No disponible'}\n` + `📝 Description: ${data.description || 'No disponible'}\n\n\n` + `🎭 Category: ${data.genre || 'No disponible'}\n`; navigator.clipboard.writeText(description); Notify('success', 'Description copied'); } } catch (e) { Notify('error', 'Error parsing JSON-LD'); } } }); } // Biến cờ phải nằm ngoài hàm để không bị reset let translatorEventBound = false; function traductor() { // Chỉ quét những comment chưa có nút dịch (dùng thuộc tính data-translated) const texts = document.querySelectorAll('#content-text:not([data-translated])'); if (texts.length === 0) return; const languages = languagesTranslate; const idiomaDestino = $id('select-languages-comments-select').value; // Tạo sẵn HTML cho dropdown ngôn ngữ để dùng chung const optionsHTML = Object.entries(languages) .map(([code, name]) => `<option value="${code}" ${code === idiomaDestino ? 'selected' : ''}>${name}</option>`) .join(''); // Gắn nút dịch vào các comment mới texts.forEach((texto) => { texto.setAttribute('data-translated', 'true'); // Đánh dấu là đã gắn nút const controlsHTML = ` <div class="traductor-container"> <button class="buttons-tranlate" data-action="translate-comment"> Translate <i class="fa-solid fa-language"></i></button> <select class="select-traductor"> ${optionsHTML} </select> </div> `; texto.insertAdjacentHTML('afterend', safeHTML(controlsHTML)); }); // Áp dụng Event Delegation: Chỉ gắn sự kiện click 1 lần duy nhất lên document if (!translatorEventBound) { translatorEventBound = true; document.addEventListener('click', (e) => { const btn = e.target.closest('.buttons-tranlate[data-action="translate-comment"]'); if (!btn) return; const container = btn.closest('.traductor-container'); const selectLang = container.querySelector('.select-traductor'); const textNode = container.previousElementSibling; // Thẻ #content-text if (!textNode || !selectLang) return; const urlLista = `?client=dict-chrome-ex&sl=auto&tl=${selectLang.value}&q=` + encodeURIComponent(textNode.textContent); btn.innerHTML = safeHTML('Translating... <i class="fa-solid fa-spinner fa-spin"></i>'); fetch(apiGoogleTranslate + urlLista) .then((response) => response.json()) .then((datos) => { textNode.textContent = datos[0][0]; btn.textContent = 'Translated'; }) .catch((err) => { console.error('Error en la traducción:', err); btn.textContent = 'Error'; }); }); } } function limpiarHTML(selector) { $m(selector).forEach((button) => button.remove()); } // === CODE TỐI ƯU MỚI THAY THẾ CHO SCROLL EVENT === (YT only) if (!isYTMusic) { let _commentIO = null; let _commentMO = null; function initSmartCommentObserver() { const commentsContainer = document.querySelector('#comments'); if (!commentsContainer) return; // Disconnect previous observers to avoid duplicates if (_commentIO) { try { _commentIO.disconnect(); } catch (e) { } _commentIO = null; } if (_commentMO) { try { _commentMO.disconnect(); } catch (e) { } _commentMO = null; } _commentIO = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { _commentMO = new MutationObserver((mutations) => { let shouldUpdate = false; for (let m of mutations) { if (m.addedNodes.length > 0) { shouldUpdate = true; break; } } if (shouldUpdate) { window.requestAnimationFrame(() => { if (settings.avatars) agregarBotonesDescarga(); if (settings.translation) traductor(); }); } }); const commentContents = document.querySelector('ytd-comments #contents'); if (commentContents) { _commentMO.observe(commentContents, { childList: true, subtree: true }); } _commentIO.disconnect(); } }); _commentIO.observe(commentsContainer); } if (!window.__ytToolsCommentNavBound) { window.__ytToolsCommentNavBound = true; document.addEventListener('yt-navigate-finish', () => { setTimeout(initSmartCommentObserver, 1500); }); } initSmartCommentObserver(); } // end if (!isYTMusic) // === KẾT THÚC CODE TỐI ƯU === // Shorts DOM observer (YT only) – guarded via __ytToolsRuntime.shortsObserver if (!isYTMusic) { const contentScrollable = $e('.anchored-panel.style-scope.ytd-shorts #contents.style-scope.ytd-item-section-renderer.style-scope.ytd-item-section-renderer'); if (contentScrollable) { // Disconnect previous Shorts observer if it exists if (__ytToolsRuntime.shortsObserver) { try { __ytToolsRuntime.shortsObserver.disconnect(); } catch (e) { } __ytToolsRuntime.shortsObserver = null; } let domTimeout; __ytToolsRuntime.shortsObserver = new MutationObserver(() => { if (domTimeout) clearTimeout(domTimeout); domTimeout = setTimeout(() => { insertButtons(); addIcon(); }, 300); }); __ytToolsRuntime.shortsObserver.observe(contentScrollable, { childList: true, subtree: true }); } } // end if (!isYTMusic) shorts observer function agregarBotonesDescarga() { const avatars = $m('#author-thumbnail-button #img.style-scope.yt-img-shadow'); avatars.forEach((img) => { if (img.parentElement.querySelector('.yt-image-avatar-download')) return; const button = $cl('button'); button.innerHTML = safeHTML('<i class="fa fa-download"></i>'); button.classList.add('yt-image-avatar-download'); button.onclick = async function () { try { const imageUrl = img.src.split('=')[0]; const response = await fetch(imageUrl); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const parentComment = img.closest('ytd-comment-thread-renderer, ytd-comment-renderer'); const nameElement = parentComment?.querySelector('#author-text'); let authorName = nameElement ? nameElement.textContent.trim() : 'avatar'; authorName = authorName.replace(/[\/\\:*?"<>|]/g, ''); const link = $cl('a'); link.href = blobUrl; link.download = `${authorName}_avatar.jpg` || 'avatar.jpg'; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); // Đợi 1s cho trình duyệt bắt đầu tải } catch (error) { console.error('Error al descargar la imagen:', error); } }; img.parentElement.style.position = 'relative'; img.parentElement.appendChild(button); }); } const redirectToClassic = () => { const videoId = window.location.pathname.split('/').pop(); const classicUrl = `https://www.youtube.com/watch?v=${videoId}`; window.open(classicUrl, '_blank'); $e('video.video-stream.html5-main-video').pause(); }; // Update the Shorts "views" button label (same bar as Classic). Call with viewCount from API/cache. function updateShortsViewsButton(videoId, viewCount) { const bar = $e('reel-action-bar-view-model'); if (!bar) return; const viewsWrap = bar.querySelector('[data-yt-tools-shorts-views]'); if (!viewsWrap) return; const labelSpan = viewsWrap.querySelector('.yt-spec-button-shape-with-label__label span, [role="text"]'); if (!labelSpan) return; labelSpan.textContent = Number.isFinite(viewCount) && viewCount >= 0 ? FormatterNumber(viewCount, 0) : '—'; } // Update the Shorts "rating" button label (rating 0–5 from API/cache, shown as e.g. "4.9"). function updateShortsRatingButton(videoId, rating) { const bar = $e('reel-action-bar-view-model'); if (!bar) return; const ratingWrap = bar.querySelector('[data-yt-tools-shorts-rating]'); if (!ratingWrap) return; const labelSpan = ratingWrap.querySelector('.yt-spec-button-shape-with-label__label span, [role="text"]'); if (!labelSpan) return; labelSpan.textContent = (Number.isFinite(rating) && rating >= 0 && rating <= 5) ? rating.toFixed(1) : '—'; } // Build one YT-style button for the reel action bar using pure DOM API (Trusted Types safe). function createReelBarButton(opts) { const wrap = document.createElement('div'); wrap.className = 'button-view-model ytSpecButtonViewModelHost'; if (opts.dataAttr) wrap.setAttribute(opts.dataAttr, '1'); const label = document.createElement('label'); label.className = 'yt-spec-button-shape-with-label ytSpecButtonShapeWithLabelHost'; const button = document.createElement('button'); button.type = 'button'; button.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-l yt-spec-button-shape-next--icon-button ytSpecButtonShapeNextHost ytSpecButtonShapeNextTonal ytSpecButtonShapeNextMono ytSpecButtonShapeNextSizeL ytSpecButtonShapeNextIconButton'; button.title = opts.title || ''; button.setAttribute('aria-label', opts.ariaLabel || ''); const iconDiv = document.createElement('div'); iconDiv.className = 'yt-spec-button-shape-next__icon'; iconDiv.setAttribute('aria-hidden', 'true'); const iconSpan = document.createElement('span'); iconSpan.className = 'yt-icon-shape ytSpecIconShapeHost'; // Use safeHTML + temp div to parse SVG (works with Trusted Types) if (opts.iconSvg) { try { const tempDiv = document.createElement('div'); tempDiv.innerHTML = safeHTML(opts.iconSvg); while (tempDiv.firstChild) { iconSpan.appendChild(tempDiv.firstChild); } } catch (e) { console.warn('[YT Tools] SVG parse error:', e); } } iconDiv.appendChild(iconSpan); button.appendChild(iconDiv); const labelDiv = document.createElement('div'); labelDiv.className = 'yt-spec-button-shape-with-label__label'; labelDiv.setAttribute('aria-hidden', 'false'); const labelSpan = document.createElement('span'); labelSpan.className = 'yt-core-attributed-string yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--text-alignment-center yt-core-attributed-string--word-wrapping'; labelSpan.setAttribute('role', 'text'); labelSpan.textContent = opts.labelText || ''; labelDiv.appendChild(labelSpan); label.appendChild(button); label.appendChild(labelDiv); wrap.appendChild(label); if (opts.onclick) button.addEventListener('click', opts.onclick); return wrap; } const eyeIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-eye"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>'; const classicIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-tv"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 9a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2l0 -9" /><path d="M16 3l-4 4l-4 -4" /></svg>'; const starIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>'; function insertReelBarButtons() { const isShortsPage = document.location.pathname.startsWith('/shorts'); const bar = $e('reel-action-bar-view-model'); if (!isShortsPage || !bar) { document.querySelectorAll('[data-yt-tools-shorts-classic], [data-yt-tools-shorts-views], [data-yt-tools-shorts-rating]').forEach(el => el.remove()); return; } if (bar.querySelector('[data-yt-tools-shorts-classic]')) return; const classicBtn = createReelBarButton({ dataAttr: 'data-yt-tools-shorts-classic', title: 'Classic mode', ariaLabel: 'Chế độ cổ điển', iconSvg: classicIconSvg, labelText: '', onclick: redirectToClassic, }); const viewsBtn = createReelBarButton({ dataAttr: 'data-yt-tools-shorts-views', title: 'Vistas', ariaLabel: 'Vistas', iconSvg: eyeIconSvg, labelText: '—', onclick: function () { }, }); const ratingBtn = createReelBarButton({ dataAttr: 'data-yt-tools-shorts-rating', title: 'Rating (likes/dislikes)', ariaLabel: 'Rating', iconSvg: starIconSvg, labelText: '—', onclick: function () { }, }); bar.insertBefore(ratingBtn, bar.firstChild); bar.insertBefore(viewsBtn, bar.firstChild); bar.insertBefore(classicBtn, bar.firstChild); const videoId = (document.location.pathname.split('/').filter(Boolean))[1]; if (videoId) { const persisted = getLikesDislikesFromPersistedCache(videoId); if (persisted && persisted.viewCount != null) updateShortsViewsButton(videoId, persisted.viewCount); if (persisted && persisted.rating != null) updateShortsRatingButton(videoId, persisted.rating); } __ytToolsRuntime.updateShortsViewsButton = updateShortsViewsButton; __ytToolsRuntime.updateShortsRatingButton = updateShortsRatingButton; } const insertButtons = () => { insertReelBarButtons(); }; const targetNode = $e('body'); if (targetNode != undefined && !isYTMusic) { const element = $e('ytd-item-section-renderer[static-comments-header] #contents'); if (element != undefined && settings.theme !== 'custom') { const observerElementDom = (elem) => { const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { element.style.background = `${selectedTheme.gradient ?? ''}`; } else { return } }) return observer.observe($e(`${elem}`)) } observerElementDom('ytd-item-section-renderer[static-comments-header] #contents') } } // Stats function formatTime(seconds) { if (isNaN(seconds)) return '0h 0m 0s'; seconds = Math.floor(seconds); const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; return `${h}h ${m}m ${s}s`; } function updateUI() { $id('total-time').textContent = formatTime(usageTime); $id('video-time').textContent = formatTime(videoTime); $id('shorts-time').textContent = formatTime(shortsTime); const maxTime = 86400; // 24 hours $id('usage-bar').style.width = `${(usageTime / maxTime) * 100}%`; $id('video-bar').style.width = `${(videoTime / maxTime) * 100}%`; $id('shorts-bar').style.width = `${(shortsTime / maxTime) * 100}%`; } function detectContentType(videoElement) { if (/\/shorts\//.test(window.location.pathname)) return 'shorts'; let parent = videoElement; while ((parent = parent.parentElement) !== null) { if (parent.classList.contains('shorts-container') || parent.classList.contains('reel-video') || parent.tagName === 'YTD-REEL-VIDEO-RENDERER') { return 'shorts'; } } if (videoElement.closest('ytd-watch-flexy') || videoElement.closest('#primary-inner')) { return 'video'; } if (videoElement.closest('ytd-thumbnail') || videoElement.closest('ytd-rich-item-renderer')) { return 'video'; } return null; } function findActiveVideo() { const videos = $m('video'); for (const video of videos) { if (!video.paused && !video.ended && video.readyState > 2) { return video; } } return null; } function updateCanvasSize() { if (canvas) { canvas.width = window.innerWidth; canvas.height = canvasHeight; } } function onWaveStyleChange(e) { waveStyle = e.target.value; const selectAppend = $id('select-wave-visualizer-select'); if (selectAppend) selectAppend.value = e.target.value; saveSettings(); } function cleanup(fullCleanup = false) { if (fullCleanup && animationId) { cancelAnimationFrame(animationId); animationId = null; } if (currentVideo) { currentVideo.removeEventListener('play', showCanvas); currentVideo.removeEventListener('pause', hideCanvas); currentVideo.removeEventListener('ended', hideCanvas); } if (fullCleanup) { if (canvas && canvas.parentNode) { canvas.parentNode.removeChild(canvas); canvas = null; ctx = null; } if (controlPanel && controlPanel.parentNode) { controlPanel.parentNode.removeChild(controlPanel); controlPanel = null; } if (source) { try { source.disconnect(); // Reconnect source directly to destination to keep audio playing // (createMediaElementSource routes ALL audio through Web Audio API) if (audioCtx && audioCtx.state !== 'closed') { source.connect(audioCtx.destination); } } catch (err) { } // Don't null source — cached on video.__ytToolsAudioSource for reuse } // Keep audioCtx running — source is connected to destination for audio passthrough if (currentVideo && currentVideo[PROCESSED_FLAG]) { delete currentVideo[PROCESSED_FLAG]; } currentVideo = null; isSetup = false; window.removeEventListener('resize', updateCanvasSize); const selectAppend = $id('select-wave-visualizer-select'); if (selectAppend) selectAppend.removeEventListener('change', onWaveStyleChange); } else { if (canvas) canvas.style.opacity = '0'; if (controlPanel) controlPanel.style.opacity = '0'; } } function createCanvasOverlay() { if (canvas) return; const parent = document.body; canvas = document.createElement('canvas'); canvas.id = 'wave-visualizer-canvas'; canvas.width = window.innerWidth; canvas.height = canvasHeight; canvas.style.position = 'fixed'; canvas.style.left = '0'; canvas.style.top = '0'; canvas.style.width = '100%'; canvas.style.pointerEvents = 'none'; canvas.style.backgroundColor = 'transparent'; canvas.style.zIndex = '10000'; canvas.style.opacity = '0'; canvas.style.transition = 'opacity 0.3s'; parent.appendChild(canvas); ctx = canvas.getContext('2d'); } function createControlPanelWave() { if (controlPanel) return; controlPanel = $cl('div'); controlPanel.id = 'wave-visualizer-control'; const selectAppend = $id('select-wave-visualizer-select'); waveStyle = settings.waveVisualizerSelected; if (selectAppend) { selectAppend.removeEventListener('change', onWaveStyleChange); selectAppend.addEventListener('change', onWaveStyleChange); } } // setting Audio y Analyser function setupAudioAnalyzer(video) { if (!video || video[PROCESSED_FLAG]) return; video[PROCESSED_FLAG] = true; cleanup(false); currentVideo = video; createCanvasOverlay(); createControlPanelWave(); // Reuse existing AudioContext if possible (suspend/resume pattern) if (!audioCtx || audioCtx.state === 'closed') { const AudioContext = window.AudioContext || window.webkitAudioContext; audioCtx = new AudioContext(); } else if (audioCtx.state === 'suspended') { audioCtx.resume(); } analyser = audioCtx.createAnalyser(); analyser.fftSize = 2048; analyser.smoothingTimeConstant = 0.85; bufferLength = analyser.fftSize; dataArray = new Uint8Array(bufferLength); smoothedData = new Array(bufferLength).fill(128); try { // Reuse cached source if video already has one (createMediaElementSource is one-shot per element) if (video.__ytToolsAudioSource) { source = video.__ytToolsAudioSource; try { source.disconnect(); } catch (e) { } } else { source = audioCtx.createMediaElementSource(video); video.__ytToolsAudioSource = source; } source.connect(analyser); analyser.connect(audioCtx.destination); } catch (e) { Notify('error', "MediaElementSource or error:", e); cleanup(true); return; } video.removeEventListener('play', showCanvas); video.removeEventListener('pause', hideCanvas); video.removeEventListener('ended', hideCanvas); video.addEventListener('play', showCanvas); video.addEventListener('pause', hideCanvas); video.addEventListener('ended', hideCanvas); window.removeEventListener('resize', updateCanvasSize); window.addEventListener('resize', updateCanvasSize); draw(); isSetup = true; } function draw() { animationId = requestAnimationFrame(draw); if (parseFloat(canvas.style.opacity) <= 0) return; analyser.getByteTimeDomainData(dataArray); for (let i = 0; i < bufferLength; i++) { smoothedData[i] += smoothingFactor * (dataArray[i] - smoothedData[i]); } ctx.clearRect(0, 0, canvas.width, canvas.height); let sliceWidth = canvas.width / bufferLength; switch (waveStyle) { case 'linea': { ctx.lineWidth = 2; ctx.strokeStyle = 'lime'; ctx.beginPath(); let x = 0; for (let i = 0; i < bufferLength; i++) { let amplitude = Math.max(0, smoothedData[i] - 128) * scale; if (i === 0) ctx.moveTo(x, amplitude); else ctx.lineTo(x, amplitude); x += sliceWidth; } ctx.stroke(); break; } case 'barras': { let x = 0; for (let i = 0; i < bufferLength; i += 5) { let amplitude = Math.max(0, smoothedData[i] - 128) * scale; ctx.fillStyle = 'cyan'; ctx.fillRect(x, 0, sliceWidth * 4, amplitude); x += sliceWidth * 5; } break; } case 'curva': { ctx.lineWidth = 2; ctx.strokeStyle = 'yellow'; ctx.beginPath(); ctx.moveTo(0, Math.max(0, smoothedData[0] - 128) * scale); for (let i = 0; i < bufferLength - 1; i++) { let x0 = i * sliceWidth; let x1 = (i + 1) * sliceWidth; let y0 = Math.max(0, smoothedData[i] - 128) * scale; let y1 = Math.max(0, smoothedData[i + 1] - 128) * scale; let cp1x = x0 + sliceWidth / 3; let cp1y = y0; let cp2x = x1 - sliceWidth / 3; let cp2y = y1; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x1, y1); } ctx.stroke(); break; } case 'picos': { ctx.fillStyle = 'magenta'; let x = 0; for (let i = 0; i < bufferLength; i += 5) { let amplitude = Math.max(0, smoothedData[i] - 128) * scale; ctx.beginPath(); ctx.arc(x, amplitude, 2, 0, Math.PI * 2); ctx.fill(); x += sliceWidth * 5; } break; } case 'solida': { ctx.beginPath(); let x = 0; ctx.moveTo(0, 0); for (let i = 0; i < bufferLength; i++) { let amplitude = Math.max(0, smoothedData[i] - 128) * scale; ctx.lineTo(x, amplitude); x += sliceWidth; } ctx.lineTo(canvas.width, 0); ctx.closePath(); ctx.fillStyle = 'rgba(0,255,0,0.3)'; ctx.fill(); break; } case 'dinamica': { let gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); gradient.addColorStop(0, 'red'); gradient.addColorStop(0.5, 'purple'); gradient.addColorStop(1, 'blue'); ctx.lineWidth = 3; ctx.strokeStyle = gradient; ctx.beginPath(); let x = 0; for (let i = 0; i < bufferLength; i++) { let amplitude = Math.max(0, smoothedData[i] - 128) * scale; if (i === 0) ctx.moveTo(x, amplitude); else ctx.lineTo(x, amplitude); x += sliceWidth; } ctx.stroke(); break; } case 'montana': { ctx.beginPath(); let x = 0; ctx.moveTo(0, 0); for (let i = 0; i < bufferLength; i++) { let amp = (smoothedData[i] - 128) * scale * 0.8; ctx.lineTo(x, amp); x += sliceWidth; } ctx.lineTo(canvas.width, 0); ctx.closePath(); ctx.fillStyle = 'rgba(128,128,255,0.4)'; ctx.fill(); break; } default: break; } } // Sử dụng các API sự kiện có sẵn của YouTube để tránh dùng MutationObserver // (chỉ gắn 1 lần, tránh leak khi applySettings chạy lại) if (!window.__ytToolsPageDataBound && !isYTMusic) { window.__ytToolsPageDataBound = true; document.addEventListener('yt-page-data-updated', () => { requestAnimationFrame(() => { if (window.location.pathname.startsWith('/shorts')) { insertButtons(); } addIcon(); }); }); } // Cập nhật thống kê thời gian xem định kỳ mà không cần MutationObserver // GM_setValue chỉ gọi mỗi 30 giây để giảm I/O, UI vẫn cập nhật mỗi 1 giây if (!__ytToolsRuntime.statsIntervalId) { let __lastStatsSave = 0; __ytToolsRuntime.statsIntervalId = setInterval(() => { const now = Date.now(); const delta = (now - lastUpdate) / 1000; const isVisible = document.visibilityState === 'visible'; if (isVisible) { usageTime += delta; } // Only do DOM query when tab is visible if (isVisible) { const activeVideoEl = document.querySelector('video.video-stream'); if (activeVideoEl && !activeVideoEl.paused && !activeVideoEl.ended) { const type = window.location.pathname.startsWith('/shorts') ? 'shorts' : 'video'; if (type === 'video') videoTime += delta; else shortsTime += delta; } } lastUpdate = now; // Chỉ lưu vào GM storage mỗi 30 giây để giảm I/O if (now - __lastStatsSave >= 30000) { __lastStatsSave = now; GM_setValue(STORAGE.USAGE, usageTime); GM_setValue(STORAGE.VIDEO, videoTime); GM_setValue(STORAGE.SHORTS, shortsTime); } if (isVisible && $id('stats')?.classList?.contains('active')) updateUI(); }, 2000); // Reduced from 1s to 2s — UI still feels responsive // Lưu ngay khi user rời trang window.addEventListener('pagehide', () => { GM_setValue(STORAGE.USAGE, usageTime); GM_setValue(STORAGE.VIDEO, videoTime); GM_setValue(STORAGE.SHORTS, shortsTime); }, { capture: true }); } // Chạy lần đầu tiên cho Shorts nếu đang ở trang Shorts if (!isYTMusic && window.location.pathname.startsWith('/shorts')) { insertButtons(); } // --- KẾT THÚC GLOBAL OBSERVER --- checkForVideo(); // retry: video element may not exist at first call (line ~5228) // [REMOVED] Duplicate stats interval was here — already handled above (line ~5924). updateUI(); // end stats if (__ytToolsRuntime.settingsLoaded) { saveSettings(); } } // Build YTM toolbar using pure DOM API (bypasses Trusted Types) const UPDATE_INTERVAL = 1000; const STORAGE = { USAGE: 'YT_TOTAL_USAGE', VIDEO: 'YT_VIDEO_TIME', SHORTS: 'YT_SHORTS_TIME' }; let usageTime = GM_getValue(STORAGE.USAGE, 0); let videoTime = GM_getValue(STORAGE.VIDEO, 0); let shortsTime = GM_getValue(STORAGE.SHORTS, 0); let lastUpdate = Date.now(); let activeVideo = null; let activeType = null; // Inicializar almacenamiento GM_setValue(STORAGE.USAGE, usageTime); GM_setValue(STORAGE.VIDEO, videoTime); GM_setValue(STORAGE.SHORTS, shortsTime); console.log('Script en ejecución by: Akari'); const HEADER_STYLE = 'color: #F00; font-size: 24px; font-family: sans-serif;'; const MESSAGE_STYLE = 'color: #00aaff; font-size: 16px; font-family: sans-serif;'; const CODE_STYLE = 'font-size: 14px; font-family: monospace;'; console.log( `%cYoutube Ultimate Tools (v${GM_info.script.version})\n` + '%cDeveloped by Akari\n' + '%c(Based on MDCM & nvbangg)', HEADER_STYLE, CODE_STYLE, MESSAGE_STYLE ); const currentVersion = GM_info.script.version; if (!localStorage.getItem('notification-Akari-' + currentVersion)) { Notify('info', 'Youtube Ultimate Tools by: Akari (v' + currentVersion + ')'); localStorage.setItem('notification-Akari-' + currentVersion, true); } // Add event listeners to all inputs const inputs = panel.querySelectorAll('input'); inputs.forEach((input) => { input.addEventListener('change', () => { try { saveSettings(); } catch (e) { console.error('saveSettings error:', e); } scheduleApplySettings(); }); if (input.type === 'range') { input.addEventListener('input', () => { updateSliderValues(); }); } }); // Some settings are controlled by <select> elements; ensure they persist and apply without duplicating listeners. function bindSelectOnce(id) { const el = $id(id); if (!el) return; if (el.dataset.ytToolsBound === '1') return; el.dataset.ytToolsBound = '1'; el.addEventListener('change', () => { // Persist immediately try { saveSettings(); } catch (e) { console.error('saveSettings error:', e); } // Apply with debounce scheduleApplySettings(); }); } bindSelectOnce('select-video-qualitys-select'); bindSelectOnce('select-languages-comments-select'); bindSelectOnce('select-wave-visualizer-select'); // Export configuration // Settings saved // const settings = GM_getValue(SETTINGS_KEY, '{}'); // $id('config-data').value = settings; $id('export-config').addEventListener('click', () => { const settings = GM_getValue(SETTINGS_KEY, '{}'); $id('config-data').value = settings; const configData = settings; try { JSON.parse(configData); // Validate JSON GM_setValue(SETTINGS_KEY, configData); setTimeout(() => { Notify('success', 'Configuration export successfully!'); }, 1000); } catch (e) { Notify('error', 'Invalid configuration data. Please check and try again.'); } }); // Import configuration $id('import-config').addEventListener('click', () => { const configData = $id('config-data').value; try { JSON.parse(configData); // Validate JSON GM_setValue(SETTINGS_KEY, configData); setTimeout(() => { Notify('success', 'Configuration imported successfully!'); window.location.reload(); }, 1000); // window.location.reload(); // removed: duplicate (setTimeout above already reloads) } catch (e) { Notify('error', 'Invalid configuration data. Please check and try again.'); } }); panel.style.display = 'none'; // var for wave // Load saved settings // Visible element DOM function checkElement(selector, callback, maxAttempts = 100) { let attempts = 0; const interval = setInterval(() => { if ($e(selector)) { clearInterval(interval); callback(); } else { attempts++; if (attempts >= maxAttempts) { clearInterval(interval); console.warn(`[Youtube Tools] Không tìm thấy element: ${selector}`); } } }, 100); } const checkActiveWave = $id('wave-visualizer-toggle'); if (checkActiveWave) { checkActiveWave.addEventListener('change', () => { const waveVisualizer = $e('#wave-visualizer-toggle'); if (waveVisualizer.checked) { Notify('success', 'Wave visualizer enabled'); saveSettings(); scheduleApplySettings(); } else { // Soft cleanup: hide canvas + stop animation, but keep AudioContext alive // (createMediaElementSource can only bind once per video element) if (animationId) { cancelAnimationFrame(animationId); animationId = null; } hideCanvas(); saveSettings(); Notify('success', 'Wave visualizer disabled'); } }); } const checkAudioOnlyTabToggle = $id('audio-only-tab-toggle'); if (checkAudioOnlyTabToggle) { checkAudioOnlyTabToggle.addEventListener('change', () => { const defaultEnabled = $id('audio-only-toggle') ? $id('audio-only-toggle').checked : false; setAudioOnlyTabOverride(checkAudioOnlyTabToggle.checked, defaultEnabled); const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); syncAudioOnlyTabCheckbox({ ...settings, audioOnly: defaultEnabled }); Notify('success', checkAudioOnlyTabToggle.checked ? 'Audio-only enabled for this tab' : 'Audio-only disabled for this tab'); scheduleApplySettings(); }); } const checkAudioOnlyToggle = $id('audio-only-toggle'); if (checkAudioOnlyToggle) { checkAudioOnlyToggle.addEventListener('change', () => { const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); syncAudioOnlyTabCheckbox({ ...settings, audioOnly: checkAudioOnlyToggle.checked }); Notify('success', checkAudioOnlyToggle.checked ? 'Audio-only mode enabled' : 'Audio-only mode disabled'); }); } const checkNonstopPlaybackToggle = $id('nonstop-playback-toggle'); if (checkNonstopPlaybackToggle) { checkNonstopPlaybackToggle.addEventListener('change', () => { Notify('success', checkNonstopPlaybackToggle.checked ? 'Nonstop playback enabled' : 'Nonstop playback disabled'); }); } // Themes toggle event listener (auto-disable ambient in YTM if themes are turned on) const checkThemesToggle = $id('themes-toggle'); if (checkThemesToggle) { checkThemesToggle.addEventListener('change', () => { if (isYTMusic && checkThemesToggle.checked) { const cinematicToggle = $id('cinematic-lighting-toggle'); if (cinematicToggle && cinematicToggle.checked) { cinematicToggle.checked = false; try { saveSettings(); } catch (e) { } scheduleApplySettings(); } } }); } // Cinematic/Ambient lighting toggle event listener const checkCinematicLighting = $id('cinematic-lighting-toggle'); if (checkCinematicLighting) { checkCinematicLighting.addEventListener('change', () => { const cinematicToggle = $e('#cinematic-lighting-toggle'); const syncToggle = $e('#sync-cinematic-toggle'); const cinematicDiv = $id('cinematics'); if (cinematicToggle.checked) { Notify('success', isYTMusic ? 'Ambient mode enabled' : 'Cinematic mode enabled'); } else { Notify('success', isYTMusic ? 'Ambient mode disabled' : 'Cinematic mode disabled'); } if (isYTMusic) { // YTM: use custom ambient mode if (cinematicToggle.checked) { // Auto-disable theme when ambient is ON (they conflict) const themesToggle = $id('themes-toggle'); if (themesToggle && themesToggle.checked) { themesToggle.checked = false; try { saveSettings(); } catch (e) { } scheduleApplySettings(); } ytmAmbientMode.show(); } else { ytmAmbientMode.destroy(); } } else { // YT: use cinematic lighting if (syncToggle.checked) { setTimeout(() => { toggleCinematicLighting(); }, 300); } else { if (cinematicDiv) { cinematicDiv.style.display = cinematicToggle.checked ? 'block' : 'none'; } } } }); } // Sync cinematic toggle event listener const checkSyncCinematic = $id('sync-cinematic-toggle'); if (checkSyncCinematic) { checkSyncCinematic.addEventListener('change', () => { const syncToggle = $e('#sync-cinematic-toggle'); const cinematicToggle = $e('#cinematic-lighting-toggle'); const cinematicDiv = $id('cinematics'); if (syncToggle.checked) { Notify('success', 'Sync with YouTube enabled'); // Si se activa la sincronización y el modo cinematic está activado, sincronizar con YouTube if (cinematicToggle.checked) { setTimeout(() => { toggleCinematicLighting(); }, 500); } } else { Notify('success', 'Sync with YouTube disabled'); // Si se desactiva la sincronización, aplicar inmediatamente el estado del toggle if (cinematicDiv) { cinematicDiv.style.display = cinematicToggle.checked ? 'block' : 'none'; } } }); } // Side Panel Style listener const checkSidePanelStyle = $id('side-panel-style-select'); if (checkSidePanelStyle) { checkSidePanelStyle.addEventListener('change', () => { saveSettings(); scheduleApplySettings(); }); } // Custom Timeline Color listener const checkCustomTimeline = $id('custom-timeline-color-toggle'); if (checkCustomTimeline) { checkCustomTimeline.addEventListener('change', () => { saveSettings(); scheduleApplySettings(); }); } // Use the correct selector depending on YouTube vs YouTube Music const topBarSelector = isYTMusic ? '#right-content' : 'ytd-topbar-menu-button-renderer'; checkElement(topBarSelector, () => { addIcon(); // ensure gear icon is created now that topbar exists loadSettings(); initializeHeaderButtons(); setTimeout(checkNewVersion, 3000); }); // validate change url SPA youtube document.addEventListener('fullscreenchange', () => { if (document.fullscreenElement !== null) { hideCanvas(); } else { showCanvas(); } }); // Wave retry: poll for <video> element after SPA navigation to a watch page function retryWaveSetupAfterNav() { const settings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); if (!settings.waveVisualizer) return; if (!document.location.href.includes('watch')) return; let waveRetries = 0; const maxWaveRetries = 20; // up to 10 seconds const waveRetryInterval = setInterval(() => { waveRetries++; const video = $e('video'); if (video && !video.paused) { clearInterval(waveRetryInterval); // Only set up if not already set up for this video if (video !== currentVideo || !isSetup) { if (typeof cleanup === 'function') { // cleanup is scoped inside applySettings, so call scheduleApplySettings // which will trigger checkForVideo scheduleApplySettings(); } } return; } if (video && video.paused) { // Wait for play event const onPlay = () => { video.removeEventListener('play', onPlay); clearInterval(waveRetryInterval); if (video !== currentVideo || !isSetup) { scheduleApplySettings(); } }; video.addEventListener('play', onPlay, { once: true }); clearInterval(waveRetryInterval); return; } if (waveRetries >= maxWaveRetries) { clearInterval(waveRetryInterval); } }, 500); } document.addEventListener('yt-navigate-finish', () => { // Re-inject gear icon if it was lost during navigation if (typeof addIcon === 'function') { addIcon(); } if (!document.location.href.includes('watch')) { hideCanvas(); } scheduleApplySettings(); // Retry wave setup after SPA navigation (video may not be ready yet) retryWaveSetupAfterNav(); const isYTSite = document.location.href.includes('youtube.com'); if (!isYTSite) return; if (isYTMusic) { // On YTM, re-enable button injection when navigating to a new song if (document.location.href.includes('watch')) { const existingContainer = $e('.yt-tools-container'); if (existingContainer) existingContainer.remove(); validoBotones = true; setTimeout(() => renderizarButtons(), 500); // Re-initialize ambient mode for the new video (if enabled) const savedSettings = JSON.parse(GM_getValue(SETTINGS_KEY, '{}')); if (savedSettings.cinematicLighting) { ytmAmbientMode.cleanup(); setTimeout(() => ytmAmbientMode.setup(), 1000); } } else { // Not on a watch page, cleanup ambient mode ytmAmbientMode.cleanup(); } } else { // Re-inject lockup stats (reduced passes to save CPU) if (document.location.href.includes('youtube.com/watch')) { [400, 1500].forEach((ms) => setTimeout(() => { injectLockupCachedStats(); injectShortsLockupCachedStats(); retargetLockupStatsObserverIfNeeded(); }, ms)); } else { // Re-inject when landing on home [400, 1800].forEach((ms) => setTimeout(() => { injectLockupCachedStats(); injectShortsLockupCachedStats(); }, ms)); } } }); GM_registerMenuCommand('Update Script by: Akari', function () { window.open('https://update.greasyfork.org/scripts/576162/YouTube%20Ultimate%20Tools.user.js', '_blank'); }); // apis for download // https://video-download-api.com // 4kdownload let ytmScrollListenerInited = false; let ytmScrollInitAttempts = 0; function initYTMHeaderScroll() { if (!isYTMusic || ytmScrollListenerInited) return; const navBar = document.querySelector('ytmusic-nav-bar'); if (!navBar) { if (ytmScrollInitAttempts < 10) { ytmScrollInitAttempts++; setTimeout(initYTMHeaderScroll, 500); } return; } ytmScrollListenerInited = true; const updateHeader = () => { const isScrolled = window.scrollY > 10; const isWatchPage = window.location.pathname.startsWith('/watch'); const isPlayerOpen = document.body.hasAttribute('player-page-open') || navBar.hasAttribute('opened') || isWatchPage; const navBarBg = document.querySelector('#nav-bar-background'); if (isScrolled || isPlayerOpen) { navBar.classList.add('scrolled'); if (navBarBg) navBarBg.classList.add('scrolled'); } else { navBar.classList.remove('scrolled'); if (navBarBg) navBarBg.classList.remove('scrolled'); } }; window.addEventListener('scroll', updateHeader, { passive: true }); window.addEventListener('popstate', updateHeader); // Initial checks to ensure it catches the state on load/refresh updateHeader(); setTimeout(updateHeader, 500); setTimeout(updateHeader, 2000); // Observe state changes const observer = new MutationObserver(updateHeader); observer.observe(document.body, { attributes: true, attributeFilter: ['player-page-open'] }); observer.observe(navBar, { attributes: true, attributeFilter: ['opened'] }); } function applyPageBackground(url, themeColor = null) { const isYTMusic = window.location.hostname === 'music.youtube.com'; const selector = isYTMusic ? 'body, ytmusic-app' : 'ytd-app, body'; const styleId = 'yt-tools-page-background'; let styleEl = $id(styleId); if (!styleEl) { styleEl = document.createElement('style'); styleEl.id = styleId; document.head.appendChild(styleEl); } if (url) { const overlayColor = themeColor || 'rgba(0,0,0,0.5)'; styleEl.textContent = ` ${selector} { background: transparent !important; background-color: transparent !important; } /* Layer 1: Blurred Background Image */ body::before { content: "" !important; position: fixed !important; top: -10px !important; left: -10px !important; width: calc(100% + 20px) !important; height: calc(100% + 20px) !important; background-image: url("${url}") !important; background-size: cover !important; background-position: center !important; background-attachment: fixed !important; background-repeat: no-repeat !important; filter: blur(8px) brightness(0.8) !important; z-index: -3 !important; pointer-events: none !important; } /* Layer 2: Theme Overlay (Semi-transparent) */ body::after { content: "" !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background: ${themeColor ? themeColor : 'rgba(0,0,0,0.5)'} !important; opacity: ${themeColor ? '0.6' : '1'} !important; z-index: -2 !important; pointer-events: none !important; } ${isYTMusic ? ` /* YTM: Elevate content above blur layers */ ytmusic-app { position: relative !important; z-index: 3 !important; } ` : ''} #content.ytmusic-app, #page-manager.ytd-app, #columns.ytd-watch-flexy, ytd-browse, ytmusic-browse-response, ytmusic-section-list-renderer, ytmusic-shelf-renderer, ytmusic-grid-renderer, ytmusic-player-page, ytmusic-app-layout, ytmusic-guide-renderer, tp-yt-app-drawer, tp-yt-app-drawer #contentContainer, tp-yt-app-drawer #contentContainer.tp-yt-app-drawer, #mini-guide, #mini-guide-renderer, #guide-wrapper, #guide-content, #guide-spacer, #guide-renderer, #sections.ytmusic-guide-renderer, ytmusic-guide-section-renderer, ytmusic-guide-entry-renderer, tp-yt-paper-item.ytmusic-guide-entry-renderer, #items.ytmusic-guide-section-renderer, #divider.ytmusic-guide-section-renderer, ytmusic-app-layout.content-scrolled, ytmusic-app-layout #background, ytmusic-app-layout #guide-background, ytmusic-app-layout #player-bar-background, ytmusic-app-layout #nav-bar-background, #contents.ytmusic-section-list-renderer, #header.ytmusic-browse-response, #guide-wrapper.ytmusic-guide-renderer, ytmusic-responsive-header-renderer, .background-gradient.ytmusic-browse-response, #content-wrapper.ytmusic-browse-response, ytmusic-carousel-shelf-renderer, .ytmusic-shelf, ytmusic-chip-cloud-renderer, ytmusic-carousel-shelf-basic-header-renderer, ytmusic-header-renderer, ytmusic-tabbed-browse-renderer, ytmusic-detail-header-renderer, ytmusic-item-section-renderer, ytmusic-immersive-header-renderer, ytmusic-card-shelf-renderer { background: transparent !important; background-color: transparent !important; background-image: none !important; --ytmusic-background: transparent !important; --ytmusic-general-background: transparent !important; --ytmusic-guide-background: transparent !important; --iron-drawer-background-color: transparent !important; --yt-spec-general-background-a: transparent !important; --yt-spec-general-background-b: transparent !important; --yt-spec-general-background-c: transparent !important; --yt-spec-menu-background: transparent !important; } ${isYTMusic ? ` /* YTM Nav Bar: transparent at top, dark blurred when scrolled */ body.ytm-style-transparent #nav-bar-background.ytmusic-app-layout, body.ytm-ambient-active #nav-bar-background.ytmusic-app-layout { background: transparent !important; transition: background 0.3s ease, backdrop-filter 0.3s ease !important; } body.ytm-style-transparent ytmusic-nav-bar, body.ytm-ambient-active ytmusic-nav-bar, body.ytm-style-transparent #nav-bar-divider, body.ytm-ambient-active #nav-bar-divider { background: transparent !important; border: none !important; transition: background 0.3s ease !important; } body.ytm-style-transparent ytmusic-nav-bar.scrolled, body.ytm-ambient-active ytmusic-nav-bar.scrolled, body.ytm-style-transparent #nav-bar-background.scrolled, body.ytm-ambient-active #nav-bar-background.scrolled, body.ytm-style-transparent ytmusic-nav-bar[opened], body.ytm-ambient-active ytmusic-nav-bar[opened], body.ytm-style-transparent[player-page-open] ytmusic-nav-bar, body.ytm-ambient-active[player-page-open] ytmusic-nav-bar, body.ytm-style-transparent[player-page-open] #nav-bar-background, body.ytm-ambient-active[player-page-open] #nav-bar-background { background: rgba(10, 10, 10, 0.4) !important; backdrop-filter: blur(25px) !important; -webkit-backdrop-filter: blur(25px) !important; } body.ytm-ambient-active[player-page-open] ytmusic-nav-bar, body.ytm-ambient-active[player-page-open] #nav-bar-background { background: transparent !important; } /* YTM Player Bar: semi-transparent with blur - respect ambient */ body.ytm-style-transparent ytmusic-player-bar, body.ytm-ambient-active ytmusic-player-bar { background: rgba(0, 0, 0, 0.2) !important; backdrop-filter: blur(30px) !important; -webkit-backdrop-filter: blur(30px) !important; border-top: 1px solid rgba(255, 255, 255, 0.05) !important; } body.ytm-ambient-active ytmusic-player-bar { background: transparent !important; border-top: 1px solid rgba(255, 255, 255, 0.1) !important; } /* YTM Sidebar (Expanded & Collapsed): Glass with separator line - respect ambient */ body.ytm-style-transparent tp-yt-app-drawer #contentContainer, body.ytm-style-transparent #mini-guide, body.ytm-style-transparent #mini-guide-renderer, body.ytm-ambient-active tp-yt-app-drawer #contentContainer, body.ytm-ambient-active #mini-guide, body.ytm-ambient-active #mini-guide-renderer { background: rgba(0, 0, 0, 0.1) !important; backdrop-filter: blur(25px) !important; -webkit-backdrop-filter: blur(25px) !important; border-right: 1px solid rgba(255, 255, 255, 0.1) !important; } body.ytm-ambient-active tp-yt-app-drawer #contentContainer, body.ytm-ambient-active #mini-guide, body.ytm-ambient-active #mini-guide-renderer { background: transparent !important; } /* Standardized YTM Glass Buttons (Edit, Menu, Play, etc.) */ body.ytm-style-transparent button.ytSpecButtonShapeNextHost, body.ytm-style-transparent yt-button-shape button, body.ytm-style-transparent .history-button #button, body.ytm-ambient-active button.ytSpecButtonShapeNextHost, body.ytm-ambient-active yt-button-shape button, body.ytm-ambient-active .history-button #button { background: rgba(255, 255, 255, 0.15) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; color: #fff !important; } /* Clean Guide Button */ yt-icon-button#guide-button, yt-icon-button#guide-button *, #guide-button, #guide-button #button, #guide-button #interaction, #guide-button yt-icon, #guide-button .yt-interaction, #guide-button .stroke.yt-interaction, #guide-button .fill.yt-interaction { background: transparent !important; background-color: transparent !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; border: none !important; box-shadow: none !important; --yt-spec-touch-response: transparent !important; --yt-spec-touch-response-inverse: transparent !important; --yt-sys-color-baseline--touch-response-inverse: transparent !important; } /* Play button specific fixes */ ytmusic-play-button-renderer { background: transparent !important; --ytmusic-play-button-background-color: transparent !important; --ytmusic-play-button-active-background-color: rgba(255, 255, 255, 0.25) !important; } ytmusic-play-button-renderer .content-wrapper { background: rgba(255, 255, 255, 0.2) !important; backdrop-filter: blur(15px) !important; -webkit-backdrop-filter: blur(15px) !important; border-radius: 50% !important; box-shadow: 0 0 10px rgba(0,0,0,0.3) !important; } ytmusic-play-button-renderer:hover .content-wrapper { background: rgba(255, 255, 255, 0.3) !important; } ytmusic-play-button-renderer yt-icon, ytmusic-play-button-renderer #icon, ytmusic-play-button-renderer .icon, ytmusic-play-button-renderer .icon.ytmusic-play-button-renderer, ytmusic-play-button-renderer yt-icon.ytmusic-play-button-renderer { background: transparent !important; background-color: transparent !important; color: #fff !important; --ytmusic-play-button-icon-color: #fff !important; opacity: 1 !important; visibility: visible !important; } /* Ensure SVGs inside are visible */ ytmusic-play-button-renderer svg { fill: #fff !important; } ` : ''} /* Engagement panels: Solid on regular YT, but NOT on Shorts */ ytd-watch-flexy ytd-engagement-panel-section-list-renderer, ytd-watch-flexy ytd-engagement-panel-section-list-renderer #content, ytd-watch-flexy ytd-engagement-panel-section-list-renderer #header, ytd-watch-flexy ytd-engagement-panel-title-header-renderer, ytd-watch-flexy ytd-engagement-panel-title-header-renderer #header, ytd-watch-flexy ytd-section-list-renderer[engagement-panel] { background: #212121 !important; background-color: #212121 !important; } /* Nuclear transparency for Shorts engagement panels to reveal theme background */ ytd-shorts #shorts-panel-container, ytd-shorts #anchored-panel, ytd-shorts ytd-engagement-panel-section-list-renderer, ytd-shorts ytd-engagement-panel-section-list-renderer #content, ytd-shorts ytd-engagement-panel-section-list-renderer #header, /* Highly specific YouTube selectors identified during debugging */ ytd-shorts ytd-engagement-panel-section-list-renderer[match-content-theme] #content, ytd-shorts ytd-engagement-panel-section-list-renderer[match-content-theme] #content.ytd-engagement-panel-section-list-renderer, ytd-shorts ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"] #content, ytd-shorts ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"] #header, ytd-shorts ytd-engagement-panel-title-header-renderer, ytd-shorts ytd-engagement-panel-title-header-renderer #header, ytd-shorts ytd-comments-header-renderer, ytd-shorts ytd-comment-thread-renderer, ytd-shorts ytd-comment-view-model, ytd-shorts ytd-item-section-renderer, ytd-shorts #sections.ytd-item-section-renderer, ytd-shorts #contents.ytd-item-section-renderer, ytd-shorts ytd-comment-simplebox-renderer { background: transparent !important; background-color: transparent !important; } /* Search button - restore default YT gray */ ytd-searchbox #search-icon-legacy, button.ytSearchboxComponentSearchButton, button.ytSearchboxComponentSearchButtonDark { background-color: #222222 !important; border: none !important; } /* Voice search button - add blur backdrop for visibility */ #voice-search-button .ytSpecButtonShapeNextHost, #voice-search-button button { background: rgba(255, 255, 255, 0.15) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; border-radius: 50% !important; } /* Hide YTM native background elements when custom background is set */ body:not(.ytm-ambient-active) #mini-guide-background, ytmusic-browse-response #background.immersive-background, ytmusic-fullbleed-thumbnail-renderer[is-background], ytmusic-player-page #background.immersive-background, #background.ytmusic-browse-response { opacity: 0 !important; pointer-events: none !important; visibility: hidden !important; } /* Hide Shorts cinematic black blocks */ #cinematic-container.ytd-reel-video-renderer, #shorts-cinematic-container, #cinematic-shorts-scrim.ytd-shorts { display: none !important; opacity: 0 !important; visibility: hidden !important; } /* Remove dark gradient overlays from Shorts */ .overlay.ytd-reel-video-renderer, ytd-reel-player-overlay-renderer, ytd-reel-player-overlay-renderer #overlay, .overlay-container.ytd-reel-player-overlay-renderer { background: transparent !important; background-image: none !important; } `; } else { styleEl.textContent = ''; } } // --- Background Image Customization --- const inputFile = $id('background_image'); const preview = $id('background-image-preview'); const removeBtn = $id('remove-background-image'); if (inputFile && preview) { // show preview const storedImage = localStorage.getItem('backgroundImage'); if (storedImage) { preview.style.backgroundImage = `url(${storedImage})`; preview.classList.add('has-image'); if (removeBtn) removeBtn.style.display = 'flex'; applyPageBackground(storedImage); } else { preview.style.backgroundImage = ''; preview.classList.remove('has-image'); if (removeBtn) removeBtn.style.display = 'none'; applyPageBackground(null); } preview.addEventListener('click', (e) => { if (e.target === removeBtn) return; inputFile.click(); }); // add background image (bind once) inputFile.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function (ev) { const dataUrl = ev.target.result; preview.style.backgroundImage = `url(${dataUrl})`; preview.classList.add('has-image'); localStorage.setItem('backgroundImage', dataUrl); if (removeBtn) removeBtn.style.display = 'flex'; applyPageBackground(dataUrl); }; reader.readAsDataURL(file); }); // Remove background image (bind once) if (removeBtn && removeBtn.dataset.ytToolsBound !== '1') { removeBtn.dataset.ytToolsBound = '1'; removeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // Only allow real user click to remove stored background if (e.isTrusted === false) return; preview.style.backgroundImage = ''; preview.classList.remove('has-image'); localStorage.removeItem('backgroundImage'); removeBtn.style.display = 'none'; applyPageBackground(null); // Force theme refresh to remove image from dynamic CSS if (typeof scheduleApplySettings === 'function') { scheduleApplySettings(); } }); } } // Nuclear fix for persistent black cinematic blocks in Shorts function nukeShortsCinematic() { if (isYTMusic) return; // 1. Remove from regular DOM const selector = '#cinematic-container.ytd-reel-video-renderer, #shorts-cinematic-container, #cinematic-shorts-scrim'; document.querySelectorAll(selector).forEach(el => el.remove()); // 2. Remove from Shadow DOMs of all reel renderers document.querySelectorAll('ytd-reel-video-renderer').forEach(reel => { if (reel.shadowRoot) { const cinematic = reel.shadowRoot.querySelector('#cinematic-container'); if (cinematic) cinematic.remove(); } }); // 3. Force transparency on engagement panels in Shorts document.querySelectorAll('ytd-engagement-panel-section-list-renderer[shorts-panel], ytd-shorts ytd-engagement-panel-section-list-renderer').forEach(panel => { const content = panel.querySelector('#content'); const header = panel.querySelector('#header'); if (content) { content.style.setProperty('background', 'transparent', 'important'); content.style.setProperty('background-color', 'transparent', 'important'); } if (header) { header.style.setProperty('background', 'transparent', 'important'); header.style.setProperty('background-color', 'transparent', 'important'); } panel.style.setProperty('background', 'transparent', 'important'); panel.style.setProperty('background-color', 'transparent', 'important'); }); // 4. Force transparency on navigation container in Shorts document.querySelectorAll('.navigation-container.ytd-shorts').forEach(nav => { nav.style.setProperty('background', 'transparent', 'important'); nav.style.setProperty('background-color', 'transparent', 'important'); }); } // Run periodically during Shorts browsing - only once if (!window.__ytToolsShortsNukeInterval) { window.__ytToolsShortsNukeInterval = setInterval(() => { if (window.location.pathname.startsWith('/shorts/')) { nukeShortsCinematic(); } }, 1500); } })();