youtube-windowed-fullscreen/content.js

299 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);