更佳 YouTube 剧场模式

改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性。并修复近期 YouTube 更新中损坏的全屏界面。

目前为 2025-04-13 提交的版本。查看 最新版本

// ==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.11
// @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
// @noframes
// @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";

    // CONFIG AND CONSTANTS
    const CONFIG = {
        // UI Constants
        MIN_CHAT_SIZE: { width: '300px', height: '356px' },
        DRAG_BAR_HEIGHT: '35px',

        // Default settings
        DEFAULT_SETTINGS: {
            isScriptActive: true,
            isSimpleMode: true,
            enableOnlyForLiveStreams: false,
            modifyVideoPlayer: true,
            modifyChat: true,
            setLowHeadmast: false,
            useCustomPlayerHeight: false,
            floatingChat: false,
            chatOpacity: '0.95',
            chatOffset: { left: '0px', top: '-500px' },
            chatSize: { width: '300px', height: '356px' },
            debug: false
        },

        // Default empty blacklist
        DEFAULT_BLACKLIST: [],

        // Version requirements
        REQUIRED_VERSIONS: {
            Tampermonkey: '5.4.624'
        }
    };

    // TRANSLATIONS
    const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage;

    function getPreferredLanguage() {
        if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW') {
            return 'zh-CN';
        }
        // Check if language is supported, otherwise fall back to English
        return ['en-US', 'zh-TW', 'zh-CN', 'ja'].includes(BROWSER_LANGUAGE)
            ? BROWSER_LANGUAGE
            : 'en-US';
    }

    const TRANSLATIONS = {
        'en-US': {
            tampermonkeyOutdatedAlert: "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',
            useCustomPlayerHeight: 'Use Custom Player Height',
            playerHeightText: 'Player Height',
            floatingChat: 'Floating Chat',
            blacklistVideo: 'Blacklist Video',
            unblacklistVideo: 'Unblacklist Video',
            simpleMode: 'Simple Mode',
            advancedMode: 'Advanced Mode',
            debug: 'DEBUG'
        },
        'zh-TW': {
            tampermonkeyOutdatedAlert: "看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 5.4.6224 或更高版本。",
            turnOn: '開啟',
            turnOff: '關閉',
            livestreamOnlyMode: '僅限直播模式',
            applyChatStyles: '套用聊天樣式',
            applyVideoPlayerStyles: '套用影片播放器樣式',
            moveHeadmastBelowVideoPlayer: '將頁首橫幅移到影片播放器下方',
            useCustomPlayerHeight: '使用自訂播放器高度',
            playerHeightText: '播放器高度',
            floatingChat: '浮動聊天室',
            blacklistVideo: '將影片加入黑名單',
            unblacklistVideo: '從黑名單中移除影片',
            simpleMode: '簡易模式',
            advancedMode: '進階模式',
            debug: '偵錯'
        },
        'zh-CN': {
            tampermonkeyOutdatedAlert: "看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 5.4.6224 或更高版本。",
            turnOn: '开启',
            turnOff: '关闭',
            livestreamOnlyMode: '仅限直播模式',
            applyChatStyles: '应用聊天样式',
            applyVideoPlayerStyles: '应用视频播放器样式',
            moveHeadmastBelowVideoPlayer: '将页首横幅移动到视频播放器下方',
            useCustomPlayerHeight: '使用自定义播放器高度',
            playerHeightText: '播放器高度',
            floatingChat: '浮动聊天室',
            blacklistVideo: '将视频加入黑名单',
            unblacklistVideo: '从黑名单中移除视频',
            simpleMode: '简易模式',
            advancedMode: '高级模式',
            debug: '调试'
        },
        'ja': {
            tampermonkeyOutdatedAlert: "ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン5.4.6224以上に更新してください。",
            turnOn: "オンにする",
            turnOff: "オフにする",
            livestreamOnlyMode: "ライブ配信専用モード",
            applyChatStyles: "チャットスタイルを適用",
            applyVideoPlayerStyles: "ビデオプレイヤースタイルを適用",
            moveHeadmastBelowVideoPlayer: "ヘッドマストをビデオプレイヤーの下に移動",
            useCustomPlayerHeight: "カスタムプレイヤーの高さを使用",
            playerHeightText: "プレイヤーの高さ",
            floatingChat: "フローティングチャット",
            blacklistVideo: "動画をブラックリストに追加",
            unblacklistVideo: "ブラックリストから動画を解除",
            simpleMode: "シンプルモード",
            advancedMode: "高度モード",
            debug: "デバッグ"
        }
    };

    function getLocalizedText() {
        return TRANSLATIONS[getPreferredLanguage()] || TRANSLATIONS['en-US'];
    }

    // STATE VARIABLES
    const state = {
        userSettings: { ...CONFIG.DEFAULT_SETTINGS },
        advancedSettingsBackup: null,
        blacklist: new Set(),
        useCompatibilityMode: false,
        menuItems: new Set(),
        activeStyles: new Map(),
        resizeObserver: null,
        moviePlayer: null,
        videoId: null,
        chatFrame: null,
        currentPageType: '',
        isFullscreen: false,
        isTheaterMode: false,
        chatCollapsed: true,
        isLiveStream: false,
        chatWidth: 0,
        moviePlayerHeight: 0,
        isOldTampermonkey: false,
        isScriptRecentlyUpdated: false
    };

    // GM API COMPATIBILITY
    const GM = {
        registerMenuCommand: state.useCompatibilityMode ? GM_registerMenuCommand : window.GM?.registerMenuCommand,
        unregisterMenuCommand: state.useCompatibilityMode ? GM_unregisterMenuCommand : window.GM?.unregisterMenuCommand,
        getValue: state.useCompatibilityMode ? GM_getValue : window.GM?.getValue,
        setValue: state.useCompatibilityMode ? GM_setValue : window.GM?.setValue,
        listValues: state.useCompatibilityMode ? GM_listValues : window.GM?.listValues,
        deleteValue: state.useCompatibilityMode ? GM_deleteValue : window.GM?.deleteValue,
        notification: state.useCompatibilityMode ? GM_notification : window.GM?.notification
    };

    // STYLE DEFINITIONS
    const styleRules = {
        chatStyle: {
            id: "betterTheater-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: "betterTheater-videoPlayerStyle",
            getRule: () => {
                if (state.userSettings.useCustomPlayerHeight) {
                    return `
                        ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
                            min-height: 0px !important;
                            height: ${state.userSettings.playerHeightPx}px !important;
                        }
                    `;
                } else {
                    return `
                        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: "betterTheater-headmastStyle",
            getRule: () => `
                #masthead-container.ytd-app {
                    max-width: calc(100% - ${state.chatWidth}px) !important;
                }
            `,
        },

        lowHeadmastStyle: {
            id: "betterTheater-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;
                }
                ${state.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: ${state.moviePlayerHeight}px !important;
                    position: relative !important;
                }
            `,
        },

        videoPlayerFixStyle: {
            id: "betterTheater-videoPlayerFixStyle",
            getRule: () => `
                .html5-video-container {
                    top: -1px !important;
                }
                #skip-navigation.ytd-masthead {
                    left: -500px;
                }
            `,
        },

        chatFrameFixStyle: {
            id: "betterTheater-chatFrameFixStyle",
            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: "betterTheater-chatRendererFixStyle",
            getRule: () => `
                ytd-live-chat-frame[theater-watch-while][rounded-container] {
                    border-bottom: 0 !important;
                }
            `,
        },

        floatingChatStyle: {
            id: "betterTheater-floatingChatStyle",
            getRule: () => `
                #chat-container {
                    min-width: ${CONFIG.MIN_CHAT_SIZE.width} !important;
                    max-width: 100vw !important;
                    max-height: 100vh !important;
                    position: absolute;
                    border-radius: 0 0 10px 10px !important;
                }
                #chat {
                    top: ${CONFIG.DRAG_BAR_HEIGHT} !important;
                    height: calc(100% - ${CONFIG.DRAG_BAR_HEIGHT}) !important;
                    width: inherit !important;
                    min-width: inherit !important;
                    max-width: inherit !important;
                    min-height: ${CONFIG.MIN_CHAT_SIZE.height} !important;
                    max-height: 100vh !important;
                }
            `,
        },

        floatingChatStyleExpanded: {
            id: "betterTheater-floatingChatStyleExpanded",
            getRule: () => `
                #chat-container {
                    min-height: ${CONFIG.MIN_CHAT_SIZE.height} !important;
                }
                ytd-live-chat-frame:not([theater-watch-while])[rounded-container] {
                    border-top-left-radius: 0 !important;
                    border-top-right-radius: 0 !important;
                    border-top: 0 !important;
                }
                ytd-live-chat-frame:not([theater-watch-while])[rounded-container] iframe.ytd-live-chat-frame {
                    border-top-left-radius: 0 !important;
                    border-top-right-radius: 0 !important;
                }
            `,
        },

        floatingChatStyleCollapsed: {
            id: "betterTheater-floatingChatStyleCollapsed",
            getRule: () => `
                ytd-live-chat-frame[round-background] #show-hide-button.ytd-live-chat-frame>ytd-toggle-button-renderer.ytd-live-chat-frame,
                ytd-live-chat-frame[round-background] #show-hide-button.ytd-live-chat-frame>ytd-button-renderer.ytd-live-chat-frame {
                    margin: 0 !important;
                    border-radius: 0 0 12px 12px !important;
                    border-left: 1px solid var(--yt-spec-10-percent-layer) !important;
                    border-right: 1px solid var(--yt-spec-10-percent-layer) !important;
                    border-bottom: 1px solid var(--yt-spec-10-percent-layer) !important;
                    background-clip: padding-box !important;
                }
                ytd-live-chat-frame[modern-buttons][collapsed] {
                    border-radius: 0 0 12px 12px !important;
                }
                button.yt-spec-button-shape-next.yt-spec-button-shape-next--outline.yt-spec-button-shape-next--mono.yt-spec-button-shape-next--size-m {
                    border-radius: 0 0 12px 12px !important;
                    border: none !important;
                }
                .chat-resize-handle {
                    visibility: hidden !important;
                }
            `,
        },

        debugResizeHandleStyle: {
            id: "betterTheater-debugResizeHandleStyle",
            getRule: () => `
                /* Default state for resize handles */
                #chat-container .chat-resize-handle {
                    background: transparent;
                    opacity: 0;
                }
                /* Debug state for resize handles when #chat-container has [debug] attribute */
                #chat-container[debug] .chat-resize-handle {
                    opacity: 0.5;
                }
                #chat-container[debug] .chat-resize-handle.rs-right {
                    background: rgba(255, 0, 0, 0.5);
                }
                #chat-container[debug] .chat-resize-handle.rs-left {
                    background: rgba(0, 255, 0, 0.5);
                }
                #chat-container[debug] .chat-resize-handle.rs-bottom {
                    background: rgba(0, 0, 255, 0.5);
                }
                #chat-container[debug] .chat-resize-handle.rs-top {
                    background: rgba(255, 255, 0, 0.5);
                }
                #chat-container[debug] .chat-resize-handle.rs-bottom-left {
                    background: rgba(0, 255, 255, 0.5);
                }
                #chat-container[debug] .chat-resize-handle.rs-top-left {
                    background: rgba(255, 255, 0, 0.5);
                }
                #chat-container[debug] .chat-resize-handle.rs-top-right {
                    background: rgba(255, 0, 0, 0.5);
                }
                #chat-container[debug] .chat-resize-handle.rs-bottom-right {
                    background: rgba(255, 0, 255, 0.5);
                }
            `,
        },

        chatSliderStyle: {
            id: "betterTheater-chatSliderStyle",
            getRule: () => `
                .chat-drag-bar input[type=range] {
                    -webkit-appearance: none;
                    width: 100px;
                    height: 4px;
                    background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
                    border-radius: 2px;
                    outline: none;
                }
                .chat-drag-bar input[type=range]::-webkit-slider-thumb {
                    -webkit-appearance: none;
                    appearance: none;
                    width: 14px;
                    height: 14px;
                    border-radius: 50%;
                    background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
                    cursor: pointer;
                }
                .chat-drag-bar input[type=range]::-moz-range-thumb {
                    width: 14px;
                    height: 14px;
                    border-radius: 50%;
                    background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
                    cursor: pointer;
                }
                .chat-drag-bar input[type=range]::-moz-range-track {
                    background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
                    height: 4px;
                    border-radius: 2px;
                }
                .chat-drag-bar input[type=range]::-ms-thumb {
                    background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
                }
                .chat-drag-bar input[type=range]::-ms-track {
                    background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
                    height: 4px;
                    border-radius: 2px;
                }
            `
        }
    };

    // STYLE MANAGEMENT
    function applyStyle(style, setPersistent = false) {
        if (typeof style.getRule !== 'function') return;
        if (state.activeStyles.has(style.id)) {
            removeStyle(style);
        }

        const styleElement = document.createElement('style');
        styleElement.id = style.id;
        styleElement.type = 'text/css';
        styleElement.textContent = style.getRule();
        (document.head || document.documentElement).appendChild(styleElement);
        state.activeStyles.set(style.id, {
            element: styleElement,
            persistent: setPersistent
        });
    }

    function removeStyle(style) {
        if (!state.activeStyles.has(style.id)) return;

        const { element: styleElement } = state.activeStyles.get(style.id);
        if (styleElement && styleElement.parentNode) {
            styleElement.parentNode.removeChild(styleElement);
        }

        state.activeStyles.delete(style.id);
    }

    function removeAllStyles() {
        state.activeStyles.forEach((styleData, styleId) => {
            if (!styleData.persistent) {
                removeStyle({ id: styleId });
            }
        });
    }

    function setStyleState(style, on = true) {
        on ? applyStyle(style) : removeStyle(style);
    }

    // RESIZE HANDLES
    function isAtMinSize(element) {
        const style = window.getComputedStyle(element);
        const width = parseFloat(style.width);
        const height = parseFloat(style.height);
        const minWidth = parseFloat(style.minWidth);
        const minHeight = parseFloat(style.minHeight);

        return {
            isAtMinWidth: width <= minWidth,
            isAtMinHeight: height <= minHeight
        };
    }

    function addResizeHandles(chatContainer) {
        const handleConfigs = {
            right: {
                width: "6px", top: "0", right: "0", bottom: "0",
                cursor: "ew-resize", horizontal: true, vertical: false
            },
            left: {
                width: "6px", top: "0", left: "0", bottom: "0",
                cursor: "ew-resize", horizontal: true, vertical: false
            },
            bottom: {
                height: "6px", left: "0", bottom: "0", right: "0",
                cursor: "ns-resize", horizontal: false, vertical: true
            },
            top: {
                height: "6px", left: "0", top: "0", right: "0",
                cursor: "ns-resize", horizontal: false, vertical: true
            },
            bottomLeft: {
                width: "12px", height: "12px", left: "0", bottom: "0",
                cursor: "nesw-resize", horizontal: true, vertical: true
            },
            topLeft: {
                width: "12px", height: "12px", left: "0", top: "0",
                cursor: "nwse-resize", horizontal: true, vertical: true
            },
            topRight: {
                width: "12px", height: "12px", right: "0", top: "0",
                cursor: "nesw-resize", horizontal: true, vertical: true
            },
            bottomRight: {
                width: "12px", height: "12px", right: "0", bottom: "0",
                cursor: "nwse-resize", horizontal: true, vertical: true
            }
        };

        const handles = {};
        for (const [position, config] of Object.entries(handleConfigs)) {
            const handle = document.createElement("div");
            handle.className = `chat-resize-handle rs-${position}`;
            handle.style.position = "absolute";
            handle.style.zIndex = "10001";
            Object.assign(handle.style, config);
            chatContainer.appendChild(handle);
            handles[position] = handle;
            initResizeHandler(handle, config);
        }
        return handles;

        function initResizeHandler(handle, config) {
            let startX, startY, startWidth, startHeight, startLeft, startTop;
            async function saveChatSize() {
                state.userSettings.chatSize = {
                    width: chatContainer.style.width,
                    height: chatContainer.style.height
                };
                state.userSettings.chatOffset = {
                    left: chatContainer.style.left,
                    top: chatContainer.style.top
                };
                await updateSetting('chatSize', state.userSettings.chatSize);
                await updateSetting('chatOffset', state.userSettings.chatOffset);
            }

            handle.addEventListener("pointerdown", function (e) {
                if (e.pointerType === "mouse" && e.button !== 0) return;
                e.preventDefault();
                startX = e.clientX;
                startY = e.clientY;
                startWidth = chatContainer.offsetWidth;
                startHeight = chatContainer.offsetHeight;
                startLeft = parseFloat(getComputedStyle(chatContainer).left) || 0;
                startTop = parseFloat(getComputedStyle(chatContainer).top) || 0;

                handle.setPointerCapture(e.pointerId);
            });

            handle.addEventListener("pointermove", function (e) {
                if (!handle.hasPointerCapture(e.pointerId)) return;
                e.preventDefault();

                const movieRect = state.moviePlayer.getBoundingClientRect();
                const chatParentRect = chatContainer.parentElement.getBoundingClientRect();
                const chatRect = chatContainer.getBoundingClientRect();
                const minWidth = parseInt(CONFIG.MIN_CHAT_SIZE.width);
                const minHeight = parseInt(CONFIG.MIN_CHAT_SIZE.height);
                let dx = e.clientX - startX;
                let dy = e.clientY - startY;
                if (dx === 0 && dy === 0) return;
                dx = Math.max(-startX, Math.min(dx, movieRect.right - startX));
                dy = Math.max(-startY, Math.min(dy, movieRect.bottom - startY));

                const tooSmall = isAtMinSize(chatContainer);

                if (config.horizontal) {
                    const isRightSide = handle.className.includes('right');
                    const isLeftSide = handle.className.includes('left');
                    if (isRightSide) {
                        if (!tooSmall.isAtMinWidth || dx > 0) {
                            if (startWidth + dx < minWidth) dx = minWidth - startWidth;
                            if (chatRect.left + startWidth + dx > movieRect.right) {
                                dx = movieRect.right - chatRect.left - startWidth;
                            }
                            chatContainer.style.width = Math.max(minWidth, startWidth + dx) + "px";
                        }
                    } else if (isLeftSide) {
                        if (!tooSmall.isAtMinWidth || dx < 0) {
                            if (startWidth - dx < minWidth) dx = startWidth - minWidth;
                            if (startLeft + chatParentRect.left + dx < movieRect.left) {
                                dx = movieRect.left - startLeft - chatParentRect.left;
                            }
                            chatContainer.style.width = Math.max(minWidth, startWidth - dx) + "px";
                            chatContainer.style.left = (startLeft + dx) + "px";
                        }
                    }
                }

                if (config.vertical) {
                    const isBottomSide = handle.className.includes('bottom');
                    const isTopSide = handle.className.includes('top');

                    if (isBottomSide) {
                        if (!tooSmall.isAtMinHeight || dy > 0) {
                            if (startHeight + dy < minHeight) dy = minHeight - startHeight;
                            if (chatRect.top + startHeight + dy > movieRect.bottom) {
                                dy = movieRect.bottom - chatRect.top - startHeight;
                            }
                            chatContainer.style.height = Math.max(minHeight, startHeight + dy) + "px";
                        }
                    } else if (isTopSide) {
                        if (!tooSmall.isAtMinHeight || dy < 0) {
                            if (startHeight - dy < minHeight) dy = startHeight - minHeight;
                            if (startTop + chatParentRect.top + dy < movieRect.top) {
                                dy = movieRect.top - startTop - chatParentRect.top;
                            }
                            chatContainer.style.height = Math.max(minHeight, startHeight - dy) + "px";
                            chatContainer.style.top = (startTop + dy) + "px";
                        }
                    }
                }
            });

            handle.addEventListener("pointerup", function (e) {
                handle.releasePointerCapture(e.pointerId);
                saveChatSize();
            });
        }
    }

    // Removes all resize handles from a container
    function removeResizeHandles(chatContainer) {
        if (!chatContainer) return;
        const handles = chatContainer.querySelectorAll(".chat-resize-handle");
        handles.forEach(handle => handle.remove());
    }

    // DRAG BAR & CHAT CONTROLS
    function addDragBarWithOpacitySlider(chatContainer) {
        let existingBar = chatContainer.querySelector('.chat-drag-bar');
        if (existingBar) return existingBar;

        // Add chat slider style if not present
        applyStyle(styleRules.chatSliderStyle, true);

        const dragBar = document.createElement("div");
        dragBar.className = "chat-drag-bar";
        dragBar.style.position = "absolute";
        dragBar.style.top = "0";
        dragBar.style.left = "0";
        dragBar.style.right = "0";
        dragBar.style.height = "15px";
        dragBar.style.background = "var(--yt-live-chat-background-color)";
        dragBar.style.color = "var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color))";
        dragBar.style.border = "1px solid var(--yt-spec-10-percent-layer)";
        dragBar.style.backgroundClip = "padding-box";
        dragBar.style.display = "flex";
        dragBar.style.alignItems = "center";
        dragBar.style.justifyContent = "space-between";
        dragBar.style.padding = (parseInt(CONFIG.DRAG_BAR_HEIGHT) - 15) / 2 + "px";
        dragBar.style.zIndex = "10000";
        dragBar.style.borderRadius = "12px 12px 0 0";

        const dragLabel = document.createElement("div");
        dragLabel.innerText = "⋮⋮";
        dragLabel.style.fontSize = "var(--yt-live-chat-header-font-size, 18px)";
        dragLabel.style.userSelect = "none";

        const opacitySlider = document.createElement("input");
        opacitySlider.type = "range";
        opacitySlider.min = "20";
        opacitySlider.max = "100";
        opacitySlider.value = Math.round(parseFloat(state.userSettings.chatOpacity) * 100).toString();
        opacitySlider.style.marginLeft = "10px";

        opacitySlider.addEventListener("input", () => {
            const newOpacity = opacitySlider.value / 100;
            chatContainer.style.opacity = newOpacity;
        });

        opacitySlider.addEventListener("mouseup", () => {
            updateSetting('chatOpacity', chatContainer.style.opacity);
        });

        ["pointerdown", "pointermove", "pointerup"].forEach(eventType => {
            opacitySlider.addEventListener(eventType, (e) => {
                e.stopPropagation();
            });
        });

        dragBar.appendChild(dragLabel);
        dragBar.appendChild(opacitySlider);
        chatContainer.insertBefore(dragBar, chatContainer.firstChild);

        // Add drag functionality
        setupDragBehavior(dragBar, chatContainer);

        return dragBar;
    }

    function setupDragBehavior(dragBar, chatContainer) {
        let dragging = false;
        let startX = 0, startY = 0;
        let containerStartLeft = 0;
        let containerStartTop = 0;
        const dragThreshold = 5;

        async function saveChatPosition() {
            state.userSettings.chatOffset = {
                left: chatContainer.style.left,
                top: chatContainer.style.top
            };
            await updateSetting('chatOffset', state.userSettings.chatOffset);
        }

        // Handle pointer down event
        dragBar.addEventListener("pointerdown", function (e) {
            if (e.pointerType === "mouse" && e.button !== 0) return;

            dragging = false;
            startX = e.clientX;
            startY = e.clientY;
            containerStartLeft = parseFloat(getComputedStyle(chatContainer).left) || 0;
            containerStartTop = parseFloat(getComputedStyle(chatContainer).top) || 0;

            dragBar.setPointerCapture(e.pointerId);
            e.preventDefault();
        });

        // Handle pointer move event
        dragBar.addEventListener("pointermove", function (e) {
            if (!dragBar.hasPointerCapture(e.pointerId)) return;

            let dx = e.clientX - startX;
            let dy = e.clientY - startY;

            if (dx === 0 && dy === 0) return;

            // Start dragging after threshold is crossed
            if (!dragging && Math.hypot(dx, dy) > dragThreshold) {
                dragging = true;
            }

            if (dragging) {
                // Calculate new position
                let newLeft = containerStartLeft + dx;
                let newTop = containerStartTop + dy;

                // Get boundaries
                const movieRect = state.moviePlayer.getBoundingClientRect();
                const chatParentRect = chatContainer.parentElement.getBoundingClientRect();

                // Constrain to left edge
                if (newLeft + chatParentRect.left < 0) {
                    newLeft = -chatParentRect.left;
                }

                // Constrain to top edge
                if (newTop + chatParentRect.top < 0) {
                    newTop = -chatParentRect.top;
                }

                // Constrain to right edge
                if (newLeft > movieRect.right - (chatParentRect.left + chatContainer.offsetWidth)) {
                    newLeft = movieRect.right - (chatParentRect.left + chatContainer.offsetWidth);
                }

                // Constrain to bottom edge, accounting for chat collapsed state
                if (state.chatCollapsed) {
                    const showHideButton = chatContainer.querySelector('#show-hide-button');
                    if (showHideButton && newTop > movieRect.bottom - (chatParentRect.top + showHideButton.offsetHeight + dragBar.offsetHeight)) {
                        newTop = movieRect.bottom - (chatParentRect.top + showHideButton.offsetHeight + dragBar.offsetHeight);
                    }
                } else {
                    if (newTop > movieRect.bottom - (chatParentRect.top + chatContainer.offsetHeight)) {
                        newTop = movieRect.bottom - (chatParentRect.top + chatContainer.offsetHeight);
                    }
                }

                // Apply new position
                chatContainer.style.left = newLeft + "px";
                chatContainer.style.top = newTop + "px";

                e.preventDefault();
            }
        });

        // Handle pointer up event
        dragBar.addEventListener("pointerup", function (e) {
            dragBar.releasePointerCapture(e.pointerId);
            dragging = false;
            saveChatPosition();
        });
    }

    // Removes drag bar from chat container
    function removeDragBarWithOpacitySlider(chatContainer) {
        if (!chatContainer) return;
        const dragBar = chatContainer.querySelector('.chat-drag-bar');
        if (dragBar) dragBar.remove();
    }

    // Removes all chat-related styles from container
    function removeAllChatStyles(chatContainer) {
        removeStyle(styleRules.floatingChatStyleCollapsed);
        removeStyle(styleRules.floatingChatStyleExpanded);
        removeStyle(styleRules.floatingChatStyle);

        if (chatContainer) chatContainer.style = '';
    }

    /**
     * Applies saved chat style to container
     * @param {HTMLElement} chatContainer - The chat container
     */
    function applySavedChatStyle(chatContainer) {
        if (!chatContainer) return;

        const chatPrison = chatContainer.parentElement.getBoundingClientRect();

        // Apply width
        if (state.userSettings.chatSize?.width) {
            chatContainer.style.width = Math.min(
                window.innerWidth,
                parseFloat(state.userSettings.chatSize.width)
            ) + 'px';
        }

        // Apply height
        if (state.userSettings.chatSize?.height) {
            chatContainer.style.height = Math.min(
                window.innerHeight,
                parseFloat(state.userSettings.chatSize.height)
            ) + 'px';
        }

        // Apply left position
        if (state.userSettings.chatOffset?.left !== undefined) {
            const leftPos = parseFloat(state.userSettings.chatOffset.left);
            chatContainer.style.left = Math.min(
                Math.max(leftPos, chatPrison.left),
                window.innerWidth - chatPrison.width
            ) + 'px';
        }

        // Apply top position
        if (state.userSettings.chatOffset?.top !== undefined) {
            const topPos = parseFloat(state.userSettings.chatOffset.top);
            chatContainer.style.top = Math.min(
                Math.max(topPos, chatPrison.top),
                window.innerHeight - chatPrison.height
            ) + 'px';
        }

        // Apply opacity
        chatContainer.style.opacity = parseFloat(state.userSettings.chatOpacity);
    }

    // STYLE UPDATE FUNCTIONS
    /**
     * Updates styles based on current settings and state
     */
    function updateStyles() {
        try {
            // Enforce dependency: custom player height requires modifying player
            if (state.userSettings.useCustomPlayerHeight) {
                state.userSettings.modifyVideoPlayer = true;
            }

            // Check if script should be disabled
            const shouldNotActivate =
                !state.userSettings.isScriptActive ||
                (state.blacklist && state.blacklist.has(state.videoId)) ||
                (state.userSettings.enableOnlyForLiveStreams && !state.isLiveStream);

            if (shouldNotActivate) {
                removeAllStyles();
                if (state.moviePlayer && state.moviePlayer.setCenterCrop) {
                    state.moviePlayer.setCenterCrop();
                }
                return;
            }

            // Apply main styles based on settings
            setStyleState(styleRules.chatStyle, state.userSettings.modifyChat);
            setStyleState(styleRules.videoPlayerStyle, state.userSettings.modifyVideoPlayer);

            // Update header styles
            updateHeadmastStyle();

            // Reset player crop if needed
            if (state.moviePlayer && state.moviePlayer.setCenterCrop) {
                state.moviePlayer.setCenterCrop();
            }
        } catch (error) {
            logDebug(`Error when updating styles: ${error}`, 'error');
        }
    }

    /**
     * Updates the headmast styles based on current state
     */
    function updateHeadmastStyle() {
        updateLowHeadmastStyle();

        // Determine if headmast should be shrunk to account for chat
        const shouldShrinkHeadmast =
            state.isTheaterMode &&
            state.chatFrame?.getAttribute('theater-watch-while') === '' &&
            (state.userSettings.setLowHeadmast || state.userSettings.modifyChat);

        // Update chat width for style calculation
        state.chatWidth = state.chatFrame?.offsetWidth || 0;

        // Apply or remove headmast style
        setStyleState(styleRules.headmastStyle, shouldShrinkHeadmast);
    }

    /**
     * Updates low headmast style based on current state
     */
    function updateLowHeadmastStyle() {
        if (!state.moviePlayer) return;

        const shouldApplyLowHeadmast =
            state.userSettings.setLowHeadmast &&
            state.isTheaterMode &&
            !state.isFullscreen &&
            state.currentPageType === 'watch';

        setStyleState(styleRules.lowHeadmastStyle, shouldApplyLowHeadmast);
    }

    /**
     * Updates floating chat styles based on fullscreen state
     */
    function updateFullscreenFloatingChatStyle() {
        try {
            const chatContainer = document.querySelector('#chat-container');

            // Update collapsed/expanded styles based on chat state
            setStyleState(styleRules.floatingChatStyleCollapsed,
                state.chatCollapsed && state.isFullscreen);
            setStyleState(styleRules.floatingChatStyleExpanded,
                !state.chatCollapsed && state.isFullscreen);
            setStyleState(styleRules.floatingChatStyle, state.isFullscreen);

            // Configure floating chat if needed
            if (state.userSettings.isScriptActive &&
                state.userSettings.floatingChat &&
                state.isLiveStream &&
                chatContainer &&
                state.isFullscreen) {

                applySavedChatStyle(chatContainer);
                removeDragBarWithOpacitySlider(chatContainer);
                addDragBarWithOpacitySlider(chatContainer);
                addResizeHandles(chatContainer);

                // Apply saved settings
                chatContainer.style.opacity = parseFloat(state.userSettings.chatOpacity);
                chatContainer.style.width = Math.min(
                    window.innerWidth,
                    parseFloat(state.userSettings.chatSize.width)
                ) + 'px';
                chatContainer.style.height = Math.min(
                    window.innerHeight,
                    parseFloat(state.userSettings.chatSize.height)
                ) + 'px';
                chatContainer.style.left = parseFloat(state.userSettings.chatOffset.left) + 'px';
                chatContainer.style.top = parseFloat(state.userSettings.chatOffset.top) + 'px';
            } else if (chatContainer) {
                // Remove chat modifications if not needed
                removeAllChatStyles(chatContainer);
                removeDragBarWithOpacitySlider(chatContainer);
                removeResizeHandles(chatContainer);
            }
        } catch (error) {
            logDebug(`Error when updating fullscreen chat styles: ${error}`, 'error');
        }
    }

    /**
     * Updates debug visual indicators
     */
    function updateDebugStyles() {
        const chatContainer = document.querySelector('#chat-container');
        if (chatContainer) {
            if (state.userSettings.debug) {
                chatContainer.setAttribute("debug", "");
            } else {
                chatContainer.removeAttribute("debug");
            }
        }
    }

    // EVENT HANDLERS
    /**
     * Updates fullscreen status and related styles
     */
    function updateFullscreenStatus() {
        state.isFullscreen = !!document.fullscreenElement;
        updateFullscreenFloatingChatStyle();
    }

    /**
     * Handles theater mode toggle events
     * @param {Event} event - The theater mode event
     */
    function updateTheaterStatus(event) {
        state.isTheaterMode = !!event?.detail?.enabled;
        updateStyles();
    }

    /**
     * Handles chat status change events
     * @param {Event} event - The chat status event
     */
    function updateChatStatus(event) {
        state.chatFrame = event.target;
        state.chatCollapsed = event.detail !== false;
        updateFullscreenFloatingChatStyle();

        // Wait for player API to be ready before updating styles
        window.addEventListener('player-api-ready', updateStyles, { once: true });
    }

    /**
     * Updates movie player reference and sets up resize observer
     */
    function updateMoviePlayer() {
        const newMoviePlayer = document.querySelector('#movie_player');

        // Create resize observer if needed
        if (!state.resizeObserver) {
            state.resizeObserver = new ResizeObserver(() => {
                state.moviePlayerHeight = state.moviePlayer?.offsetHeight || 0;
                updateStyles();
            });
        }

        // Stop observing old player
        if (state.moviePlayer) {
            state.resizeObserver.unobserve(state.moviePlayer);
        }

        // Start observing new player
        state.moviePlayer = newMoviePlayer;
        if (state.moviePlayer) {
            state.resizeObserver.observe(state.moviePlayer);
        }
    }

    /**
     * Handles video status update events
     * @param {Event} event - The video status event
     */
    function updateVideoStatus(event) {
        try {
            state.currentPageType = event.detail.pageData.page;
            state.videoId = event.detail.pageData.playerResponse.videoDetails.videoId;
            state.isLiveStream = event.detail.pageData.playerResponse.videoDetails.isLiveContent;

            updateMoviePlayer();
            refreshMenuOptions();
        } catch (error) {
            logDebug(`Failed to update video status: ${error}`, 'error');
        }
    }

    // SETTINGS MANAGEMENT
    /**
     * Updates a setting in storage
     * @param {string} key - The setting key
     * @param {any} value - The setting value
     */
    async function updateSetting(key, value) {
        try {
            let currentSettings = await GM.getValue('settings', CONFIG.DEFAULT_SETTINGS);
            currentSettings[key] = value;
            await GM.setValue('settings', currentSettings);
            state.userSettings[key] = value;
        } catch (error) {
            logDebug(`Error updating setting: ${error}`, 'error');
        }
    }

    /**
     * Loads user settings from storage
     */
    async function loadUserSettings() {
        try {
            const storedSettings = await GM.getValue('settings', CONFIG.DEFAULT_SETTINGS);
            const newSettings = {};
            let needsSave = false;

            // Use stored settings or defaults
            for (const key in CONFIG.DEFAULT_SETTINGS) {
                if (key in storedSettings) {
                    newSettings[key] = storedSettings[key];
                } else {
                    newSettings[key] = CONFIG.DEFAULT_SETTINGS[key];
                    needsSave = true;
                }
            }

            // Check for obsolete settings
            for (const key in storedSettings) {
                if (!(key in CONFIG.DEFAULT_SETTINGS)) {
                    needsSave = true;
                }
            }

            // Save settings if needed
            state.userSettings = newSettings;
            if (needsSave) {
                await GM.setValue('settings', state.userSettings);
            }

            updateMode();
        } catch (error) {
            logDebug(`Error loading user settings: ${error}`, 'error');
            throw new Error(`Error loading user settings: ${error}. Aborting script.`);
        }
    }

    /**
     * Updates the mode (simple/advanced) based on user settings
     */
    function updateMode() {
        if (state.userSettings.isSimpleMode === true) {
            // Backup advanced settings before switching to simple mode
            state.advancedSettingsBackup = {
                ...state.userSettings,
                isSimpleMode: false
            };

            // Apply simple mode settings
            state.userSettings = {
                ...CONFIG.DEFAULT_SETTINGS,
                isScriptActive: state.userSettings.isScriptActive,
                isSimpleMode: true
            };

            logDebug('Using simple mode');
        } else if (state.advancedSettingsBackup) {
            // Restore advanced settings
            state.userSettings = {
                ...state.advancedSettingsBackup,
                isSimpleMode: false
            };

            logDebug('Using advanced mode');
            logDebug('Advanced settings backup:', state.advancedSettingsBackup);
        }

        logDebug(`Loaded settings: ${JSON.stringify(state.userSettings)}`);
    }

    /**
     * Loads video blacklist from storage
     */
    async function loadBlacklist() {
        try {
            let storedBlacklist = await GM.getValue('blacklist', CONFIG.DEFAULT_BLACKLIST);
            state.blacklist = new Set(
                Array.isArray(storedBlacklist) ? storedBlacklist : []
            );

            logDebug(`Loaded blacklist: ${JSON.stringify(Array.from(state.blacklist))}`);
        } catch (error) {
            logDebug(`Error loading blacklist: ${error}`, 'error');
            throw new Error(`Error loading blacklist: ${error}. Aborting script.`);
        }
    }

    /**
     * Updates the blacklist in storage
     */
    async function updateBlacklist() {
        try {
            await GM.setValue('blacklist', Array.from(state.blacklist));
        } catch (error) {
            logDebug(`Error updating blacklist: ${error}`, 'error');
        }
    }

    /**
     * Updates script info in storage and checks for updates
     */
    async function updateScriptInfo() {
        try {
            const oldScriptInfo = await GM.getValue('scriptInfo', null);
            const newScriptInfo = {
                version: getScriptVersionFromMeta(),
            };

            await GM.setValue('scriptInfo', newScriptInfo);

            // Check if script was updated
            if (!oldScriptInfo || compareVersions(newScriptInfo.version, oldScriptInfo?.version) !== 0) {
                state.isScriptRecentlyUpdated = true;
            }

            logDebug(`Previous script info: ${JSON.stringify(oldScriptInfo)}`);
            logDebug(`Updated script info: ${JSON.stringify(newScriptInfo)}`);
        } catch (error) {
            logDebug(`Error updating script info: ${error}`, 'error');
        }
    }

    /**
     * Cleans up old storage keys
     */
    async function cleanupOldStorage() {
        try {
            const allowedKeys = ['settings', 'scriptInfo', 'blacklist'];
            const keys = await GM.listValues();

            for (const key of keys) {
                if (!allowedKeys.includes(key)) {
                    await GM.deleteValue(key);
                    logDebug(`Deleted leftover key: ${key}`);
                }
            }
        } catch (error) {
            logDebug(`Error cleaning up old storage keys: ${error}`, 'error');
        }
    }

    // MENU MANAGEMENT
    /**
     * Removes all menu options
     */
    function removeMenuOptions() {
        state.menuItems.forEach((menuItem) => {
            GM.unregisterMenuCommand(menuItem);
        });
        state.menuItems.clear();
    }

    /**
     * Updates and shows menu options based on current state
     */
    async function refreshMenuOptions() {
        const shouldAutoClose = state.isOldTampermonkey;
        removeMenuOptions();

        // Advanced mode menu options
        const advancedMenuOptions = state.userSettings.isSimpleMode ? {} : {
            toggleOnlyLiveStreamMode: {
                alwaysShow: true,
                label: () => `${state.userSettings.enableOnlyForLiveStreams ? "✅" : "❌"} ${getLocalizedText().livestreamOnlyMode}`,
                menuId: "toggleOnlyLiveStreamMode",
                handleClick: async function () {
                    state.userSettings.enableOnlyForLiveStreams = !state.userSettings.enableOnlyForLiveStreams;
                    await updateSetting('enableOnlyForLiveStreams', state.userSettings.enableOnlyForLiveStreams);
                    updateStyles();
                    refreshMenuOptions();
                },
            },
            toggleChatStyle: {
                alwaysShow: true,
                label: () => `${state.userSettings.modifyChat ? "✅" : "❌"} ${getLocalizedText().applyChatStyles}`,
                menuId: "toggleChatStyle",
                handleClick: async function () {
                    state.userSettings.modifyChat = !state.userSettings.modifyChat;
                    await updateSetting('modifyChat', state.userSettings.modifyChat);
                    updateStyles();
                    refreshMenuOptions();
                },
            },
            ...(!state.userSettings.useCustomPlayerHeight ? {
                toggleVideoPlayerStyle: {
                    alwaysShow: true,
                    label: () => `${state.userSettings.modifyVideoPlayer ? "✅" : "❌"} ${getLocalizedText().applyVideoPlayerStyles}`,
                    menuId: "toggleVideoPlayerStyle",
                    handleClick: async function () {
                        state.userSettings.modifyVideoPlayer = !state.userSettings.modifyVideoPlayer;
                        await updateSetting('modifyVideoPlayer', state.userSettings.modifyVideoPlayer);
                        updateStyles();
                        refreshMenuOptions();
                    },
                },
            } : {}),
            toggleLowHeadmast: {
                alwaysShow: true,
                label: () => `${state.userSettings.setLowHeadmast ? "✅" : "❌"} ${getLocalizedText().moveHeadmastBelowVideoPlayer}`,
                menuId: "toggleLowHeadmast",
                handleClick: async function () {
                    state.userSettings.setLowHeadmast = !state.userSettings.setLowHeadmast;
                    await updateSetting('setLowHeadmast', state.userSettings.setLowHeadmast);
                    updateStyles();
                    refreshMenuOptions();
                },
            },
            toggleCustomPlayerHeight: {
                alwaysShow: true,
                label: () => `${state.userSettings.useCustomPlayerHeight ? "✅" : "❌"} ${getLocalizedText().useCustomPlayerHeight}`,
                menuId: "toggleCustomPlayerHeight",
                handleClick: async function () {
                    state.userSettings.useCustomPlayerHeight = !state.userSettings.useCustomPlayerHeight;
                    await updateSetting('useCustomPlayerHeight', state.userSettings.useCustomPlayerHeight);
                    updateStyles();
                    refreshMenuOptions();
                },
            },
            ...(state.userSettings.useCustomPlayerHeight ? {
                customHeightInputSelector: {
                    alwaysShow: true,
                    label: () => `🔢 ${getLocalizedText().playerHeightText} (${state.userSettings.playerHeightPx}px)`,
                    menuId: "customHeightInputSelector",
                    handleClick: async function () {
                        const playerHeightInputValue = await promptForNumber();
                        if (playerHeightInputValue === null) return;

                        state.userSettings.playerHeightPx = playerHeightInputValue;
                        await updateSetting('playerHeightPx', playerHeightInputValue);
                        updateStyles();
                        refreshMenuOptions();
                    },
                },
            } : {}),
            toggleFloatingChat: {
                alwaysShow: true,
                label: () => `${state.userSettings.floatingChat ? "✅" : "❌"} ${getLocalizedText().floatingChat}`,
                menuId: "toggleFloatingChat",
                handleClick: async function () {
                    state.userSettings.floatingChat = !state.userSettings.floatingChat;
                    await updateSetting('floatingChat', state.userSettings.floatingChat);
                    refreshMenuOptions();
                },
            },
            toggleDebug: {
                alwaysShow: true,
                label: () => `${state.userSettings.debug ? "✅" : "❌"} ${getLocalizedText().debug}`,
                menuId: "toggleDebug",
                handleClick: async function () {
                    state.userSettings.debug = !state.userSettings.debug;
                    await updateSetting('debug', state.userSettings.debug);
                    updateDebugStyles();
                    refreshMenuOptions();
                }
            }
        };

        // Common menu options for both simple and advanced modes
        const commonMenuOptions = {
            toggleScript: {
                alwaysShow: true,
                label: () => `🔄 ${state.userSettings.isScriptActive ? getLocalizedText().turnOff : getLocalizedText().turnOn}`,
                menuId: "toggleScript",
                handleClick: async function () {
                    state.userSettings.isScriptActive = !state.userSettings.isScriptActive;
                    await updateSetting('isScriptActive', state.userSettings.isScriptActive);
                    updateStyles();
                    refreshMenuOptions();
                },
            },
            addVideoToBlacklist: {
                alwaysShow: true,
                label: () => `🚫 ${state.blacklist.has(state.videoId) ? getLocalizedText().unblacklistVideo : getLocalizedText().blacklistVideo} [id: ${state.videoId}]`,
                menuId: "addVideoToBlacklist",
                handleClick: async function () {
                    if (state.blacklist.has(state.videoId)) {
                        state.blacklist.delete(state.videoId);
                    } else {
                        state.blacklist.add(state.videoId);
                    }
                    await updateBlacklist();
                    updateStyles();
                    refreshMenuOptions();
                },
            },
            toggleSimpleMode: {
                alwaysShow: true,
                label: () => `${state.userSettings.isSimpleMode ? "🚀 " + getLocalizedText().simpleMode : "🔧 " + getLocalizedText().advancedMode}`,
                menuId: "toggleSimpleMode",
                handleClick: async function () {
                    state.userSettings.isSimpleMode = !state.userSettings.isSimpleMode;
                    await updateSetting('isSimpleMode', state.userSettings.isSimpleMode);
                    updateMode();
                    updateStyles();
                    refreshMenuOptions();
                },
            },
        };

        // Combine menu options
        const menuOptions = {
            ...commonMenuOptions,
            ...advancedMenuOptions
        };

        // Process and register all menu options
        for (const [_, item] of Object.entries(menuOptions)) {
            if (!item.alwaysShow && !state.userSettings.expandMenu) continue;

            const menuId = GM.registerMenuCommand(item.label(), item.handleClick, {
                id: item.menuId,
                autoClose: shouldAutoClose,
            });

            state.menuItems.add(item.menuId);
        }
    }

    /**
     * Shows a number input prompt with validation
     * @param {string} message - The prompt message
     * @param {Function} validator - Optional validation function
     * @returns {number|null} The entered number or null if cancelled
     */
    async function promptForNumber(message = "Enter a number:", validator = null) {
        while (true) {
            const input = prompt(message);

            if (input === null) return null;

            const value = Number(input.trim());
            const isValidNumber = input.trim() !== "" && !isNaN(value);
            const passesCustomValidator = typeof validator === "function" ? validator(value) : true;

            if (isValidNumber && passesCustomValidator) {
                return value;
            } else {
                alert("⚠️ Please enter a valid number.");
            }
        }
    }

    // UTILITY FUNCTIONS
    /**
     * Logs debug information if debug mode is enabled
     * @param {string} message - The message to log
     * @param {string} [level='log'] - The log level ('log', 'warn', 'error')
     * @param {*} [data] - Optional data to log
     */
    function logDebug(message, level = 'log', data) {
        if (!state.userSettings.debug) return;

        const consoleMethod = console[level] || console.log;

        if (data !== undefined) {
            consoleMethod('[Better Theater] ' + message, data);
        } else {
            consoleMethod('[Better Theater] ' + message);
        }
    }

    /**
     * Compares two version strings
     * @param {string} v1 - First version string
     * @param {string} v2 - Second version string
     * @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
     */
    function compareVersions(v1, v2) {
        if (!v1 || !v2) return 0;

        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;
    }

    /**
     * Gets the script version from metadata
     * @returns {string} The script version
     */
    function getScriptVersionFromMeta() {
        const versionMatch = GM_info.scriptMetaStr.match(/@version\s+([^\r\n]+)/);
        return versionMatch ? versionMatch[1].trim() : null;
    }

    /**
     * Checks if we have the required Greasemonkey API
     * @returns {boolean} Whether the required API is available
     */
    function detectGreasemonkeyAPI() {
        if (typeof GM !== 'undefined') return true;

        if (typeof GM_info !== 'undefined') {
            state.useCompatibilityMode = true;
            logDebug("Running in compatibility mode", 'warn');
            return true;
        }

        return false;
    }

    /**
     * Checks if Tampermonkey is up to date
     */
    function checkTampermonkeyVersion() {
        if (GM_info.scriptHandler === "Tampermonkey" &&
            compareVersions(GM_info.version, CONFIG.REQUIRED_VERSIONS.Tampermonkey) !== 1) {

            state.isOldTampermonkey = true;

            if (state.isScriptRecentlyUpdated) {
                GM.notification({
                    text: getLocalizedText().tampermonkeyOutdatedAlert,
                    timeout: 15000
                });
            }
        }
    }

    /**
     * Checks if the current page is a live chat iframe
     * @returns {boolean} Whether this is a live chat iframe
     */
    function isLiveChatIFrame() {
        return /^https?:\/\/.*youtube\.com\/live_chat.*$/.test(window.location.href);
    }

    // EVENT LISTENERS
    /**
     * Attaches all necessary event listeners
     */
    function attachEventListeners() {
        // YouTube-specific events
        window.addEventListener('yt-set-theater-mode-enabled', updateTheaterStatus, true);
        window.addEventListener('yt-chat-collapsed-changed', updateChatStatus, true);
        window.addEventListener('yt-page-data-fetched', updateVideoStatus, true);
        window.addEventListener('yt-page-data-updated', updateStyles, true);

        // Standard events
        window.addEventListener('fullscreenchange', updateFullscreenStatus, true);
        window.addEventListener('yt-navigate-finish', updateDebugStyles, { once: true });
    }

    // INITIALIZATION
    /**
     * Initializes the script
     */
    async function initialize() {
        try {
            // Check for Greasemonkey API
            if (!detectGreasemonkeyAPI()) {
                throw new Error("Did not detect valid Greasemonkey API");
            }

            // Initialize static styles
            applyStyle(styleRules.debugResizeHandleStyle, true);

            // Clean up and load settings
            await cleanupOldStorage();
            await loadUserSettings();
            await loadBlacklist();
            await updateScriptInfo();

            // Check Tampermonkey version
            checkTampermonkeyVersion();

            // Handle iframe case
            if (isLiveChatIFrame()) {
                applyStyle(styleRules.chatFrameFixStyle, true);
                return;
            }

            // Apply fixes and initialize
            applyStyle(styleRules.chatRendererFixStyle, true);
            applyStyle(styleRules.videoPlayerFixStyle, true);
            updateStyles();
            attachEventListeners();
            refreshMenuOptions();

        } catch (error) {
            logDebug(`Error when initializing script: ${error}. Aborting script.`, 'error');
        }
    }

    // Start the script
    initialize();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址