First Commit, Code by Claude 4.7 Opus

This commit is contained in:
Tronax 2026-05-15 21:14:06 +02:00
commit 77972c36bd
6 changed files with 319 additions and 0 deletions

299
content.js Normal file
View file

@ -0,0 +1,299 @@
// YT Windowed Fullscreen v2.0
// Setzt eine CSS-Klasse auf <html> 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);