commit 77972c36bd265c53e203ba3885415ce1af55d606 Author: Tronax Date: Fri May 15 21:14:06 2026 +0200 First Commit, Code by Claude 4.7 Opus diff --git a/content.js b/content.js new file mode 100644 index 0000000..6b832ec --- /dev/null +++ b/content.js @@ -0,0 +1,299 @@ +// 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); diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..d8d0d50 Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..ad455b5 Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon32.png b/icons/icon32.png new file mode 100644 index 0000000..8b82997 Binary files /dev/null and b/icons/icon32.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..4a276c2 Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..9e6a530 --- /dev/null +++ b/manifest.json @@ -0,0 +1,20 @@ +{ + "manifest_version": 3, + "name": "YT Windowed Fullscreen", + "author": "Tronax mail@tronax.dev", + "version": "2.0", + "description": "Windowed Fullscreen Button – kompatibel mit altem & neuem YouTube UI", + "icons": { + "16": "icons/icon16.png", + "32": "icons/icon32.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "content_scripts": [ + { + "matches": ["https://www.youtube.com/*"], + "js": ["content.js"], + "run_at": "document_idle" + } + ] +}