Youtube Fullscreen Mode

-

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name              Youtube Fullscreen Mode
// @name:ko           유튜브 풀스크린
// @description       -
// @description:ko    -
// @version           2025.09.23
// @author            ndaesik
// @icon              https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Youtube_shorts_icon.svg/193px-Youtube_shorts_icon.svg.png
// @match             *://*.youtube.com/*
// @grant             GM_getValue
// @grant             GM_setValue
// @grant             GM_registerMenuCommand
// @grant             GM_unregisterMenuCommand
// @namespace https://ndaesik.tistory.com/
// ==/UserScript==

(() => {
  'use strict';
  if (window.top !== window.self) return;

  const STORE_KEY = 'yt_auto_theater_enabled';
  let autoTheaterEnabled = typeof GM_getValue === 'function' ? !!GM_getValue(STORE_KEY, true) : true;
  let menuId = null;

  const scrollbarHideCSS = document.createElement('style');
  scrollbarHideCSS.dataset.tm = 'yt-fullscreen-suggest';
  scrollbarHideCSS.textContent = `
    body{overflow-y:auto;}
  `.replaceAll(';','!important;');

  const fullscreenVideoCSS = document.createElement('style');
  fullscreenVideoCSS.dataset.tm = 'yt-fullscreen-video';
  fullscreenVideoCSS.textContent = `
    ytd-app:not([guide-persistent-and-visible]) [theater] #player video,
    :is(ytd-watch-flexy[theater],ytd-watch-flexy[fullscreen]) #full-bleed-container {
      height:100vh;max-height:100vh;min-height:100vh;
    }
    ytd-watch-flexy[theater]{scrollbar-width:none;}
    ytd-watch-flexy[theater]::-webkit-scrollbar{display:none;}
    ytd-watch-flexy[theater] ~ body{scrollbar-width:none;-ms-overflow-style:none;}
    ytd-watch-flexy[theater] ~ body::-webkit-scrollbar{display:none;}
  `.replaceAll(';','!important;');

  const autoHideTopCSS = document.createElement('style');
  autoHideTopCSS.dataset.tm = 'yt-fullscreen-autohide';
  autoHideTopCSS.className = 'autoHideTopCSS';
  autoHideTopCSS.textContent = `
    #masthead-container.ytd-app:hover,#masthead-container.ytd-app:focus-within{width:100%;}
    #masthead-container.ytd-app,
    #masthead-container.ytd-app:not(:hover):not(:focus-within){width:calc(50% - 150px);}
    #masthead-container.ytd-app:not(:hover):not(:focus-within){transition:width .4s ease-out .4s;}
    ytd-app:not([guide-persistent-and-visible]) :is(#masthead-container ytd-masthead, #masthead-container.ytd-app::after){
      transform:translateY(-56px);transition:transform .1s .3s ease-out;
    }
    ytd-app:not([guide-persistent-and-visible]) :is(#masthead-container:hover ytd-masthead, #masthead-container:hover.ytd-app::after, #masthead-container:focus-within ytd-masthead){
      transform:translateY(0);
    }
    ytd-app:not([guide-persistent-and-visible]) ytd-page-manager{margin-top:0;}
  `.replaceAll(';','!important;');

  const $ = {
    els: { ytdApp: null, player: null, chatFrame: null },
    update() {
      this.els.ytdApp = document.querySelector('ytd-app');
      this.els.player = document.querySelector('#ytd-player');
      this.els.chatFrame = document.querySelector('ytd-live-chat-frame');
    }
  };

  let scrollTimer = null, isContentHidden = false;
  let observerForTheater = null;
  let listenersAttached = false;
  let listenerAbort = null;

  const isWatchPage = () => location.pathname === '/watch';
  const inTheater = () => {
    $.update();
    const { ytdApp, player, chatFrame } = $.els;
    return ytdApp && player && isWatchPage() &&
      (window.innerWidth - ytdApp.offsetWidth + player.offsetWidth +
       (chatFrame && !chatFrame.attributes.collapsed ? chatFrame.offsetWidth : 0)) === window.innerWidth;
  };

  const rafUntil = (pred, timeout = 5000) => new Promise(resolve => {
    const t0 = performance.now();
    const tick = () => {
      if (pred()) return resolve(true);
      if (performance.now() - t0 > timeout) return resolve(false);
      requestAnimationFrame(tick);
    };
    tick();
  });

  const waitForStableLayout = async (timeout = 1500) => {
    const t0 = performance.now();
    let lastH = 0, stableCount = 0;
    const okProgress = () => {
      const p = document.querySelector('yt-page-navigation-progress');
      if (!p) return true;
      if (p.hasAttribute('hidden')) return true;
      const s = getComputedStyle(p);
      return s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0';
    };
    const okPlayer = () => {
      const v = document.querySelector('#ytd-player video, #movie_player video, ytd-player video');
      return !!(v && v.readyState >= 1);
    };
    const okWatch = () => {
      const flexy = document.querySelector('ytd-watch-flexy');
      return !!flexy && (flexy.hasAttribute('theater') || flexy.hasAttribute('fullscreen') || document.querySelector('.ytp-size-button'));
    };
    while (performance.now() - t0 < timeout) {
      const h = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
      stableCount = (h === lastH) ? (stableCount + 1) : 0;
      lastH = h;
      if (stableCount >= 2 && okProgress() && okPlayer() && okWatch()) return true;
      await new Promise(r => requestAnimationFrame(r));
    }
    return false;
  };

  const forceScrollTop = async () => {
    const html = document.documentElement;
    const body = document.body;
    const prevBehavior = html.style.scrollBehavior;
    const prevAnchorHtml = html.style.overflowAnchor;
    const prevAnchorBody = body.style.overflowAnchor;
    html.style.scrollBehavior = 'auto';
    html.style.overflowAnchor = 'none';
    body.style.overflowAnchor = 'none';
    const playerRoot = document.querySelector('#full-bleed-container, #ytd-player, ytd-player');
    if (playerRoot && typeof playerRoot.scrollIntoView === 'function') {
      try { playerRoot.scrollIntoView({ block: 'start', inline: 'nearest' }); } catch(_) {}
    }
    const attempts = [0, 0, 16, 33, 66, 100, 150, 200, 250, 300, 400];
    for (const ms of attempts) {
      if (ms) await new Promise(r => setTimeout(r, ms));
      window.scrollTo(0, 0);
      void document.body.offsetHeight;
      if (window.scrollY === 0) break;
    }
    html.style.scrollBehavior = prevBehavior || '';
    html.style.overflowAnchor = prevAnchorHtml || '';
    body.style.overflowAnchor = prevAnchorBody || '';
  };

  const resetScrollTopSmart = async () => {
    try { if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; } catch(_) {}
    window.scrollTo(0,0);
    await new Promise(r => requestAnimationFrame(() => { window.scrollTo(0,0); r(); }));
    await new Promise(r => requestAnimationFrame(() => { window.scrollTo(0,0); r(); }));
    await waitForStableLayout(1500);
    await forceScrollTop();
  };

  let autoHideOn = false;

  const shouldShowAutoHideCSS = () => {
    const scrollPosition = window.scrollY;
    const viewportHeight = window.innerHeight + 56;
    return isWatchPage() && inTheater() && scrollPosition <= viewportHeight;
  };

  const updateAutoHideCSS = () => {
    const need = shouldShowAutoHideCSS();
    if (need === autoHideOn) return;
    autoHideOn = need;
    if (need) {
      if (!document.head.querySelector('style[data-tm="yt-fullscreen-autohide"]')) {
        document.head.appendChild(autoHideTopCSS);
      }
    } else {
      autoHideTopCSS.remove();
    }
  };

  const checkConditions = () => {
    if (!isWatchPage()) return;
    const watchFlexy = document.querySelector('ytd-watch-flexy');
    const primaryContent = document.querySelector('#primary');
    const secondaryContent = document.querySelector('#secondary');
    const isTheater = watchFlexy?.hasAttribute('theater');
    const isScrollTop = window.scrollY === 0;
    if (!primaryContent || !secondaryContent || !isTheater) return;
    if (isScrollTop && !isContentHidden) {
      if (scrollTimer) clearTimeout(scrollTimer);
      scrollTimer = setTimeout(() => {
        primaryContent.style.display = 'none';
        secondaryContent.style.display = 'none';
        isContentHidden = true;
      }, 2000);
    } else if (!isScrollTop && scrollTimer) {
      clearTimeout(scrollTimer);
      scrollTimer = null;
      if (isContentHidden) {
        primaryContent.style.display = '';
        secondaryContent.style.display = '';
        isContentHidden = false;
      }
    }
  };

  const showContent = () => {
    const primaryContent = document.querySelector('#primary');
    const secondaryContent = document.querySelector('#secondary');
    if (isContentHidden && primaryContent && secondaryContent) {
      primaryContent.style.display = '';
      secondaryContent.style.display = '';
      isContentHidden = false;
      if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null; }
      setTimeout(checkConditions, 1000);
    }
  };

  const attachCSS = () => {
    if (!document.head.querySelector('style[data-tm="yt-fullscreen-suggest"]')) {
      document.head.appendChild(scrollbarHideCSS);
    }
    if (!document.head.querySelector('style[data-tm="yt-fullscreen-video"]')) {
      document.head.appendChild(fullscreenVideoCSS);
    }
    updateAutoHideCSS();
  };

  const detachCSS = () => {
    scrollbarHideCSS.remove();
    fullscreenVideoCSS.remove();
    autoHideTopCSS.remove();
    observerForTheater?.disconnect();
    observerForTheater = null;
    if (listenerAbort) { listenerAbort.abort(); listenerAbort = null; }
    listenersAttached = false;
    if (isContentHidden) {
      const primaryContent = document.querySelector('#primary');
      const secondaryContent = document.querySelector('#secondary');
      if (primaryContent) primaryContent.style.display = '';
      if (secondaryContent) secondaryContent.style.display = '';
      isContentHidden = false;
    }
    if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null; }
    autoHideOn = false;
  };

  const ensureTheaterOnce = () => {
    if (!autoTheaterEnabled || !isWatchPage()) return;
    const flexy = document.querySelector('ytd-watch-flexy');
    if (!flexy || flexy.hasAttribute('theater')) return;
    const sizeBtn = document.querySelector('.ytp-size-button');
    if (sizeBtn) {
      sizeBtn.click();
      return;
    }
    try { flexy.setAttribute('theater',''); } catch(_) {};
  };

  const ensureTheaterAggressive = async () => {
    if (!autoTheaterEnabled || !isWatchPage()) return;
    await waitForStableLayout(2000);
    for (let i=0;i<4;i++){
      ensureTheaterOnce();
      const ok = document.querySelector('ytd-watch-flexy')?.hasAttribute('theater');
      if (ok) break;
      await new Promise(r=>setTimeout(r, 150 + i*150));
    }
  };

  const setupEventListeners = () => {
    if (listenersAttached) return;
    listenersAttached = true;
    listenerAbort = new AbortController();
    const sig = listenerAbort.signal;

    window.addEventListener('scroll', () => requestAnimationFrame(() => {
      updateAutoHideCSS();
      checkConditions();
    }), { passive: true, signal: sig });

    window.addEventListener('click', () => {
      setTimeout(updateAutoHideCSS, 100);
      requestAnimationFrame(showContent);
    }, { passive: true, signal: sig });

    window.addEventListener('wheel', () => requestAnimationFrame(showContent), { passive: true, signal: sig });

    const watchFlexyInit = () => {
      const watchFlexy = document.querySelector('ytd-watch-flexy');
      if (!watchFlexy) return;
      observerForTheater?.disconnect();
      observerForTheater = new MutationObserver(() => requestAnimationFrame(() => {
        checkConditions();
        ensureTheaterOnce();
      }));
      observerForTheater.observe(watchFlexy, { attributes: true, attributeFilter: ['theater'] });
    };
    new MutationObserver(watchFlexyInit).observe(document.documentElement, { childList: true, subtree: true });
    watchFlexyInit();

    const resync = () => { resetScrollTopSmart(); ensureTheaterAggressive(); };
    window.addEventListener('yt-navigate-finish', resync, { passive: true, signal: sig });
    window.addEventListener('yt-navigate-cache-restored', resync, { passive: true, signal: sig });
    window.addEventListener('yt-page-data-updated', resync, { passive: true, signal: sig });
    window.addEventListener('ytd-player-updated', resync, { passive: true, signal: sig });
  };

  const URL_EVENT = 'tm-url-change';
  const fireUrlEvent = () => window.dispatchEvent(new Event(URL_EVENT));

  const _pushState = history.pushState;
  const _replaceState = history.replaceState;
  history.pushState = function(...args) { const r = _pushState.apply(this, args); fireUrlEvent(); return r; };
  history.replaceState = function(...args) { const r = _replaceState.apply(this, args); fireUrlEvent(); return r; };

  window.addEventListener('popstate', fireUrlEvent);
  window.addEventListener('hashchange', fireUrlEvent);
  window.addEventListener('yt-navigate-finish', fireUrlEvent);
  window.addEventListener('yt-navigate-start', fireUrlEvent);

  let lastPath = location.pathname + location.search;

  const onRoute = () => {
    const now = location.pathname + location.search;
    if (now === lastPath) return;
    lastPath = now;
    if (isWatchPage()) {
      attachCSS();
      setupEventListeners();
      resetScrollTopSmart();
      requestAnimationFrame(() => {
        updateAutoHideCSS();
        checkConditions();
        ensureTheaterAggressive();
      });
      setTimeout(() => { if (window.scrollY !== 0) resetScrollTopSmart(); }, 500);
    } else {
      detachCSS();
    }
  };

  const attachMenu = () => {
    try {
      if (typeof GM_unregisterMenuCommand === 'function' && menuId) {
        GM_unregisterMenuCommand(menuId);
        menuId = null;
      }
      if (typeof GM_registerMenuCommand === 'function') {
        const label = `Auto Theater Mode: ${autoTheaterEnabled ? 'On' : 'Off'}`;
        menuId = GM_registerMenuCommand(label, () => toggleAutoTheater());
      }
    } catch(_) {}
  };

  const persistState = () => {
    try { if (typeof GM_setValue === 'function') GM_setValue(STORE_KEY, autoTheaterEnabled); } catch(_){}
  };

  const toggleAutoTheater = () => {
    autoTheaterEnabled = !autoTheaterEnabled;
    persistState();
    attachMenu();
    if (autoTheaterEnabled) ensureTheaterAggressive();
  };

  const boot = () => {
    attachMenu();
    if (isWatchPage()) {
      attachCSS();
      setupEventListeners();
      resetScrollTopSmart();
      requestAnimationFrame(() => {
        updateAutoHideCSS();
        checkConditions();
        ensureTheaterAggressive();
      });
      setTimeout(() => { if (window.scrollY !== 0) resetScrollTopSmart(); }, 500);
    } else {
      detachCSS();
    }
  };

  window.addEventListener(URL_EVENT, onRoute);
  boot();
})();