您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
-
当前为
// ==UserScript== // @name Youtube Fullscreen Mode // @name:ko 유튜브 풀스크린 // @description - // @description:ko - // @version 2025.09.11 // @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 none // @namespace https://ndaesik.tistory.com/ // ==/UserScript== (() => { 'use strict'; // 최상위 문서만 (embed/iframe 차단) if (window.top !== window.self) return; // ---------- CSS 정의 (노드 재사용) ---------- const suggestBoxToDarkCSS = document.createElement('style'); suggestBoxToDarkCSS.dataset.tm = 'yt-fullscreen-suggest'; suggestBoxToDarkCSS.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; // (3) 리스너 수명관리용 AbortController 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 resetScrollTopHard = () => { try { if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; } catch (_) {} // 1) 즉시 window.scrollTo(0, 0); // 2) 첫 rAF requestAnimationFrame(() => { window.scrollTo(0, 0); // 3) 두 번째 rAF requestAnimationFrame(() => { window.scrollTo(0, 0); }); }); // 4) 레이아웃/광고/플레이어 로딩 후 혹시 모를 밀림 보정 setTimeout(() => window.scrollTo(0, 0), 1000); }; // (4) autoHide 상태 캐싱 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; // 상태 변화 없으면 skip 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); } }; // ---------- CSS attach/detach ---------- const attachCSS = () => { // 이미 붙어 있으면 skip if (!document.head.querySelector('style[data-tm="yt-fullscreen-suggest"]')) { document.head.appendChild(suggestBoxToDarkCSS); } if (!document.head.querySelector('style[data-tm="yt-fullscreen-video"]')) { document.head.appendChild(fullscreenVideoCSS); } updateAutoHideCSS(); }; const detachCSS = () => { // CSS 회수 suggestBoxToDarkCSS.remove(); fullscreenVideoCSS.remove(); autoHideTopCSS.remove(); // (3) 옵저버 종료 observerForTheater?.disconnect(); observerForTheater = null; // (3) 리스너 종료 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; } // (4) 캐시 초기화 autoHideOn = false; }; // ---------- 이벤트 바인딩 (중복 방지) ---------- const setupEventListeners = () => { if (listenersAttached) return; listenersAttached = true; // (3) AbortController 기반 일괄 수명관리 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 }); // theater 속성 변화 감시 (현 구조 유지) const watchFlexyInit = () => { const watchFlexy = document.querySelector('ytd-watch-flexy'); if (!watchFlexy) return; observerForTheater?.disconnect(); observerForTheater = new MutationObserver(() => requestAnimationFrame(checkConditions)); observerForTheater.observe(watchFlexy, { attributes: true, attributeFilter: ['theater'] }); }; new MutationObserver(watchFlexyInit).observe(document.documentElement, { childList: true, subtree: true }); watchFlexyInit(); }; // ---------- URL 감시 (SPA 지원) ---------- const URL_EVENT = 'tm-url-change'; const fireUrlEvent = () => window.dispatchEvent(new Event(URL_EVENT)); // history API 패치 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); // YouTube 자체 이벤트도 훅 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(); resetScrollTopHard(); requestAnimationFrame(() => { updateAutoHideCSS(); checkConditions(); }); } else { detachCSS(); } }; // 초기 1회 실행 const boot = () => { if (isWatchPage()) { attachCSS(); setupEventListeners(); resetScrollTopHard(); requestAnimationFrame(() => { updateAutoHideCSS(); checkConditions(); }); } else { detachCSS(); } }; // URL 변화 감지 핸들러 등록 window.addEventListener(URL_EVENT, onRoute); // 첫 로드 boot(); })();