// ==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.5.7
// @match *://www.youtube.com/*
// @match *://www.youtube-nocookie.com/*
// @grant GM.addStyle
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @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 the script
const DEFAULT_SETTINGS = {
isScriptActive: true,
enableOnlyForLiveStreams: false,
modifyVideoPlayer: true,
modifyChat: true,
blacklist: new Set()
};
let userSettings = { ...DEFAULT_SETTINGS };
let useCompatibilityMode = false;
let menuItems = new Set();
let activeStyles = new Map();
let hidChatTemporarily = false;
let moviePlayer;
let videoId;
let chatFrame;
let isFullscreen = false;
let isTheaterMode = false;
let chatCollapsed = true;
let isLiveStream = false;
let chatWidth = 0;
// Greasemonkey API Compatibility Layer
const GMCustomAddStyle = useCompatibilityMode ? GM_addStyle : GM.addStyle;
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;
// Style Rules
const styleRules = {
chatStyle: {
id: "chatStyle",
getRule: () => `
ytd-live-chat-frame[theater-watch-while][rounded-container] {
border-radius: 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;
}
`,
},
}
// Apply static styles for specific YouTube UI fixes
function applyStaticVideoPlayerFixStyles() {
GMCustomAddStyle(`
.html5-video-container {
top: -1px !important;
}
`);
}
function applyStaticChatFrameFixStyles() {
GMCustomAddStyle(`
ytd-live-chat-frame[theater-watch-while][rounded-container] {
border-top: 0 !important;
border-bottom: 0 !important;
}
#panel-pages.yt-live-chat-renderer {
border-bottom: 0 !important;
}
`);
const panelPages = document.querySelector('iron-pages#panel-pages');
if (panelPages.offsetHeight <= 3) {
GMCustomAddStyle(`
#panel-pages.yt-live-chat-renderer{
border-top: 0 !important;
}
`);
}
}
// Fix fullscreen chat issues by toggling chat renderer
function toggleChatRendererToTemporarilyFixFullscreenIssues() {
if (isFullscreen) {
if (chat && !chat.collapsed) {
chat.getElementsByTagName('button')[0].click();
hidChatTemporarily = true;
}
} else if (hidChatTemporarily) {
chat.getElementsByTagName('button')[0].click();
hidChatTemporarily = false;
}
}
// Apply and remove styles dynamically based on settings
function removeStyle(style) {
if (!activeStyles.has(style.id)) return;
const styleElement = activeStyles.get(style.id);
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
activeStyles.delete(style.id);
}
function removeAllStyles() {
activeStyles.forEach((styleElement, styleId) => {
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
});
activeStyles.clear();
}
function applyStyle(style) {
if (activeStyles.has(style.id)) return;
if (typeof style.getRule !== 'function') return;
const styleElement = GMCustomAddStyle(style.getRule());
activeStyles.set(style.id, styleElement);
}
// Update styles dynamically based on settings and current state
function updateStyles() {
const shouldNotActivate =
!userSettings.isScriptActive ||
userSettings.blacklist.has(videoId) ||
(userSettings.enableOnlyForLiveStreams && !isLiveStream);
if (shouldNotActivate) {
removeAllStyles();
if (moviePlayer) moviePlayer.setCenterCrop(); //trigger size update for the html5 video element
return;
}
if (userSettings.modifyChat) {
applyStyle(styleRules.chatStyle);
const mastHeadContainer = document.querySelector('#masthead-container');
let chatFramePositionValid =
mastHeadContainer.getBoundingClientRect().bottom < 0 ||
chatFrame?.getBoundingClientRect().top <= mastHeadContainer.getBoundingClientRect().bottom;
let shouldShrinkHeadmast = isTheaterMode && !chatCollapsed && chatFramePositionValid;
if (shouldShrinkHeadmast) {
chatWidth = chatFrame?.offsetWidth || 0;
applyStyle(styleRules.headmastStyle);
} else {
removeStyle(styleRules.headmastStyle);
}
} else {
[styleRules.chatStyle, styleRules.headmastStyle].forEach(removeStyle);
}
if (userSettings.modifyVideoPlayer) {
applyStyle(styleRules.videoPlayerStyle);
} else {
removeStyle(styleRules.videoPlayerStyle);
}
if (moviePlayer) moviePlayer.setCenterCrop(); //trigger size update for the html5 video element
}
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 updateFullscreenStatus() {
isFullscreen = document.fullscreenElement;
toggleChatRendererToTemporarilyFixFullscreenIssues(); // To fix fullscreen issues this needs to alway run
}
function updateVideoStatus(event) {
try {
videoId = event.detail.pageData.playerResponse.videoDetails.videoId;
moviePlayer = document.querySelector('#movie_player');
isLiveStream = event.detail.pageData.playerResponse.videoDetails.isLiveContent;
showMenuOptions();
} catch (error) {
throw ("Failed to update video status due to this error. Error: " + error);
}
}
// Menu management for user interaction
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();
}
function showMenuOptions() {
removeMenuOptions();
const menuOptions = {
toggleScript: {
alwaysShow: true,
label: () => `🔄 ${userSettings.isScriptActive ? "Turn Off" : "Turn On"}`,
menuId: "toggleScript",
handleClick: function () {
userSettings.isScriptActive = !userSettings.isScriptActive;
GMCustomSetValue('isScriptActive', userSettings.isScriptActive);
updateStyles();
showMenuOptions();
},
},
toggleOnlyLiveStreamMode: {
alwaysShow: true,
label: () => `${userSettings.enableOnlyForLiveStreams ? "✅" : "❌"} Livestream Only Mode`,
menuId: "toggleOnlyLiveStreamMode",
handleClick: function () {
userSettings.enableOnlyForLiveStreams = !userSettings.enableOnlyForLiveStreams;
GMCustomSetValue('enableOnlyForLiveStreams', userSettings.enableOnlyForLiveStreams);
updateStyles();
showMenuOptions();
},
},
toggleChatStyle: {
alwaysShow: true,
label: () => `${userSettings.modifyChat ? "✅" : "❌"} Apply Chat Styles`,
menuId: "toggleChatStyle",
handleClick: function () {
userSettings.modifyChat = !userSettings.modifyChat;
GMCustomSetValue('modifyChat', userSettings.modifyChat);
updateStyles();
showMenuOptions();
},
},
toggleVideoPlayerStyle: {
alwaysShow: true,
label: () => `${userSettings.modifyVideoPlayer ? "✅" : "❌"} Apply Video Player Styles`,
menuId: "toggleVideoPlayerStyle",
handleClick: function () {
userSettings.modifyVideoPlayer = !userSettings.modifyVideoPlayer;
GMCustomSetValue('modifyVideoPlayer', userSettings.modifyVideoPlayer);
updateStyles();
showMenuOptions();
},
},
addVideoToBlacklist: {
alwaysShow: true,
label: () => `${userSettings.blacklist.has(videoId) ? "Unblacklist Video " : "Blacklist Video"} [id: ${videoId}]`,
menuId: "addVideoToBlacklist",
handleClick: function () {
if (userSettings.blacklist.has(videoId)) {
userSettings.blacklist.delete(videoId);
} else {
userSettings.blacklist.add(videoId);
}
GMCustomSetValue('blacklist', [...userSettings.blacklist]);
updateStyles();
showMenuOptions();
},
},
};
processMenuOptions(menuOptions, (item) => {
GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
id: item.menuId,
autoClose: false,
});
menuItems.add(item.menuId);
});
}
// User Setting Handling
async function loadUserSettings() {
try {
const storedValues = await GMCustomListValues();
for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
if (!storedValues.includes(key)) {
await GMCustomSetValue(key, value instanceof Set ? Array.from(value) : value);
}
}
for (const key of storedValues) {
if (!(key in DEFAULT_SETTINGS)) {
await GMCustomDeleteValue(key);
}
}
const keyValuePairs = await Promise.all(
storedValues.map(async key => [key, await GMCustomGetValue(key)])
);
keyValuePairs.forEach(([newKey, newValue]) => {
userSettings[newKey] = newValue;
});
// Convert blacklist to Set if it exists
if (userSettings.blacklist) {
userSettings.blacklist = new Set(userSettings.blacklist);
}
console.log(`Loaded user settings: ${JSON.stringify(userSettings)}`);
} catch (error) {
throw `Error loading user settings: ${error}. Aborting script.`;
}
}
// Check compatibility with Greasemonkey API
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;
}
// Attach necessary event listeners
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);
}
// Check if the script is running inside a live chat iframe
function isLiveChatIFrame() {
const liveChatIFramePattern = /^https?:\/\/.*youtube\.com\/live_chat.*$/;
const currentUrl = window.location.href;
return liveChatIFramePattern.test(currentUrl);
}
// Initialize the script
async function initialize() {
try {
if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API";
if (isLiveChatIFrame()) return applyStaticChatFrameFixStyles(); // Fixes the terrible css of the live chat iframe.
applyStaticVideoPlayerFixStyles(); // Fixes video player end screen style rounding issues during certain zoom levels.
await loadUserSettings();
updateStyles();
attachEventListeners();
showMenuOptions();
} catch (error) {
return console.error(`Error when initializing script: ${error}. Aborting script.`);
}
}
// Entry Point
initialize();
})();