管理 YouTube 全萤幕模式切换,包含四种模式:原生、浏览器API、网页全萤幕(置中容器)、网页全萤幕(置顶容器)。仅在影片播放页面启用核心功能。
// ==UserScript==
// @name YouTube Fullscreen Manager
// @name:zh-CN YouTube 全萤幕管理器
// @name:en YouTube Fullscreen Manager
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 管理 YouTube 全螢幕模式切換,包含四種模式:原生、瀏覽器API、網頁全螢幕(置中容器)、網頁全螢幕(置頂容器)。僅在影片播放頁面啟用核心功能。
// @description:zh-CN 管理 YouTube 全萤幕模式切换,包含四种模式:原生、浏览器API、网页全萤幕(置中容器)、网页全萤幕(置顶容器)。仅在影片播放页面启用核心功能。
// @description:en Manages YouTube fullscreen switching with four modes: Native, Browser API, Web Fullscreen (Centered Container), Web Fullscreen (Top Container). Core functionality only activates on video playback pages.
// @match https://www.youtube.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const LANG = /^zh-(cn|tw|hk|mo|sg)/i.test(navigator.language) ? 'zh' : 'en';
const i18n = {
zh: {
menuFullscreenMode: '📺 設定 YouTube 全螢幕模式',
fullscreenModeOptions: {
1: '1. 原生最大化 (點擊 .ytp-fullscreen-button)',
2: '2. 原生API最大化 (toggleNativeFullscreen)',
3: '3. 網頁全螢幕 (容器置中)',
4: '4. 網頁全螢幕 (容器置頂)'
},
promptFullscreen: '選擇 YouTube 全螢幕模式:',
saveAlert: '設定已保存,需重新整理頁面後生效'
},
en: {
menuFullscreenMode: '📺 Set YouTube Fullscreen Mode',
fullscreenModeOptions: {
1: '1. Native maximization (click .ytp-fullscreen-button)',
2: '2. Native API maximization (toggleNativeFullscreen)',
3: '3. Web Fullscreen (Centered Container)',
4: '4. Web Fullscreen (Top Container)'
},
promptFullscreen: 'Select YouTube fullscreen mode:',
saveAlert: 'Settings saved. Refresh page to apply'
}
};
// 配置管理 / Configuration management
const CONFIG_STORAGE_KEY = 'YouTubeFullscreenManagerConfig';
const DEFAULT_CONFIG = {
youtubeFullscreenMode: 2 // 預設模式改為2 / Default mode changed to 2
};
const getConfig = () => {
const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {});
return { ...DEFAULT_CONFIG, ...savedConfig };
};
const saveConfig = (config) => {
const currentConfig = { ...config };
const isDefault = Object.keys(DEFAULT_CONFIG).every(key =>
currentConfig[key] === DEFAULT_CONFIG[key]
);
if (isDefault) {
GM_setValue(CONFIG_STORAGE_KEY, {});
return;
}
GM_setValue(CONFIG_STORAGE_KEY, currentConfig);
};
let CONFIG = getConfig();
// 註冊選單 / Register menu
const registerMenuCommands = () => {
const t = i18n[LANG];
GM_registerMenuCommand(t.menuFullscreenMode, handleFullscreenModeSetting);
};
const handleFullscreenModeSetting = () => {
const t = i18n[LANG];
const options = t.fullscreenModeOptions;
const choice = prompt(
`${t.promptFullscreen}\n${Object.values(options).join('\n')}`,
CONFIG.youtubeFullscreenMode
);
if (choice && options[choice]) {
CONFIG.youtubeFullscreenMode = parseInt(choice);
saveConfig(CONFIG);
alert(t.saveAlert);
}
};
// 核心功能控制變量 / Core functionality control variables
let isCoreActive = false; // 核心功能是否啟動 / Whether core functionality is active
let videoDoubleClickHandler = null; // 用於存儲雙擊處理函數 / Used to store the double-click handler
let keydownHandler = null; // 用於存儲按鍵處理函數 / Used to store the keydown handler
let mutationObserver = null; // 用於監聽DOM變更 / Used to observe DOM changes
// 狀態變量 / State variables
let isWebFullscreened = false;
let originalVideoParent = null;
let originalVideoStyles = {};
let originalParentStyles = {};
let webFullscreenContainer = null;
// 切換函數 / Toggle functions
function toggleWebFullscreen(video) {
if (!video) return;
if (isWebFullscreened) {
// 恢復原狀 / Restore original state
if (webFullscreenContainer && webFullscreenContainer.contains(video)) {
webFullscreenContainer.removeChild(video);
}
if (webFullscreenContainer && document.body.contains(webFullscreenContainer)) {
document.body.removeChild(webFullscreenContainer);
webFullscreenContainer = null;
}
if (originalVideoParent && !originalVideoParent.contains(video)) {
originalVideoParent.appendChild(video);
}
Object.assign(video.style, originalVideoStyles);
if (originalVideoParent) {
Object.assign(originalVideoParent.style, originalParentStyles);
}
isWebFullscreened = false;
originalVideoParent = null;
} else {
// 進入全螢幕 / Enter fullscreen
originalVideoParent = video.parentElement;
if (!originalVideoParent) return;
originalVideoStyles = {
position: video.style.position,
top: video.style.top,
left: video.style.left,
width: video.style.width,
height: video.style.height,
zIndex: video.style.zIndex,
objectFit: video.style.objectFit,
objectPosition: video.style.objectPosition
};
originalParentStyles = {
position: originalVideoParent.style.position,
overflow: originalVideoParent.style.overflow
};
if (!webFullscreenContainer) {
webFullscreenContainer = document.createElement('div');
webFullscreenContainer.id = 'web-fullscreen-container';
// 根據模式設定容器樣式 / Set container styles based on mode
let containerStyles;
if (CONFIG.youtubeFullscreenMode === 3) { // 模式3: 容器置中 (覆蓋整個視窗) / Mode 3: Centered container (covers entire window)
containerStyles = {
position: 'fixed', // 固定定位 / Fixed positioning
top: '0',
left: '0',
width: '100vw',
height: '100vh',
zIndex: '2147483645',
backgroundColor: 'black',
display: 'flex',
alignItems: 'center', // 垂直置中 / Center vertically
justifyContent: 'center' // 水平置中 / Center horizontally
};
} else { // 模式4: 容器置頂 / Mode 4: Top container
containerStyles = {
position: 'relative',
zIndex: '2147483645',
backgroundColor: 'black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto',
maxWidth: '100%',
maxHeight: '100vh'
};
}
Object.assign(webFullscreenContainer.style, containerStyles);
webFullscreenContainer.addEventListener('click', () => {
if (video && !video.paused) {
video.pause();
} else if (video) {
video.play().catch(() => {});
}
});
}
Object.assign(originalVideoParent.style, {
position: 'static',
overflow: 'visible'
});
originalVideoParent.removeChild(video);
webFullscreenContainer.appendChild(video);
document.body.insertBefore(webFullscreenContainer, document.body.firstChild);
// 模式3: 設定影片置中並最大化 / Mode 3: Set video to center and maximize
// 模式4: 設定影片置中並最大化 / Mode 4: Set video to center and maximize
video.style.position = '';
video.style.top = '';
video.style.left = '';
video.style.width = CONFIG.youtubeFullscreenMode === 3 ? '100%' : '100%';
video.style.height = CONFIG.youtubeFullscreenMode === 3 ? '100%' : 'auto';
video.style.maxWidth = CONFIG.youtubeFullscreenMode === 3 ? 'none' : 'none';
video.style.maxHeight = CONFIG.youtubeFullscreenMode === 3 ? 'none' : '100vh';
video.style.zIndex = '';
video.style.objectFit = 'contain'; // 保持比例並填滿容器 (模式3) 或適應容器 (模式4) / Maintain aspect ratio and fit within container (Mode 3) or adapt to container (Mode 4)
video.style.objectPosition = 'center'; // 置中 / Center
isWebFullscreened = true;
}
}
function toggleNativeFullscreen(video) {
if (!video) return;
try {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
let elementToFullscreen = video;
for (let i = 0; i < 2; i++) {
elementToFullscreen = elementToFullscreen.parentElement || elementToFullscreen;
}
elementToFullscreen.requestFullscreen?.() ||
elementToFullscreen.webkitRequestFullscreen?.() ||
elementToFullscreen.msRequestFullscreen?.() ||
video.requestFullscreen?.() ||
video.webkitRequestFullscreen?.() ||
video.msRequestFullscreen?.();
}
} catch (e) {
console.error('Fullscreen error:', e);
}
}
function toggleFullscreen(video) {
switch(CONFIG.youtubeFullscreenMode) {
case 1:
document.querySelector('.ytp-fullscreen-button')?.click();
break;
case 2:
toggleNativeFullscreen(video);
break;
case 3:
case 4: // 模式3和4都使用相同的函數,僅容器定位不同 / Mode 3 and 4 use same function, only container positioning differs
toggleWebFullscreen(video);
break;
}
}
// 雙擊處理 / Double-click handling
function setupVideoEventOverrides(video) {
if (videoDoubleClickHandler) {
video.removeEventListener('dblclick', videoDoubleClickHandler);
}
videoDoubleClickHandler = (e) => {
e.preventDefault();
e.stopPropagation();
toggleFullscreen(video);
};
video.addEventListener('dblclick', videoDoubleClickHandler);
}
// 按鍵處理 / Key handling
function handleKeyEvent(e) {
if (e.target.matches('input, textarea, select') || e.target.isContentEditable) return;
const video = document.querySelector('video, ytd-player video');
if (!video) return;
// Enter鍵切換全螢幕 / Enter key to toggle fullscreen
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
e.preventDefault();
toggleFullscreen(video);
}
}
// 綁定核心功能 / Bind core functionality
function bindCoreFeatures() {
if (isCoreActive) return; // 如果已啟動則不重複綁定 / Don't re-bind if already active
document.querySelectorAll('video').forEach(video => {
if (!video.dataset.fullscreenBound) {
setupVideoEventOverrides(video);
video.dataset.fullscreenBound = 'true';
}
});
keydownHandler = handleKeyEvent;
document.addEventListener('keydown', keydownHandler, true);
// 監聽動態內容 / Listen for dynamic content
mutationObserver = new MutationObserver(() => {
document.querySelectorAll('video').forEach(video => {
if (!video.dataset.fullscreenBound) {
setupVideoEventOverrides(video);
video.dataset.fullscreenBound = 'true';
}
});
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
isCoreActive = true;
}
// 釋放核心功能 / Release core functionality
function unbindCoreFeatures() {
if (!isCoreActive) return; // 如果未啟動則不需釋放 / Don't release if not active
document.querySelectorAll('video[data-fullscreen-bound]').forEach(video => {
if (videoDoubleClickHandler) {
video.removeEventListener('dblclick', videoDoubleClickHandler);
}
delete video.dataset.fullscreenBound;
});
if (keydownHandler) {
document.removeEventListener('keydown', keydownHandler, true);
keydownHandler = null;
}
if (mutationObserver) {
mutationObserver.disconnect();
mutationObserver = null;
}
// 退出全螢幕狀態 / Exit fullscreen state
if (isWebFullscreened) {
// 觸發一次切換以恢復原狀 / Trigger a toggle to restore original state
const video = document.querySelector('video, ytd-player video');
if (video) {
// 手動調用切換函數恢復狀態 / Manually call toggle function to restore state
if (webFullscreenContainer && webFullscreenContainer.contains(video)) {
webFullscreenContainer.removeChild(video);
}
if (webFullscreenContainer && document.body.contains(webFullscreenContainer)) {
document.body.removeChild(webFullscreenContainer);
webFullscreenContainer = null;
}
if (originalVideoParent && !originalVideoParent.contains(video)) {
originalVideoParent.appendChild(video);
}
if (video && originalVideoStyles) Object.assign(video.style, originalVideoStyles);
if (originalVideoParent && originalParentStyles) Object.assign(originalVideoParent.style, originalParentStyles);
isWebFullscreened = false;
originalVideoParent = null;
}
}
isCoreActive = false;
}
// 檢查是否為影片播放頁面 / Check if it's a video playback page
const isVideoPage = () => location.pathname.startsWith('/watch');
// 初始化 / Initialization
function init() {
registerMenuCommands();
// 初始檢查 / Initial check
if (isVideoPage()) {
bindCoreFeatures();
}
// 監聽 URL 變化 / Listen for URL changes
let currentPath = location.pathname;
const observer = new MutationObserver(() => {
if (location.pathname !== currentPath) {
currentPath = location.pathname;
if (isVideoPage()) {
bindCoreFeatures();
} else {
unbindCoreFeatures();
}
}
});
observer.observe(document, { childList: true, subtree: true });
// 監聽 popstate 事件 (瀏覽器前後按鈕) / Listen for popstate event (browser back/forward buttons)
window.addEventListener('popstate', () => {
if (isVideoPage()) {
bindCoreFeatures();
} else {
unbindCoreFeatures();
}
});
}
init();
})();