First Commit, Code by Claude 4.7 Opus
This commit is contained in:
commit
77972c36bd
6 changed files with 319 additions and 0 deletions
299
content.js
Normal file
299
content.js
Normal 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue