// ==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();
})();