- // ==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.3
- // @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';
-
- const DEFAULT_SETTINGS = {
- isScriptActive: true,
- enableOnlyForLiveStreams: false,
- modifyVideoPlayer: true,
- modifyChat: true,
- blacklist: new Set()
- };
-
- let userSettings = { ...DEFAULT_SETTINGS };
- let useCompatibilityMode = false;
- let isBrokenOrMissingGMAPI = false;
-
- 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;
-
- let menuItems = new Set();
- let activeStyles = new Set();
- let chatStyles;
- let videoPlayerStyles;
- let headmastStyles;
-
- let temporaryFixRuleStyles;
- let staticChatFrameFixStyles;
-
- let moviePlayer;
- let videoId;
- let chatFrame;
- let isTheaterMode = false;
- let chatCollapsed = true;
- let isLiveStream = false;
-
- // Helper Functions
- //-------------------------------------------------------------------------------
- function removeStyle(style) {
- if (!activeStyles.has(style)) return;
- if (style && style.parentNode) {
- style.parentNode.removeChild(style);
- }
- activeStyles.delete(style);
- }
-
- function removeAllStyles() {
- activeStyles.forEach((style) => {
- removeStyle(style);
- });
- activeStyles.clear();
- }
-
- function addStyle(styleRule, styleObject) {
- if (activeStyles.has(styleObject)) return styleObject;
- styleObject = GMCustomAddStyle(styleRule);
- activeStyles.add(styleObject);
- return styleObject;
- }
-
- // Apply Styles
- //----------------------------------
- function applyCssRulesToTemporarilyFixYouTubeFullscreen() {
- GMCustomAddStyle(`
- .html5-video-container {
- top: -1px !important;
- display: flex !important;
- justify-content: center !important;
- }
- .ytp-fit-cover-video .html5-main-video {
- left: 0 !important;
- object-fit: contain !important;
- width: 100% !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;
- }
- `);
- }
- }
-
- function applyChatStyles() {
- chatStyles = addStyle(`
- 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;
- }
- `, chatStyles);
- }
-
- function applyVideoPlayerStyles() {
- videoPlayerStyles = addStyle(`
- ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
- max-height: calc(100vh - var(--ytd-watch-flexy-masthead-height)) !important;
- }
- .html5-video-container {
- top: -1px !important;
- }
- `, videoPlayerStyles);
- }
-
- function applyHeadmastStyles() {
- headmastStyles = addStyle(`
- #masthead-container.ytd-app {
- max-width: calc(100% - ${chatFrame.offsetWidth}px) !important;
- }
- `, headmastStyles);
- }
-
- // Update Stuff
- //------------------------------------------------------
- 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) {
- applyChatStyles();
-
- const mastHeadContainer = document.querySelector('#masthead-container');
- let shouldShrinkHeadmast = isTheaterMode && !chatCollapsed
- && chatFrame?.getBoundingClientRect().top <= mastHeadContainer.getBoundingClientRect().bottom;
-
- if (shouldShrinkHeadmast) {
- applyHeadmastStyles();
- } else {
- removeStyle(headmastStyles);
- }
- } else {
- [chatStyles, headmastStyles].forEach(removeStyle);
- }
-
- if (userSettings.modifyVideoPlayer) {
- applyVideoPlayerStyles();
- } else {
- removeStyle(videoPlayerStyles);
- }
- 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 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);
- }
- }
-
- // Functions for the GUI
- //-----------------------------------------------------
- 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);
- });
- }
-
- // Handle User Preferences
- //------------------------------------------------
- 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) {
- console.error(error);
- }
- }
- // Verify Grease Monkey API
- //-----------------------------------------------
- function checkGMAPI() {
- if (typeof GM != 'undefined') return;
- if (typeof GM_info != 'undefined') {
- useCompatibilityMode = true;
- console.warn("Running in compatibility mode.");
- return;
- }
- isBrokenOrMissingGMAPI = true;
- }
-
- // Preparation Stuff
- //-------------------------------------------------
- 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);
- }
-
- function isLiveChatIFrame() {
- const liveChatIFramePattern = /^https?:\/\/.*youtube\.com\/live_chat.*$/;
- const currentUrl = window.location.href;
- return liveChatIFramePattern.test(currentUrl);
- }
-
- async function initialize() {
- checkGMAPI();
- try {
- if (isBrokenOrMissingGMAPI) throw "Did not detect valid Grease Monkey API";
- if (isLiveChatIFrame()) return applyStaticChatFrameFixStyles(); // Fixes the terrible css of the live chat iframe.
- applyCssRulesToTemporarilyFixYouTubeFullscreen(); // This fixes the YouTube fullscreen issue where the video element would occasionally be blocked by the chat renderer. Would remove this if the issue is fixed.
- await loadUserSettings();
- updateStyles();
- attachEventListeners();
- showMenuOptions();
- } catch (error) {
- console.error(`Error loading user settings: ${error}. Aborting script.`);
- }
- }
- // Entry Point
- //-------------------------------------------
- initialize();
- })();