🏠 返回首頁 

Greasy Fork is available in English.

Better Theater Mode for YouTube

Improves YouTube's theater mode with a Twitch.tv-like design, enhancing video and chat layouts, while maintaining performance and compatibility. Also fixes the broken fullscreen UI from the recent YouTube update.


Установить этот скрипт?
Рекомендуемый автором скрипт

Вам также может понравится Youtube HD Premium.


Установить этот скрипт
// ==UserScript==
// @name                Better Theater Mode for YouTube
// @name:zh-TW          更佳 YouTube 劇場模式
// @name:zh-CN          更佳 YouTube 剧场模式
// @name:ja             より良いYouTubeシアターモード
// @icon                https://www.youtube.com/img/favicon_48.png
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_better_theater_mode_namespace
// @version             1.8.1
// @match               *://www.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @grant               GM.getValue
// @grant               GM.setValue
// @grant               GM.deleteValue
// @grant               GM.listValues
// @grant               GM.registerMenuCommand
// @grant               GM.unregisterMenuCommand
// @grant               GM.notification
// @license             MIT
// @description         Improves YouTube's theater mode with a Twitch.tv-like design, enhancing video and chat layouts, while maintaining performance and compatibility. Also fixes the broken fullscreen UI from the recent YouTube update.
// @description:zh-TW   改善 YouTube 劇場模式,參考 Twitch.tv 的設計,增強影片與聊天室佈局,同時維持效能與相容性。並修復近期 YouTube 更新中損壞的全螢幕介面。
// @description:zh-CN   改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性。并修复近期 YouTube 更新中损坏的全屏界面。
// @description:ja      YouTubeのシアターモードを改善し、Twitch.tvのデザインを参考にして、動画とチャットのレイアウトを強化しつつ、パフォーマンスと互換性を維持します。また、最近のYouTubeアップデートによる壊れたフルスクリーンUIを修正します。
// ==/UserScript==
/*jshint esversion: 11 */
(function () {
"use strict";
// -------------------------------
// Default settings for storage under the "settings" key.
// Blacklist is now stored separately.
// -------------------------------
const DEFAULT_SETTINGS = {
isScriptActive: true,
isSimpleMode: true,
enableOnlyForLiveStreams: false,
modifyVideoPlayer: true,
modifyChat: true,
setLowHeadmast: false
};
const DEFAULT_BLACKLIST = []; // default blacklist (empty)
// -------------------------------
// Other constants and translations
// -------------------------------
const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage;
const GET_PREFERRED_LANGUAGE = () => {
if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW') {
return 'zh-CN';
} else {
return BROWSER_LANGUAGE;
}
};
const TRANSLATIONS = {
'en-US': {
tampermonkeyOutdatedAlertMessage: "It looks like you're using an older version of Tampermonkey that might cause menu issues. For the best experience, please update to version 5.4.6224 or later.",
turnOn: 'Turn On',
turnOff: 'Turn Off',
livestreamOnlyMode: 'Livestream Only Mode',
applyChatStyles: 'Apply Chat Styles',
applyVideoPlayerStyles: 'Apply Video Player Styles',
moveHeadmastBelowVideoPlayer: 'Move Headmast Below Video Player',
blacklistVideo: 'Blacklist Video',
unblacklistVideo: 'Unblacklist Video',
simpleMode: 'Simple Mode',
advancedMode: 'Advanced Mode',
debug: 'DEBUG'
},
'zh-TW': {
tampermonkeyOutdatedAlertMessage: "看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 5.4.6224 或更高版本。",
turnOn: '開啟',
turnOff: '關閉',
livestreamOnlyMode: '僅限直播模式',
applyChatStyles: '套用聊天樣式',
applyVideoPlayerStyles: '套用影片播放器樣式',
moveHeadmastBelowVideoPlayer: '將頁首橫幅移到影片播放器下方',
blacklistVideo: '將影片加入黑名單',
unblacklistVideo: '從黑名單中移除影片',
simpleMode: '簡易模式',
advancedMode: '進階模式',
debug: '偵錯'
},
'zh-CN': {
tampermonkeyOutdatedAlertMessage: "看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 5.4.6224 或更高版本。",
turnOn: '开启',
turnOff: '关闭',
livestreamOnlyMode: '仅限直播模式',
applyChatStyles: '应用聊天样式',
applyVideoPlayerStyles: '应用视频播放器样式',
moveHeadmastBelowVideoPlayer: '将页首横幅移动到视频播放器下方',
blacklistVideo: '将视频加入黑名单',
unblacklistVideo: '从黑名单中移除视频',
simpleMode: '简易模式',
advancedMode: '高级模式',
debug: '调试'
},
'ja': {
tampermonkeyOutdatedAlertMessage: "ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン5.4.6224以上に更新してください。",
turnOn: "オンにする",
turnOff: "オフにする",
livestreamOnlyMode: "ライブ配信専用モード",
applyChatStyles: "チャットスタイルを適用",
applyVideoPlayerStyles: "ビデオプレイヤースタイルを適用",
moveHeadmastBelowVideoPlayer: "ヘッドマストをビデオプレイヤーの下に移動",
blacklistVideo: "動画をブラックリストに追加",
unblacklistVideo: "ブラックリストから動画を解除",
simpleMode: "シンプルモード",
advancedMode: "高度モード",
debug: "デバッグ"
}
};
const GET_LOCALIZED_TEXT = () => {
const language = GET_PREFERRED_LANGUAGE();
return TRANSLATIONS[language] || TRANSLATIONS['en-US'];
};
// -------------------------------
// Global variables for dynamic state
// -------------------------------
let userSettings = { ...DEFAULT_SETTINGS };
let blacklist = new Set();
let userSettingsBackup = { ...DEFAULT_SETTINGS };
let useCompatibilityMode = false;
let menuItems = new Set();
let activeStyles = new Map();
let resizeObserver;
let moviePlayer;
let videoId;
let chatFrame;
let currentPageType = '';
let isFullscreen = false;
let isTheaterMode = false;
let chatCollapsed = true;
let isLiveStream = false;
let chatWidth = 0;
let moviePlayerHeight = 0;
let isOldTampermonkey = false;
const updatedVersions = {
Tampermonkey: '5.4.624'
};
let isScriptRecentlyUpdated = false;
// -------------------------------
// Greasemonkey API Compatibility Layer
// -------------------------------
// (Note: We no longer use GM for adding styles; style insertion is done by our custom function.)
const GMCustomRegisterMenuCommand = useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand;
const GMCustomUnregisterMenuCommand = useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand;
const GMCustomGetValue = useCompatibilityMode ? GM_getValue : GM.getValue;
const GMCustomSetValue = useCompatibilityMode ? GM_setValue : GM.setValue;
const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues;
const GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue;
const GMCustomNotification = useCompatibilityMode ? GM_notification : GM.notification;
// -------------------------------
// (Existing) Style Rules & Functions
// -------------------------------
const styleRules = {
chatStyle: {
id: "chatStyle",
getRule: () => `
ytd-live-chat-frame[theater-watch-while][rounded-container] {
border-radius: 0 !important;
border-top: 0 !important;
}
ytd-watch-flexy[fixed-panels] #chat.ytd-watch-flexy {
top: 0 !important;
border-top: 0 !important;
border-bottom: 0 !important;
}
`,
},
videoPlayerStyle: {
id: "videoPlayerStyle",
getRule: () => `
ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
max-height: calc(100vh - var(--ytd-watch-flexy-masthead-height)) !important;
}
`,
},
headmastStyle: {
id: "headmastStyle",
getRule: () => `
#masthead-container.ytd-app {
max-width: calc(100% - ${chatWidth}px) !important;
}
`,
},
lowHeadmastStyle: {
id: "lowHeadmastStyle",
getRule: () => `
#page-manager.ytd-app {
margin-top: 0 !important;
top: calc(-1 * var(--ytd-toolbar-offset)) !important;
position: relative !important;
}
ytd-watch-flexy[flexy]:not([full-bleed-player][full-bleed-no-max-width-columns]) #columns.ytd-watch-flexy {
margin-top: var(--ytd-toolbar-offset) !important;
}
${userSettings.modifyVideoPlayer ? `
ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
max-height: 100vh !important;
}
` : ''}
#masthead-container.ytd-app {
z-index: 599 !important;
top: ${moviePlayerHeight}px !important;
position: relative !important;
}
`,
},
videoPlayerFixStyle: {
id: "staticVideoPlayerFixStyle",
getRule: () => `
.html5-video-container {
top: -1px !important;
}
#skip-navigation.ytd-masthead {
left: -500px;
}
`,
},
chatFrameFixStyle: {
id: "staticChatFrameFixStyle",
getRule: () => {
const chatInputContainer = document.querySelector("tp-yt-iron-pages#panel-pages.style-scope.yt-live-chat-renderer");
const shouldHideChatInputContainerTopBorder = chatInputContainer?.clientHeight === 0;
const borderTopStyle = shouldHideChatInputContainerTopBorder ? 'border-top: 0 !important;' : '';
return `
#panel-pages.yt-live-chat-renderer {
${borderTopStyle}
border-bottom: 0 !important;
}
`;
},
},
chatRendererFixStyle: {
id: "staticChatRendererFixStyle",
getRule: () => `
ytd-live-chat-frame[theater-watch-while][rounded-container] {
border-bottom: 0 !important;
}
`,
},
};
function removeStyle(style) {
if (!activeStyles.has(style.id)) return;
const { element: styleElement } = activeStyles.get(style.id);
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
activeStyles.delete(style.id);
}
function removeAllStyles() {
activeStyles.forEach((styleData, styleId) => {
if (!styleData.persistent) {
removeStyle({ id: styleId });
}
});
}
// Use our custom style insertion helper.
function applyStyle(style, setPersistent = false) {
if (typeof style.getRule !== 'function') return;
if (activeStyles.has(style.id)) removeStyle(style);
const styleElement = addStyleHelper(style.getRule());
activeStyles.set(style.id, { element: styleElement, persistent: setPersistent });
function addStyleHelper(css) {
const head = document.head || document.documentElement;
const styleElem = document.createElement('style');
styleElem.type = 'text/css';
styleElem.textContent = css;
head.appendChild(styleElem);
return styleElem;
}
}
function setStyleState(style, on = true) {
on ? applyStyle(style) : removeStyle(style);
}
function updateLowHeadmastStyle() {
if (!moviePlayer) return;
const shouldApplyLowHeadmast = userSettings.setLowHeadmast && isTheaterMode && !isFullscreen && currentPageType === 'watch';
setStyleState(styleRules.lowHeadmastStyle, shouldApplyLowHeadmast);
}
function updateHeadmastStyle() {
updateLowHeadmastStyle();
let shouldShrinkHeadmast = isTheaterMode &&
chatFrame?.getAttribute('theater-watch-while') === '' &&
(userSettings.setLowHeadmast || userSettings.modifyChat);
chatWidth = chatFrame?.offsetWidth || 0;
setStyleState(styleRules.headmastStyle, shouldShrinkHeadmast);
}
function updateStyles() {
try {
const shouldNotActivate =
!userSettings.isScriptActive ||
(blacklist && blacklist.has(videoId)) ||
(userSettings.enableOnlyForLiveStreams && !isLiveStream);
if (shouldNotActivate) {
removeAllStyles();
if (moviePlayer) moviePlayer.setCenterCrop(); // trigger update for html5 video element
return;
}
setStyleState(styleRules.chatStyle, userSettings.modifyChat);
setStyleState(styleRules.videoPlayerStyle, userSettings.modifyVideoPlayer);
updateHeadmastStyle();
if (moviePlayer) moviePlayer.setCenterCrop();
} catch (error) {
console.log(`Error when trying to update styles: ${error}.`);
}
}
function updateFullscreenStatus() {
isFullscreen = !!document.fullscreenElement;
}
function updateTheaterStatus(event) {
isTheaterMode = !!event?.detail?.enabled;
updateStyles();
}
function updateChatStatus(event) {
chatFrame = event.target;
chatCollapsed = event.detail !== false;
window.addEventListener('player-api-ready', () => { updateStyles(); }, { once: true });
}
function updateMoviePlayer() {
const newMoviePlayer = document.querySelector('#movie_player');
if (!resizeObserver) {
resizeObserver = new ResizeObserver(entries => {
moviePlayerHeight = moviePlayer.offsetHeight;
updateStyles();
});
}
if (moviePlayer) resizeObserver.unobserve(moviePlayer);
moviePlayer = newMoviePlayer;
if (moviePlayer) resizeObserver.observe(moviePlayer);
}
function updateVideoStatus(event) {
try {
currentPageType = event.detail.pageData.page;
videoId = event.detail.pageData.playerResponse.videoDetails.videoId;
updateMoviePlayer();
isLiveStream = event.detail.pageData.playerResponse.videoDetails.isLiveContent;
showMenuOptions();
} catch (error) {
throw ("Failed to update video status due to this error. Error: " + error);
}
}
// -------------------------------
// New Storage Helper Functions
// -------------------------------
async function loadUserSettings() {
try {
userSettings = await GMCustomGetValue('settings', DEFAULT_SETTINGS);
console.log(`Loaded user settings: ${JSON.stringify(userSettings)}`);
} catch (error) {
throw `Error loading user settings: ${error}. Aborting script.`;
}
}
async function updateSetting(key, value) {
try {
let currentSettings = await GMCustomGetValue('settings', DEFAULT_SETTINGS);
currentSettings[key] = value;
await GMCustomSetValue('settings', currentSettings);
} catch (error) {
console.log("Error updating setting: " + error);
}
}
async function loadBlacklist() {
try {
let storedBlacklist = await GMCustomGetValue('blacklist', DEFAULT_BLACKLIST);
if (Array.isArray(storedBlacklist)) {
blacklist = new Set(storedBlacklist);
} else {
blacklist = new Set();
}
console.log(`Loaded blacklist: ${JSON.stringify(Array.from(blacklist))}`);
} catch (error) {
throw `Error loading blacklist: ${error}. Aborting script.`;
}
}
async function updateBlacklist() {
try {
await GMCustomSetValue('blacklist', Array.from(blacklist));
} catch (error) {
console.log("Error updating blacklist: " + error);
}
}
async function updateScriptInfo() {
try {
const oldScriptInfo = await GMCustomGetValue('scriptInfo', null);
console.log(`Previous script info: ${JSON.stringify(oldScriptInfo)}`);
const newScriptInfo = {
version: getScriptVersionFromMeta(),
};
await GMCustomSetValue('scriptInfo', newScriptInfo);
console.log(`Updated script info: ${JSON.stringify(newScriptInfo)}`);
if (!oldScriptInfo || compareVersions(newScriptInfo.version, oldScriptInfo?.version) !== 0) {
isScriptRecentlyUpdated = true;
}
} catch (error) {
console.log("Error updating script info: " + error);
}
}
async function cleanupOldStorage() {
try {
const allowedKeys = ['settings', 'scriptInfo', 'blacklist'];
const keys = await GMCustomListValues();
for (const key of keys) {
if (!allowedKeys.includes(key)) {
await GMCustomDeleteValue(key);
console.log(`Deleted leftover key: ${key}`);
}
}
} catch (error) {
console.log("Error cleaning up old storage keys: " + error);
}
}
function getScriptVersionFromMeta() {
const meta = GM_info.scriptMetaStr;
const versionMatch = meta.match(/@version\s+([^\r\n]+)/);
return versionMatch ? versionMatch[1].trim() : null;
}
// -------------------------------
// Updated Menu Management using new storage functions
// -------------------------------
function processMenuOptions(options, callback) {
Object.values(options).forEach(option => {
if (!option.alwaysShow && !userSettings.expandMenu) return;
if (option.items) {
option.items.forEach(item => callback(item));
} else {
callback(option);
}
});
}
function removeMenuOptions() {
menuItems.forEach((menuItem) => {
GMCustomUnregisterMenuCommand(menuItem);
});
menuItems.clear();
}
async function showMenuOptions() {
const shouldAutoClose = isOldTampermonkey;
removeMenuOptions();
const advancedMenuOptions = userSettings.isSimpleMode ? {} : {
toggleOnlyLiveStreamMode: {
alwaysShow: true,
label: () => `${userSettings.enableOnlyForLiveStreams ? "✅" : "❌"} ` + GET_LOCALIZED_TEXT().livestreamOnlyMode,
menuId: "toggleOnlyLiveStreamMode",
handleClick: async function () {
userSettings.enableOnlyForLiveStreams = !userSettings.enableOnlyForLiveStreams;
await updateSetting('enableOnlyForLiveStreams', userSettings.enableOnlyForLiveStreams);
updateStyles();
showMenuOptions();
},
},
toggleChatStyle: {
alwaysShow: true,
label: () => `${userSettings.modifyChat ? "✅" : "❌"} ` + GET_LOCALIZED_TEXT().applyChatStyles,
menuId: "toggleChatStyle",
handleClick: async function () {
userSettings.modifyChat = !userSettings.modifyChat;
await updateSetting('modifyChat', userSettings.modifyChat);
updateStyles();
showMenuOptions();
},
},
toggleVideoPlayerStyle: {
alwaysShow: true,
label: () => `${userSettings.modifyVideoPlayer ? "✅" : "❌"} ` + GET_LOCALIZED_TEXT().applyVideoPlayerStyles,
menuId: "toggleVideoPlayerStyle",
handleClick: async function () {
userSettings.modifyVideoPlayer = !userSettings.modifyVideoPlayer;
await updateSetting('modifyVideoPlayer', userSettings.modifyVideoPlayer);
updateStyles();
showMenuOptions();
},
},
toggleLowHeadmast: {
alwaysShow: true,
label: () => `${userSettings.setLowHeadmast ? "✅" : "❌"} ` + GET_LOCALIZED_TEXT().moveHeadmastBelowVideoPlayer,
menuId: "toggleLowHeadmast",
handleClick: async function () {
userSettings.setLowHeadmast = !userSettings.setLowHeadmast;
await updateSetting('setLowHeadmast', userSettings.setLowHeadmast);
updateStyles();
showMenuOptions();
},
},
};
const menuOptions = {
toggleScript: {
alwaysShow: true,
label: () => `🔄 ${userSettings.isScriptActive ? GET_LOCALIZED_TEXT().turnOff : GET_LOCALIZED_TEXT().turnOn}`,
menuId: "toggleScript",
handleClick: async function () {
userSettings.isScriptActive = !userSettings.isScriptActive;
await updateSetting('isScriptActive', userSettings.isScriptActive);
updateStyles();
showMenuOptions();
},
},
...advancedMenuOptions,
addVideoToBlacklist: {
alwaysShow: true,
label: () => `🚫 ${blacklist.has(videoId) ? GET_LOCALIZED_TEXT().unblacklistVideo : GET_LOCALIZED_TEXT().blacklistVideo} [id: ${videoId}]`,
menuId: "addVideoToBlacklist",
handleClick: async function () {
if (blacklist.has(videoId)) {
blacklist.delete(videoId);
} else {
blacklist.add(videoId);
}
await updateBlacklist();
updateStyles();
showMenuOptions();
},
},
toggleSimpleMode: {
alwaysShow: true,
label: () => `${userSettings.isSimpleMode ? "🚀 " + GET_LOCALIZED_TEXT().simpleMode : "🔧 " + GET_LOCALIZED_TEXT().advancedMode}`,
menuId: "toggleSimpleMode",
handleClick: async function () {
const isNewModeSimple = !userSettings.isSimpleMode;
if (isNewModeSimple) userSettingsBackup = { ...userSettings };
await updateSetting('isSimpleMode', isNewModeSimple);
userSettings = isNewModeSimple ? { ...DEFAULT_SETTINGS } : userSettingsBackup;
userSettings.isSimpleMode = isNewModeSimple;
updateStyles();
showMenuOptions();
},
},
};
processMenuOptions(menuOptions, (item) => {
GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
id: item.menuId,
autoClose: shouldAutoClose,
});
menuItems.add(item.menuId);
});
}
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const len = Math.max(parts1.length, parts2.length);
for (let i = 0; i < len; i++) {
const num1 = parts1[i] || 0;
const num2 = parts2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
function hasGreasyMonkeyAPI() {
if (typeof GM !== 'undefined') return true;
if (typeof GM_info !== 'undefined') {
useCompatibilityMode = true;
console.warn("Running in compatibility mode.");
return true;
}
return false;
}
function CheckTampermonkeyUpdated() {
if (GM_info.scriptHandler === "Tampermonkey" &&
compareVersions(GM_info.version, updatedVersions.Tampermonkey) !== 1) {
isOldTampermonkey = true;
if (isScriptRecentlyUpdated) {
GMCustomNotification({
text: GET_LOCALIZED_TEXT().tampermonkeyOutdatedAlertMessage,
timeout: 15000
});
}
}
}
function attachEventListeners() {
window.addEventListener('yt-set-theater-mode-enabled', (event) => { updateTheaterStatus(event); }, true);
window.addEventListener('yt-chat-collapsed-changed', (event) => { updateChatStatus(event); }, true);
window.addEventListener('yt-page-data-fetched', (event) => { updateVideoStatus(event); }, true);
window.addEventListener('yt-page-data-updated', updateStyles, true);
window.addEventListener('fullscreenchange', updateFullscreenStatus, true);
}
function isLiveChatIFrame() {
const liveChatIFramePattern = /^https?:\/\/.*youtube\.com\/live_chat.*$/;
return liveChatIFramePattern.test(window.location.href);
}
// -------------------------------
// Initialize the script
// -------------------------------
async function initialize() {
try {
if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API";
await cleanupOldStorage();
await loadUserSettings();
await loadBlacklist();
await updateScriptInfo();
CheckTampermonkeyUpdated();
if (isLiveChatIFrame()) {
applyStyle(styleRules.chatFrameFixStyle, true);
return;
}
applyStyle(styleRules.chatRendererFixStyle, true);
applyStyle(styleRules.videoPlayerFixStyle, true);
updateStyles();
attachEventListeners();
showMenuOptions();
} catch (error) {
console.error(`Error when initializing script: ${error}. Aborting script.`);
}
}
// -------------------------------
// Entry Point
// -------------------------------
initialize();
})();