From 77972c36bd265c53e203ba3885415ce1af55d606 Mon Sep 17 00:00:00 2001 From: Tronax Date: Fri, 15 May 2026 21:14:06 +0200 Subject: [PATCH] First Commit, Code by Claude 4.7 Opus --- content.js | 299 ++++++++++++++++++++++++++++++++++++++++++++++ icons/icon128.png | Bin 0 -> 2454 bytes icons/icon16.png | Bin 0 -> 329 bytes icons/icon32.png | Bin 0 -> 556 bytes icons/icon48.png | Bin 0 -> 1242 bytes manifest.json | 20 ++++ 6 files changed, 319 insertions(+) create mode 100644 content.js create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon32.png create mode 100644 icons/icon48.png create mode 100644 manifest.json 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 0000000000000000000000000000000000000000..d8d0d50a898d5d2aec655fcc9a1d88ef332f70a5 GIT binary patch literal 2454 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 Eg0+EJ=l}o! literal 0 HcmV?d00001 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" + } + ] +}