// YT Windowed Fullscreen v2.0 // Setzt eine CSS-Klasse auf und stößt danach YouTubes // Layout-Neuberechnung über ein resize-Event an. let isWindowed = false; let injected = false; // ── CSS ─────────────────────────────────────────────────────────── const style = document.createElement('style'); style.textContent = ` #yt-wfs-btn { background: none; border: none; cursor: pointer; padding: 0 4px; width: 56px !important; height: 100%; display: inline-flex; align-items: center; justify-content: center; vertical-align: top; flex-shrink: 0; opacity: 0.9; position: relative; } #yt-wfs-btn svg { width: 36px !important; height: 36px !important; } #yt-wfs-btn:hover { opacity: 1; } /* Eigener Hover-Tooltip – im Player-Container statt in der Controls-Leiste verankert, damit er nicht abgeschnitten wird. */ #yt-wfs-tooltip { position: absolute; background: rgba(28,28,28,0.92); color: #fff; font-size: 12px; font-family: "Roboto", "Arial", sans-serif; padding: 5px 9px; border-radius: 4px; white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity 0.1s; z-index: 100000; } #yt-wfs-tooltip.visible { opacity: 1; } /* ── Windowed Fullscreen aktiv ──────────────────────────────── */ html.yt-wfs-active, html.yt-wfs-active body { overflow: hidden !important; margin: 0 !important; } /* Player-Container über den ganzen Viewport */ html.yt-wfs-active #movie_player, html.yt-wfs-active .html5-video-player { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; max-width: none !important; max-height: none !important; z-index: 99999 !important; background: #000 !important; border-radius: 0 !important; margin: 0 !important; padding: 0 !important; } /* Video-Element selbst muss auch mit (YouTube setzt hier Inline-Styles) */ html.yt-wfs-active #movie_player video, html.yt-wfs-active .html5-video-container, html.yt-wfs-active .video-stream { width: 100vw !important; height: 100vh !important; top: 0 !important; left: 0 !important; transform: none !important; object-fit: contain !important; } /* Alle anderen YT-Elemente unsichtbar – nicht nur z-index drücken */ html.yt-wfs-active ytd-app > *:not(#content), html.yt-wfs-active #masthead-container, html.yt-wfs-active #guide, html.yt-wfs-active ytd-mini-guide-renderer, html.yt-wfs-active #secondary, html.yt-wfs-active #below, html.yt-wfs-active #related, html.yt-wfs-active #comments, html.yt-wfs-active ytd-watch-metadata, html.yt-wfs-active #above-the-fold { visibility: hidden !important; } `; document.head.appendChild(style); // ── SVG ─────────────────────────────────────────────────────────── // makeSvg-Funktion ersetzen (zurück zu fill): function makeSvg(d) { const NS = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(NS, 'svg'); svg.setAttribute('height', '24'); svg.setAttribute('width', '24'); svg.setAttribute('viewBox', '0 0 24 24'); const path = document.createElementNS(NS, 'path'); path.setAttribute('d', d); path.setAttribute('fill', 'white'); svg.appendChild(path); return svg; } // Pfad-Konstanten ersetzen: const SVG_ENTER = 'M3 3h7v2H5v5H3V3zm11 0h7v7h-2V5h-5V3zM3 14h2v5h5v2H3v-7zm16 5h-5v2h7v-7h-2v5z'; const SVG_EXIT = 'M5 3h2v5H2V6h3V3zm12 0h2v3h3v2h-5V3zM2 16h5v5H5v-3H2v-2zm15 0h5v2h-3v3h-2v-5z'; function setIcon(btn, d) { btn.innerHTML = ''; btn.appendChild(makeSvg(d)); } // ── Hover-Tooltip ───────────────────────────────────────────────── // Wird in den Player-Container gehängt (kein overflow:hidden), damit // er – anders als ein ::after auf der Controls-Leiste – sichtbar ist. function getTooltipText() { return isWindowed ? 'Windowed Fullscreen beenden' : 'Windowed Fullscreen'; } function showTooltip(btn) { const player = document.getElementById('movie_player'); if (!player) return; let tip = document.getElementById('yt-wfs-tooltip'); if (!tip) { tip = document.createElement('div'); tip.id = 'yt-wfs-tooltip'; player.appendChild(tip); } tip.textContent = getTooltipText(); // Position relativ zum Player berechnen: mittig über dem Button. const btnRect = btn.getBoundingClientRect(); const playerRect = player.getBoundingClientRect(); // erst sichtbar machen, damit offsetWidth/Height stimmen tip.style.left = '0px'; tip.style.top = '0px'; const tipW = tip.offsetWidth; const tipH = tip.offsetHeight; let left = btnRect.left - playerRect.left + btnRect.width / 2 - tipW / 2; // innerhalb des Players halten (8px Rand) const maxLeft = playerRect.width - tipW - 8; if (left < 8) left = 8; if (left > maxLeft) left = maxLeft; const top = btnRect.top - playerRect.top - tipH - 8; tip.style.left = left + 'px'; tip.style.top = top + 'px'; tip.classList.add('visible'); } function hideTooltip() { const tip = document.getElementById('yt-wfs-tooltip'); if (tip) tip.classList.remove('visible'); } // ── Button einfügen ─────────────────────────────────────────────── function injectButton() { if (injected) return; if (!location.pathname.startsWith('/watch')) return; if (document.getElementById('yt-wfs-btn')) { injected = true; return; } const container = document.querySelector('.ytp-right-controls-right'); if (!container) return; const btn = document.createElement('button'); btn.id = 'yt-wfs-btn'; btn.className = 'ytp-button'; // aria-label am aktuellen State ausrichten – wichtig, falls der // Watchdog den Button im aktiven Modus neu einfügt. btn.setAttribute('aria-label', getTooltipText()); setIcon(btn, isWindowed ? SVG_EXIT : SVG_ENTER); btn.addEventListener('click', toggleWindowed); btn.addEventListener('mouseenter', () => showTooltip(btn)); btn.addEventListener('mouseleave', hideTooltip); const fsBtn = container.querySelector('.ytp-fullscreen-button'); if (fsBtn) container.insertBefore(btn, fsBtn); else container.appendChild(btn); // Evtl. verwaisten Tooltip aus einer früheren Button-Instanz entfernen. const oldTip = document.getElementById('yt-wfs-tooltip'); if (oldTip) oldTip.remove(); injected = true; } // ── YouTubes Layout-Neuberechnung anstoßen ──────────────────────── // YouTube reagiert auf das resize-Event und passt die Inline-Styles // des Video-Elements an den neuen Container an. Mehrfach gefeuert, // weil YT die Verarbeitung gelegentlich debounced. function forceYtResize() { window.dispatchEvent(new Event('resize')); setTimeout(() => window.dispatchEvent(new Event('resize')), 50); setTimeout(() => window.dispatchEvent(new Event('resize')), 200); } // ── Toggle ──────────────────────────────────────────────────────── function toggleWindowed() { isWindowed = !isWindowed; const btn = document.getElementById('yt-wfs-btn'); const html = document.documentElement; if (isWindowed) { html.classList.add('yt-wfs-active'); if (btn) setIcon(btn, SVG_EXIT); } else { html.classList.remove('yt-wfs-active'); if (btn) setIcon(btn, SVG_ENTER); } if (btn) btn.setAttribute('aria-label', getTooltipText()); // Falls der Tooltip beim Klick noch sichtbar ist, Text mitziehen. const tip = document.getElementById('yt-wfs-tooltip'); if (tip && tip.classList.contains('visible')) { tip.textContent = getTooltipText(); } forceYtResize(); } // ── Escape ──────────────────────────────────────────────────────── document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isWindowed) toggleWindowed(); }); // ── Watchdog ────────────────────────────────────────────────────── // MutationObserver statt setInterval: reagiert nur, wenn YouTube die // Controls-Leiste tatsächlich verändert (z.B. Player-Reload), und // pollt nicht dauerhaft im Hintergrund. let watchdog = null; function startWatchdog() { if (watchdog) return; const target = document.querySelector('.ytp-right-controls-right') || document.getElementById('movie_player'); if (!target) return; watchdog = new MutationObserver(() => { if (!document.getElementById('yt-wfs-btn')) { injected = false; injectButton(); } }); watchdog.observe(target, { childList: true, subtree: true }); } function stopWatchdog() { if (watchdog) { watchdog.disconnect(); watchdog = null; } } // ── SPA Navigation ──────────────────────────────────────────────── function onNavigate() { injected = false; stopWatchdog(); if (isWindowed) { isWindowed = false; document.documentElement.classList.remove('yt-wfs-active'); } if (!location.pathname.startsWith('/watch')) { return; } let attempts = 0; const check = setInterval(() => { if (document.querySelector('.ytp-right-controls-right')) { clearInterval(check); injectButton(); startWatchdog(); } if (++attempts > 40) clearInterval(check); }, 200); } window.addEventListener('yt-navigate-finish', onNavigate); if (document.readyState === 'complete') onNavigate(); else window.addEventListener('load', onNavigate);