更佳 YouTube 剧场模式

改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性,也达到了类似B站的网页全屏功能。同时新增可选的、自制风格的浮动聊天室功能(仅限全fullscreen模式),融入了 YouTube 原有的设计语言。

安装此脚本
作者推荐脚本

您可能也喜欢YouTube Music Opus Codec

安装此脚本
// ==UserScript==
// @name                 Better YouTube Theater Mode
// @name:zh-TW           更佳 YouTube 劇場模式
// @name:zh-CN           更佳 YouTube 剧场模式
// @name:ja              より良いYouTubeシアターモード
// @icon                 https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @author               ElectroKnight22
// @namespace            electroknight22_youtube_better_theater_mode_namespace
// @version              2.0.0
// @match                *://www.youtube.com/*
// @match                *://www.youtube-nocookie.com/*
// @noframes
// @grant                GM.getValue
// @grant                GM.setValue
// @grant                GM.deleteValue
// @grant                GM.listValues
// @grant                GM.registerMenuCommand
// @grant                GM.unregisterMenuCommand
// @grant                GM.notification
// @grant                GM_getValue
// @grant                GM_setValue
// @grant                GM_deleteValue
// @grant                GM_listValues
// @grant                GM_registerMenuCommand
// @grant                GM_unregisterMenuCommand
// @grant                GM_notification
// @run-at               document-start
// @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 adds an optional, customized floating chat for fullscreen mode, seamlessly integrated with YouTube's design.
// @description:zh-TW    改善 YouTube 劇場模式,參考 Twitch.tv 的設計,增強影片與聊天室佈局,同時維持效能與相容性。另新增可選的、自製風格的浮動聊天室功能(僅限全螢幕模式),與 YouTube 原有的設計語言相融合。
// @description:zh-CN    改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性,也达到了类似B站的网页全屏功能。同时新增可选的、自制风格的浮动聊天室功能(仅限全fullscreen模式),融入了 YouTube 原有的设计语言。
// @description:ja       YouTubeのシアターモードを改善し、Twitch.tvのデザインを参考にして、動画とチャットのレイアウトを強化しつつ、パフォーマンスと互換性を維持します。また、全画面モード専用のオプションとして、カスタマイズ済みフローティングチャット機能を、YouTubeのデザイン言語に沿って統合しています。
// ==/UserScript==
// Note: Both GM.* and GM_.* are granted for compatibility with older script managers.

/*jshint esversion: 11 */
(function () {
    'use strict';

    const CONFIG = {
        DRAG_BAR_HEIGHT: '35px',
        MIN_CHAT_SIZE: { // YouTube chat minimum size is 300px by 320px, going smaller would require a lot of CSS overrides.
            width: 300, // px
            height: 355, // px (320 + DRAG_BAR_HEIGHT)
        },
        DEFAULT_SETTINGS: {
            isSimpleMode: true,
            enableOnlyForLiveStreams: false,
            modifyVideoPlayer: true,
            modifyChat: true,
            setLowHeadmast: false,
            useCustomPlayerHeight: false,
            playerHeightPx: 600,
            floatingChat: false,
            get theaterChatWidth() { return `${CONFIG.MIN_CHAT_SIZE.width}px`; },
            chatStyle: {
                left: '0px',
                top: '-500px',
                get width() { return `${CONFIG.MIN_CHAT_SIZE.width}px`; },
                get height() { return `${CONFIG.MIN_CHAT_SIZE.height}px`; },
                opacity: '0.95',
            },
            debug: false,
        },
        DEFAULT_BLACKLIST: [],
        REQUIRED_VERSIONS: {
            Tampermonkey: '5.4.624',
        },
    };

    const BROWSER_LANGUAGE = navigator.language ?? navigator.userLanguage;

    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 getPreferredLanguage() {
        if (TRANSLATIONS[BROWSER_LANGUAGE]) {
            return BROWSER_LANGUAGE;
        }
        if (BROWSER_LANGUAGE.startsWith('zh')) {
            return 'zh-CN'; // Default to Simplified Mainland Chinese if Chinese variant is not available or not specified.
        }
        return 'en-US'; // Default to US English if all else fails.
    }

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

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

    const DOM = {
        moviePlayer: null,
        chatContainer: null,
        chatFrame: null,
        ytdWatchFlexy: null,
    };

    const createGmApi = () => {
        const isGmFallback = typeof GM === 'undefined' && typeof GM_info !== 'undefined';
        state.gmFallback = isGmFallback;

        if (isGmFallback) {
            return {
                registerMenuCommand: GM_registerMenuCommand,
                unregisterMenuCommand: GM_unregisterMenuCommand,
                getValue: GM_getValue,
                setValue: GM_setValue,
                listValues: GM_listValues,
                deleteValue: GM_deleteValue,
                notification: GM_notification,
                info: () => GM_info,
            };
        }

        return {
            registerMenuCommand: (...args) => window.GM?.registerMenuCommand?.(...args),
            unregisterMenuCommand: (...args) => window.GM?.unregisterMenuCommand?.(...args),
            getValue: (...args) => window.GM?.getValue?.(...args),
            setValue: (...args) => window.GM?.setValue?.(...args),
            listValues: (...args) => window.GM?.listValues?.(...args),
            deleteValue: (...args) => window.GM?.deleteValue?.(...args),
            notification: (...args) => window.GM?.notification?.(...args),
            info: () => window.GM?.info,
        };
    };

    const GM_API = createGmApi();

    const Utils = {
        log(message, level = 'log', data) {
            if (!state.userSettings.debug) return;
            const consoleMethod = console[level] || console.log;
            const prefix = '[Better Theater]';
            data !== undefined ? consoleMethod(prefix, message, data) : consoleMethod(prefix, message);
        },
        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;
        },
        async 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 passesValidator = typeof validator === 'function' ? validator(value) : true;

                if (isValidNumber && passesValidator) return value;
                alert('⚠️ Please enter a valid number.');
            }
        },
    };

    const StyleManager = {
        styleDefinitions: {
            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;
                    }
                    #chat-container { z-index: 2021 !important; }
                `,
            },
            videoPlayerStyle: {
                id: 'betterTheater-videoPlayerStyle',
                getRule: () =>
                    state.userSettings.useCustomPlayerHeight
                        ? `ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
                            min-height: 0px !important;
                            height: ${state.userSettings.playerHeightPx}px !important;
                        }`
                        : `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; }
                `,
            },
            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}px !important;
                        min-height: 0 !important;
                        max-width: 100vw !important;
                        max-height: 100vh !important;
                        position: absolute;
                        border-radius: 0 0 12px 12px !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 - parseInt(CONFIG.DRAG_BAR_HEIGHT)}px !important;
                        max-height: 100vh !important;
                        pointer-events: auto !important;
                    }
                    #chat[collapsed] {
                        height: ${CONFIG.DRAG_BAR_HEIGHT} !important;
                        min-height: ${CONFIG.DRAG_BAR_HEIGHT} !important;
                    }
                    .chat-drag-bar {
                        cursor: move !important;
                        pointer-events: auto !important;
                    }
                `,
            },
            floatingChatStyleExpanded: {
                id: 'betterTheater-floatingChatStyleExpanded',
                getRule: () => `
                    #chat-container { min-height: ${CONFIG.MIN_CHAT_SIZE.height}px !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: 1px solid var(--yt-spec-10-percent-layer) !important;
                        background-clip: padding-box !important;
                        border-top: none !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; }
                    #chat-container { pointer-events: none !important; }
                `,
            },
            debugResizeHandleStyle: {
                id: 'betterTheater-debugResizeHandleStyle',
                getRule: () => `
                    #chat-container .chat-resize-handle { background: transparent; opacity: 0; }
                    #chat-container[debug] .chat-resize-handle { opacity: 0.5; }
                    #chat-container[debug] .rs-right { background: rgba(255, 0, 0, 0.5); }
                    #chat-container[debug] .rs-left { background: rgba(0, 255, 0, 0.5); }
                    #chat-container[debug] .rs-bottom { background: rgba(0, 0, 255, 0.5); }
                    #chat-container[debug] .rs-top { background: rgba(255, 255, 0, 0.5); }
                    #chat-container[debug] .rs-bottom-left { background: rgba(0, 255, 255, 0.5); }
                    #chat-container[debug] .rs-top-left { background: rgba(255, 255, 0, 0.5); }
                    #chat-container[debug] .rs-top-right { background: rgba(255, 0, 0, 0.5); }
                    #chat-container[debug] .rs-bottom-right { background: rgba(255, 0, 255, 0.5); }
                `,
            },
            chatSliderStyle: {
                id: 'betterTheater-chatSliderStyle',
                getRule: () => `
                    .chat-drag-bar input[type=range] {
                        -webkit-appearance: none; appearance: none;
                        width: 100px; height: 4px; border-radius: 2px;
                        background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
                        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;
                    }
                `,
            },
            chatClampLimits: {
                id: 'betterTheater-chatClampLimits',
                getRule: () => {
                    const hostEl = DOM.ytdWatchFlexy;
                    const originalWidth = '402px';
                    const originalMinWidth = '402px';

                    if (hostEl) {
                        const style = window.getComputedStyle(hostEl);
                        const fetchedWidth = style.getPropertyValue('--ytd-watch-flexy-sidebar-width')?.trim();
                        const fetchedMinWidth = style.getPropertyValue('--ytd-watch-flexy-sidebar-min-width')?.trim();
                        return `
                            ytd-live-chat-frame[theater-watch-while] {
                                min-width: ${CONFIG.MIN_CHAT_SIZE.width}px !important;
                                max-width: 33.33vw !important;
                            }
                            .ytd-watch-flexy {
                                --ytd-watch-flexy-sidebar-width: clamp(${
                                    CONFIG.MIN_CHAT_SIZE.width
                                }px, var(--bt-chat-width), 33.33vw) !important;
                                --ytd-watch-flexy-sidebar-min-width: clamp(${
                                    CONFIG.MIN_CHAT_SIZE.width
                                }px, var(--bt-chat-width), 33.33vw) !important;
                            }
                            ytd-watch-flexy[flexy] #secondary.ytd-watch-flexy {
                                --ytd-watch-flexy-sidebar-width: ${fetchedWidth ?? originalWidth} !important;
                                --ytd-watch-flexy-sidebar-min-width: ${fetchedMinWidth ?? originalMinWidth} !important;
                            }
                            ytd-watch-next-secondary-results-renderer {
                                --ytd-reel-item-compact-layout-width: calc((${
                                    fetchedWidth ?? originalWidth
                                } - 8px) / 3) !important;
                                --ytd-reel-item-thumbnail-height: calc((${
                                    fetchedWidth ?? originalWidth
                                } / 3 / 9 * 16)) !important;
                            }
                            ytd-live-chat-frame[theater-watch-while] yt-live-chat-renderer {
                                width: 100% !important; max-width: 100% !important;
                            }
                        `;
                    }
                    return ''; // Return empty if host element not found
                },
            },
        },
        apply(styleDef, isPersistent = false) {
            if (typeof styleDef.getRule !== 'function') return;
            this.remove(styleDef); // Ensure no duplicates

            const styleElement = document.createElement('style');
            styleElement.id = styleDef.id;
            styleElement.textContent = styleDef.getRule();
            document.head.appendChild(styleElement);
            state.activeStyles.set(styleDef.id, {
                element: styleElement,
                persistent: isPersistent,
            });
        },
        remove(styleDef) {
            const styleData = state.activeStyles.get(styleDef.id);
            if (styleData) {
                styleData.element?.remove();
                state.activeStyles.delete(styleDef.id);
            }
        },
        removeAll() {
            const styleIdsToRemove = [...state.activeStyles.keys()];
            styleIdsToRemove.forEach((styleId) => {
                const styleData = state.activeStyles.get(styleId);
                if (styleData && !styleData.persistent) {
                    this.remove({ id: styleId });
                }
            });
        },
        toggle(styleDef, condition) {
            condition ? this.apply(styleDef) : this.remove(styleDef);
        },
    };

    const SettingsManager = {
        async update(key, value) {
            try {
                const settings = await GM_API.getValue('settings', CONFIG.DEFAULT_SETTINGS);
                settings[key] = value;
                await GM_API.setValue('settings', settings);
                state.userSettings[key] = value;
            } catch (error) {
                Utils.log(`Error updating setting: ${key}`, 'error', error);
            }
        },
        async load() {
            try {
                state.versionWarningShown = await GM_API.getValue('versionWarningShown', false);
                const storedSettings = await GM_API.getValue('settings', CONFIG.DEFAULT_SETTINGS);
                const newSettings = {
                    ...CONFIG.DEFAULT_SETTINGS,
                    ...storedSettings,
                };

                state.userSettings = newSettings;
                if (Object.keys(storedSettings).length !== Object.keys(newSettings).length) {
                    await GM_API.setValue('settings', state.userSettings);
                }
                this.updateMode();
            } catch (error) {
                Utils.log('Error loading settings', 'error', error);
                throw error;
            }
        },
        updateMode() {
            if (state.userSettings.isSimpleMode) {
                if (!state.advancedSettingsBackup) {
                    state.advancedSettingsBackup = {
                        ...state.userSettings,
                        isSimpleMode: false,
                    };
                }
                state.userSettings = {
                    ...CONFIG.DEFAULT_SETTINGS,
                    isSimpleMode: true,
                };
                Utils.log('Switched to Simple Mode');
            } else if (state.advancedSettingsBackup) {
                state.userSettings = {
                    ...state.advancedSettingsBackup,
                    isSimpleMode: false,
                };
                state.advancedSettingsBackup = null;
                App.warnIfOldTampermonkey();
                Utils.log('Switched to Advanced Mode', 'log', state.userSettings);
            }
            Utils.log('Loaded settings:', 'log', state.userSettings);
        },
        async loadBlacklist() {
            try {
                const stored = await GM_API.getValue('blacklist', CONFIG.DEFAULT_BLACKLIST);
                state.blacklist = new Set(Array.isArray(stored) ? stored : []);
                Utils.log('Loaded blacklist:', 'log', Array.from(state.blacklist));
            } catch (error) {
                Utils.log('Error loading blacklist', 'error', error);
                throw error;
            }
        },
        async updateBlacklist() {
            try {
                await GM_API.setValue('blacklist', Array.from(state.blacklist));
            } catch (error) {
                Utils.log('Error updating blacklist', 'error', error);
            }
        },
        async cleanupStorage() {
            try {
                const allowedKeys = ['settings', 'blacklist', 'versionWarningShown'];
                const keys = await GM_API.listValues();
                for (const key of keys) {
                    if (!allowedKeys.includes(key)) {
                        await GM_API.deleteValue(key);
                        Utils.log(`Deleted leftover key: ${key}`);
                    }
                }
            } catch (error) {
                Utils.log('Error cleaning up old storage', 'error', error);
            }
        },
    };

    const MenuManager = {
        clear() {
            state.menuItems.forEach((menuId) => GM_API.unregisterMenuCommand(menuId));
            state.menuItems.clear();
        },
        refresh() {
            this.clear();
            const LABEL = getLocalizedText();
            const shouldAutoClose = state.isOldTampermonkey;

            const menuConfig = [
                // Always visible
                {
                    label: () =>
                        `🚫 ${
                            state.blacklist.has(state.videoId) ? LABEL.unblacklistVideo : LABEL.blacklistVideo
                        } [id: ${state.videoId}]`,
                    id: 'toggleBlacklist',
                    action: async () => {
                        state.blacklist.has(state.videoId)
                            ? state.blacklist.delete(state.videoId)
                            : state.blacklist.add(state.videoId);
                        await SettingsManager.updateBlacklist();
                        App.updateAllStyles();
                    },
                },
                {
                    label: () =>
                        `${state.userSettings.isSimpleMode ? '🚀 ' + LABEL.simpleMode : '🔧 ' + LABEL.advancedMode}`,
                    id: 'toggleMode',
                    action: async () => {
                        await SettingsManager.update('isSimpleMode', !state.userSettings.isSimpleMode);
                        SettingsManager.updateMode();
                        App.updateAllStyles();
                    },
                },
                // Advanced only
                {
                    label: () =>
                        `${state.userSettings.enableOnlyForLiveStreams ? '✅' : '❌'} ${LABEL.livestreamOnlyMode}`,
                    id: 'toggleLiveOnly',
                    action: () =>
                        SettingsManager.update(
                            'enableOnlyForLiveStreams',
                            !state.userSettings.enableOnlyForLiveStreams
                        ).then(App.updateAllStyles),
                    advanced: true,
                },
                {
                    label: () => `${state.userSettings.modifyChat ? '✅' : '❌'} ${LABEL.applyChatStyles}`,
                    id: 'toggleChatStyle',
                    action: () =>
                        SettingsManager.update('modifyChat', !state.userSettings.modifyChat).then(App.updateAllStyles),
                    advanced: true,
                },
                {
                    label: () =>
                        `${state.userSettings.modifyVideoPlayer ? '✅' : '❌'} ${LABEL.applyVideoPlayerStyles}`,
                    id: 'toggleVideoStyle',
                    action: () =>
                        SettingsManager.update('modifyVideoPlayer', !state.userSettings.modifyVideoPlayer).then(
                            App.updateAllStyles
                        ),
                    advanced: true,
                    condition: () => !state.userSettings.useCustomPlayerHeight,
                },
                {
                    label: () => `${state.userSettings.setLowHeadmast ? '✅' : '❌'} ${LABEL.moveHeadmastBelowVideoPlayer}`,
                    id: 'toggleLowHeadmast',
                    action: () =>
                        SettingsManager.update('setLowHeadmast', !state.userSettings.setLowHeadmast).then(
                            App.updateAllStyles
                        ),
                    advanced: true,
                },
                {
                    label: () =>
                        `${state.userSettings.useCustomPlayerHeight ? '✅' : '❌'} ${LABEL.useCustomPlayerHeight}`,
                    id: 'toggleCustomHeight',
                    action: () =>
                        SettingsManager.update('useCustomPlayerHeight', !state.userSettings.useCustomPlayerHeight).then(
                            App.updateAllStyles
                        ),
                    advanced: true,
                },
                {
                    label: () => `🔢 ${LABEL.playerHeightText} (${state.userSettings.playerHeightPx}px)`,
                    id: 'setCustomHeight',
                    action: async () => {
                        const newHeight = await Utils.promptForNumber();
                        if (newHeight !== null) {
                            await SettingsManager.update('playerHeightPx', newHeight);
                            App.updateAllStyles();
                        }
                    },
                    advanced: true,
                    condition: () => state.userSettings.useCustomPlayerHeight,
                },
                {
                    label: () => `${state.userSettings.floatingChat ? '✅' : '❌'} ${LABEL.floatingChat}`,
                    id: 'toggleFloatingChat',
                    action: () =>
                        SettingsManager.update('floatingChat', !state.userSettings.floatingChat).then(
                            App.updateAllStyles
                        ),
                    advanced: true,
                },
                {
                    label: () => `${state.userSettings.debug ? '✅' : '❌'} ${LABEL.debug}`,
                    id: 'toggleDebug',
                    action: async () => {
                        await SettingsManager.update('debug', !state.userSettings.debug);
                        App.updateDebugStyles();
                    },
                    advanced: true,
                },
            ];

            menuConfig.forEach((item) => {
                const isAdvancedItem = item.advanced;
                const inAdvancedMode = !state.userSettings.isSimpleMode;
                const conditionMet = item.condition ? item.condition() : true;

                if (conditionMet && (!isAdvancedItem || inAdvancedMode)) {
                    const commandId = GM_API.registerMenuCommand(
                        item.label(),
                        async () => {
                            await item.action();
                            this.refresh();
                        },
                        { id: item.id, autoClose: shouldAutoClose }
                    );
                    state.menuItems.add(commandId ?? item.id);
                }
            });
        },
    };

    const ChatInteractionManager = {
        addTheaterResizeHandle() {
            if (window.innerWidth / 3 <= CONFIG.MIN_CHAT_SIZE.width) return;
            const chat = DOM.chatFrame;
            if (!chat || chat.querySelector('#chat-width-resize-handle')) return;

            const ytdWatchFlexy = DOM.ytdWatchFlexy;
            const storedWidth = state.userSettings.theaterChatWidth ?? `${CONFIG.MIN_CHAT_SIZE.width}px`;
            this._applyTheaterWidth(ytdWatchFlexy, chat, storedWidth);

            const handle = document.createElement('div');
            handle.id = 'chat-width-resize-handle';
            handle.className = 'style-scope ytd-live-chat-frame';
            Object.assign(handle.style, {
                position: 'absolute',
                top: '0',
                left: '0',
                width: '6px',
                height: '100%',
                cursor: 'ew-resize',
                zIndex: '10001',
            });
            chat.appendChild(handle);

            let startX = 0,
                startWidth = 0,
                animationFrame;

            const onPointerMove = (e) => {
                if (!handle.hasPointerCapture(e.pointerId)) return;
                cancelAnimationFrame(animationFrame);
                animationFrame = requestAnimationFrame(() => {
                    const dx = startX - e.clientX;
                    const newWidth = Math.max(CONFIG.MIN_CHAT_SIZE.width, startWidth + dx);
                    this._applyTheaterWidth(ytdWatchFlexy, chat, `${newWidth}px`);
                });
            };

            const onPointerUp = (e) => {
                handle.releasePointerCapture(e.pointerId);
                document.removeEventListener('pointermove', onPointerMove);
                document.removeEventListener('pointerup', onPointerUp);
                SettingsManager.update('theaterChatWidth', ytdWatchFlexy.style.getPropertyValue('--bt-chat-width'));
            };

            handle.addEventListener('pointerdown', (e) => {
                if (e.pointerType === 'mouse' && e.button !== 0) return;
                e.preventDefault();
                document.body.click(); // Deselect any text
                startX = e.clientX;
                startWidth = chat.getBoundingClientRect().width;
                handle.setPointerCapture(e.pointerId);
                document.addEventListener('pointermove', onPointerMove);
                document.addEventListener('pointerup', onPointerUp);
            });
        },
        _applyTheaterWidth(flexy, chat, widthCss) {
            if (flexy) flexy.style.setProperty('--bt-chat-width', widthCss);
            if (chat) {
                chat.style.width = widthCss;
                chat.style.zIndex = '1999';
            }
        },
        removeTheaterResizeHandle() {
            DOM.chatFrame?.querySelector('#chat-width-resize-handle')?.remove();
            const flexy = DOM.ytdWatchFlexy;
            const chat = DOM.chatFrame;
            if (flexy) flexy.style.removeProperty('--bt-chat-width');
            if (chat) {
                chat.style.width = '';
                chat.style.zIndex = '';
            }
        },

        initFullscreenChat(chatContainer) {
            this.applySavedChatStyle(chatContainer, true);
            this.addDragBar(chatContainer);
            this.addResizeHandles(chatContainer);
        },
        cleanupFullscreenChat(chatContainer) {
            this.removeDragBar(chatContainer);
            this.removeResizeHandles(chatContainer);
            chatContainer.style.cssText = '';
        },
        applySavedChatStyle(chatContainer, shouldSave = false) {
            if (!chatContainer || !DOM.moviePlayer) return;

            const movieRect = DOM.moviePlayer.getBoundingClientRect();
            if (movieRect.width === 0 || movieRect.height === 0) return;

            const parentRect = chatContainer.parentElement.getBoundingClientRect();
            const { width, height, top, left, opacity } = state.userSettings.chatStyle;

            const parsedWidth = parseFloat(width) ?? CONFIG.MIN_CHAT_SIZE.width;
            const parsedHeight = parseFloat(height) ?? CONFIG.MIN_CHAT_SIZE.height;

            let newWidth = Math.min(Math.max(CONFIG.MIN_CHAT_SIZE.width, parsedWidth), movieRect.width);
            let newHeight = Math.min(Math.max(CONFIG.MIN_CHAT_SIZE.height, parsedHeight), movieRect.height);

            const parsedTop = parseFloat(top) ?? 0;
            const parsedLeft = parseFloat(left) ?? 0;

            const minTop = movieRect.top - parentRect.top;
            const maxTop = movieRect.bottom - parentRect.top - newHeight;
            const minLeft = movieRect.left - parentRect.left;
            const maxLeft = movieRect.right - parentRect.left - newWidth;

            let newTop = Math.max(minTop, Math.min(parsedTop, maxTop));
            let newLeft = Math.max(minLeft, Math.min(parsedLeft, maxLeft));

            Object.assign(chatContainer.style, {
                width: `${newWidth}px`,
                height: `${newHeight}px`,
                top: `${newTop}px`,
                left: `${newLeft}px`,
                opacity: parseFloat(opacity) ?? 0.95,
            });

            if (shouldSave && (newTop !== parsedTop || newLeft !== parsedLeft)) {
                this.saveChatStyle(chatContainer);
            }
        },
        saveChatStyle(chatContainer) {
            const style = {
                width: chatContainer.style.width,
                height: chatContainer.style.height,
                left: chatContainer.style.left,
                top: chatContainer.style.top,
                opacity: chatContainer.style.opacity,
            };
            return SettingsManager.update('chatStyle', style);
        },
        addDragBar(chatContainer) {
            if (chatContainer.querySelector('.chat-drag-bar')) return;
            StyleManager.apply(StyleManager.styleDefinitions.chatSliderStyle, true);

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

            const dragHandleIcon = document.createElement('div');
            dragHandleIcon.textContent = '⋮⋮';
            Object.assign(dragHandleIcon.style, {
                fontSize: '18px',
                userSelect: 'none',
            });

            const opacitySlider = document.createElement('input');
            opacitySlider.type = 'range';
            opacitySlider.min = '20';
            opacitySlider.max = '100';
            opacitySlider.value = String(Math.round((parseFloat(state.userSettings.chatStyle.opacity) ?? 0.95) * 100));

            opacitySlider.addEventListener(
                'input',
                () => (chatContainer.style.opacity = String(opacitySlider.value / 100))
            );
            opacitySlider.addEventListener('mouseup', () => this.saveChatStyle(chatContainer));
            opacitySlider.addEventListener('pointerdown', (e) => e.stopPropagation());

            dragBar.appendChild(dragHandleIcon);
            dragBar.appendChild(opacitySlider);

            chatContainer.insertBefore(dragBar, chatContainer.firstChild);
            this._initDrag(dragBar, chatContainer);
        },
        _initDrag(dragBar, chatContainer) {
            let start = {},
                parentRect,
                movieRect,
                animationFrame,
                isDragging = false;

            const onPointerMove = (e) => {
                if (!isDragging) return;

                const isOutside =
                    e.clientX < movieRect.left ||
                    e.clientX > movieRect.right ||
                    e.clientY < movieRect.top ||
                    e.clientY > movieRect.bottom;
                if (isOutside) return;

                cancelAnimationFrame(animationFrame);
                animationFrame = requestAnimationFrame(() => {
                    const newLeft = e.clientX - start.offsetX;
                    const newTop = e.clientY - start.offsetY;

                    const isChatCollapsed = chatContainer.querySelector('#chat[collapsed]');
                    const showHideButtonHeight = chatContainer.querySelector('#show-hide-button')?.offsetHeight ?? 0;
                    const chatHeight = isChatCollapsed
                        ? parseInt(CONFIG.DRAG_BAR_HEIGHT) + showHideButtonHeight
                        : chatContainer.offsetHeight;

                    const clampedLeft = Math.max(
                        movieRect.left,
                        Math.min(newLeft, movieRect.right - chatContainer.offsetWidth)
                    );
                    const clampedTop = Math.max(movieRect.top, Math.min(newTop, movieRect.bottom - chatHeight));

                    chatContainer.style.left = `${clampedLeft - parentRect.left}px`;
                    chatContainer.style.top = `${clampedTop - parentRect.top}px`;
                });
            };

            const onPointerUp = (e) => {
                isDragging = false;
                dragBar.releasePointerCapture(e.pointerId);
                document.removeEventListener('pointermove', onPointerMove);
                document.removeEventListener('pointerup', onPointerUp);
                document.removeEventListener('pointercancel', onPointerUp);
                this.saveChatStyle(chatContainer);
            };

            dragBar.addEventListener('pointerdown', (e) => {
                if (e.pointerType === 'mouse' && e.button !== 0) return;
                e.preventDefault();
                isDragging = true;
                dragBar.setPointerCapture(e.pointerId);

                parentRect = chatContainer.parentElement.getBoundingClientRect();
                movieRect = DOM.moviePlayer.getBoundingClientRect();
                const chatRect = chatContainer.getBoundingClientRect();
                start = {
                    offsetX: e.clientX - chatRect.left,
                    offsetY: e.clientY - chatRect.top,
                };

                document.addEventListener('pointermove', onPointerMove);
                document.addEventListener('pointerup', onPointerUp);
                document.addEventListener('pointercancel', onPointerUp);
            });
        },
        removeDragBar(chatContainer) {
            chatContainer?.querySelector('.chat-drag-bar')?.remove();
            StyleManager.remove(StyleManager.styleDefinitions.chatSliderStyle);
        },
        addResizeHandles(chatContainer) {
            const handleConfigs = {
                right: {
                    cursor: 'ew-resize',
                    right: '0',
                    width: '6px',
                    top: '0',
                    bottom: '0',
                },
                left: {
                    cursor: 'ew-resize',
                    left: '0',
                    width: '6px',
                    top: '0',
                    bottom: '0',
                },
                bottom: {
                    cursor: 'ns-resize',
                    bottom: '0',
                    height: '6px',
                    left: '0',
                    right: '0',
                },
                top: {
                    cursor: 'ns-resize',
                    top: '0',
                    height: '6px',
                    left: '0',
                    right: '0',
                },
                bottomLeft: {
                    cursor: 'nesw-resize',
                    left: '0',
                    bottom: '0',
                    width: '12px',
                    height: '12px',
                },
                topLeft: {
                    cursor: 'nwse-resize',
                    left: '0',
                    top: '0',
                    width: '12px',
                    height: '12px',
                },
                topRight: {
                    cursor: 'nesw-resize',
                    right: '0',
                    top: '0',
                    width: '12px',
                    height: '12px',
                },
                bottomRight: {
                    cursor: 'nwse-resize',
                    right: '0',
                    bottom: '0',
                    width: '12px',
                    height: '12px',
                },
            };

            for (const [pos, config] of Object.entries(handleConfigs)) {
                if (chatContainer.querySelector(`.rs-${pos}`)) continue;
                const handle = document.createElement('div');
                handle.className = `chat-resize-handle rs-${pos}`;
                Object.assign(handle.style, {
                    position: 'absolute',
                    zIndex: '10001',
                    ...config,
                });
                chatContainer.appendChild(handle);
                this._initResize(handle, chatContainer);
            }
        },
        _initResize(handle, chatContainer) {
            let start, parentRect, movieRect, animationFrame;

            const onPointerMove = (e) => {
                if (!handle.hasPointerCapture(e.pointerId)) return;
                cancelAnimationFrame(animationFrame);
                animationFrame = requestAnimationFrame(() => {
                    const dx = e.clientX - start.x;
                    const dy = e.clientY - start.y;

                    let newWidth = start.width;
                    let newHeight = start.height;
                    let newLeft = start.left;
                    let newTop = start.top;

                    const className = handle.className.toLowerCase();
                    const isLeft = className.includes('left');
                    const isRight = className.includes('right');
                    const isTop = className.includes('top');
                    const isBottom = className.includes('bottom');

                    if (isRight) newWidth += dx;
                    if (isLeft) {
                        newWidth -= dx;
                        newLeft += dx;
                    }

                    if (isBottom) newHeight += dy;
                    if (isTop) {
                        newHeight -= dy;
                        newTop += dy;
                    }

                    newWidth = Math.max(CONFIG.MIN_CHAT_SIZE.width, newWidth);
                    newHeight = Math.max(CONFIG.MIN_CHAT_SIZE.height, newHeight);

                    if (isLeft)
                        newLeft = Math.max(
                            movieRect.left,
                            Math.min(newLeft, start.left + start.width - CONFIG.MIN_CHAT_SIZE.width)
                        );
                    if (isTop)
                        newTop = Math.max(
                            movieRect.top,
                            Math.min(newTop, start.top + start.height - CONFIG.MIN_CHAT_SIZE.height)
                        );

                    newWidth = Math.min(newWidth, movieRect.right - newLeft);
                    newHeight = Math.min(newHeight, movieRect.bottom - newTop);

                    chatContainer.style.width = `${newWidth}px`;
                    chatContainer.style.height = `${newHeight}px`;
                    chatContainer.style.left = `${newLeft - parentRect.left}px`;
                    chatContainer.style.top = `${newTop - parentRect.top}px`;
                });
            };

            const onPointerUp = (e) => {
                handle.releasePointerCapture(e.pointerId);
                document.removeEventListener('pointermove', onPointerMove);
                document.removeEventListener('pointerup', onPointerUp);
                this.saveChatStyle(chatContainer);
            };

            handle.addEventListener('pointerdown', (e) => {
                if (e.pointerType === 'mouse' && e.button !== 0) return;
                e.preventDefault();
                handle.setPointerCapture(e.pointerId);

                parentRect = chatContainer.parentElement.getBoundingClientRect();
                movieRect = DOM.moviePlayer.getBoundingClientRect();
                const chatRect = chatContainer.getBoundingClientRect();
                start = {
                    x: e.clientX,
                    y: e.clientY,
                    width: chatRect.width,
                    height: chatRect.height,
                    left: chatRect.left,
                    top: chatRect.top,
                };

                document.addEventListener('pointermove', onPointerMove);
                document.addEventListener('pointerup', onPointerUp);
            });
        },
        removeResizeHandles(chatContainer) {
            chatContainer?.querySelectorAll('.chat-resize-handle').forEach((h) => h.remove());
        },
    };

    const App = {
        init() {
            try {
                if (!this.detectGreasemonkey()) throw new Error('Greasemonkey API not detected');
                Utils.log(state.gmFallback ? 'Running in compatibility mode' : 'Running in normal mode', 'warn');

                StyleManager.apply(StyleManager.styleDefinitions.debugResizeHandleStyle, true);

                Promise.all([
                    SettingsManager.cleanupStorage(),
                    SettingsManager.load(),
                    SettingsManager.loadBlacklist(),
                ]).then(() => {
                    this.checkTampermonkeyVersion();
                    StyleManager.apply(StyleManager.styleDefinitions.chatRendererFixStyle, true);
                    StyleManager.apply(StyleManager.styleDefinitions.videoPlayerFixStyle, true);
                    this.onPageChange();
                    this.attachEventListeners();
                    MenuManager.refresh();
                });
            } catch (error) {
                Utils.log('Initialization failed', 'error', error);
            }
        },
        detectGreasemonkey() {
            return typeof window.GM?.info !== 'undefined' || typeof GM_info !== 'undefined';
        },
        checkTampermonkeyVersion() {
            const info = GM_API.info();
            if (info?.scriptHandler === 'Tampermonkey') {
                if (Utils.compareVersions(info.version, CONFIG.REQUIRED_VERSIONS.Tampermonkey) < 0) {
                    state.isOldTampermonkey = true;
                    this.warnIfOldTampermonkey();
                }
            }
        },
        async warnIfOldTampermonkey() {
            if (state.versionWarningShown || state.userSettings.isSimpleMode || !state.isOldTampermonkey) return;
            GM_API.notification({
                text: getLocalizedText().tampermonkeyOutdatedAlert,
                timeout: 15000,
            });
            state.versionWarningShown = true;
            await GM_API.setValue('versionWarningShown', true);
        },
        updateAllStyles(shouldSaveChatPos = false) {
            try {
                if (state.userSettings.useCustomPlayerHeight) {
                    state.userSettings.modifyVideoPlayer = true;
                }

                const isBlacklisted = state.blacklist.has(state.videoId);
                const isLiveOnly = state.userSettings.enableOnlyForLiveStreams && !state.isLiveStream;

                if (isBlacklisted || isLiveOnly) {
                    StyleManager.removeAll();
                    ChatInteractionManager.removeTheaterResizeHandle();
                    DOM.moviePlayer?.setCenterCrop?.();
                    return;
                }

                StyleManager.toggle(
                    StyleManager.styleDefinitions.videoPlayerStyle,
                    state.userSettings.modifyVideoPlayer
                );
                this.updateChatStyles();
                this.updateFullscreenChatStyles(shouldSaveChatPos);

                DOM.moviePlayer?.setCenterCrop?.();
            } catch (error) {
                Utils.log('Error updating styles', 'error', error);
            }
        },
        updateChatStyles() {
            const chatBox = DOM.chatFrame?.getBoundingClientRect();
            const isSecondaryVisible = DOM.ytdWatchFlexy?.querySelector('#secondary')?.style.display !== 'none';

            const shouldApplyChatStyle =
                state.userSettings.modifyChat &&
                state.isTheaterMode &&
                !state.isFullscreen &&
                !state.chatCollapsed &&
                chatBox?.width > 0 &&
                isSecondaryVisible;

            StyleManager.toggle(StyleManager.styleDefinitions.chatStyle, shouldApplyChatStyle);
            StyleManager.toggle(StyleManager.styleDefinitions.chatClampLimits, shouldApplyChatStyle);

            shouldApplyChatStyle
                ? ChatInteractionManager.addTheaterResizeHandle()
                : ChatInteractionManager.removeTheaterResizeHandle();

            this.updateHeadmastStyle(shouldApplyChatStyle);
        },
        updateHeadmastStyle(isChatStyled) {
            this.updateLowHeadmastStyle();

            const shouldShrinkHeadmast =
                isChatStyled &&
                DOM.chatFrame?.getAttribute('theater-watch-while') === '' &&
                (state.userSettings.setLowHeadmast || state.userSettings.modifyChat);

            state.chatWidth = DOM.chatFrame?.offsetWidth ?? 0;
            StyleManager.toggle(StyleManager.styleDefinitions.headmastStyle, shouldShrinkHeadmast);
        },
        updateLowHeadmastStyle() {
            if (!DOM.moviePlayer) return;
            const shouldApply =
                state.userSettings.setLowHeadmast &&
                state.isTheaterMode &&
                !state.isFullscreen &&
                state.currentPageType === 'watch';
            StyleManager.toggle(StyleManager.styleDefinitions.lowHeadmastStyle, shouldApply);
        },
        updateFullscreenChatStyles(shouldSave) {
            const chatContainer = DOM.chatContainer;
            const shouldEnableFloatingChat = state.userSettings.floatingChat && state.isFullscreen;

            if (!chatContainer || !shouldEnableFloatingChat) {
                if (chatContainer) ChatInteractionManager.cleanupFullscreenChat(chatContainer);
                StyleManager.remove(StyleManager.styleDefinitions.floatingChatStyleCollapsed);
                StyleManager.remove(StyleManager.styleDefinitions.floatingChatStyleExpanded);
                StyleManager.remove(StyleManager.styleDefinitions.floatingChatStyle);
                return;
            }

            const isChatAvailable = chatContainer.querySelector('#chat');
            if (isChatAvailable) {
                StyleManager.apply(StyleManager.styleDefinitions.floatingChatStyle);
                StyleManager.toggle(StyleManager.styleDefinitions.floatingChatStyleCollapsed, state.chatCollapsed);
                StyleManager.toggle(StyleManager.styleDefinitions.floatingChatStyleExpanded, !state.chatCollapsed);
                ChatInteractionManager.initFullscreenChat(chatContainer);
                if (shouldSave) {
                    ChatInteractionManager.applySavedChatStyle(chatContainer, true);
                }
            } else {
                ChatInteractionManager.cleanupFullscreenChat(chatContainer);
            }
        },
        updateDebugStyles() {
            if (DOM.chatContainer) {
                DOM.chatContainer.toggleAttribute('debug', state.userSettings.debug);
            }
        },
        updateDOMCache() {
            DOM.ytdWatchFlexy = document.querySelector('ytd-watch-flexy');
            DOM.chatContainer = document.querySelector('#chat-container');
            DOM.chatFrame = document.querySelector('ytd-live-chat-frame#chat');
        },
        updateMoviePlayerObserver() {
            const newMoviePlayer = document.querySelector('#movie_player');
            if (DOM.moviePlayer === newMoviePlayer) return;

            if (state.resizeObserver) {
                if (DOM.moviePlayer) {
                    state.resizeObserver.unobserve(DOM.moviePlayer);
                }
            } else {
                state.resizeObserver = new ResizeObserver((entries) => {
                    for (const entry of entries) {
                        state.moviePlayerHeight = entry.contentRect.height;
                        this.updateAllStyles();
                    }
                });
            }

            DOM.moviePlayer = newMoviePlayer;
            if (DOM.moviePlayer) {
                state.resizeObserver.observe(DOM.moviePlayer);
            }
        },
        onPageChange() {
            this.updateDOMCache();
            this.updateMoviePlayerObserver();
            this.updateAllStyles();
            this.updateDebugStyles();
            MenuManager.refresh();
        },

        handleFullscreenChange() {
            state.isFullscreen = !!document.fullscreenElement;
            this.updateAllStyles(true);
        },
        handleTheaterChange(event) {
            state.isTheaterMode = !!event?.detail?.enabled;
            this.updateAllStyles();
        },
        handleChatCollapse(event) {
            DOM.chatFrame = event.target;
            state.chatCollapsed = event.detail !== false;
            this.updateAllStyles(true);
        },
        handlePageData(event) {
            try {
                const pageData = event.detail.pageData;
                state.currentPageType = pageData.page;
                state.videoId = pageData.playerResponse?.videoDetails?.videoId;
                state.isLiveStream = pageData.playerResponse?.videoDetails?.isLiveContent;
                state.isFullscreen = !!document.fullscreenElement;
                this.onPageChange();
            } catch (error) {
                Utils.log('Failed to process page data', 'error', error);
            }
        },
        attachEventListeners() {
            const events = {
                'yt-set-theater-mode-enabled': (e) => this.handleTheaterChange(e),
                'yt-chat-collapsed-changed': (e) => this.handleChatCollapse(e),
                'yt-page-data-fetched': (e) => this.handlePageData(e),
                'yt-page-data-updated': () => this.onPageChange(),
                fullscreenchange: () => this.handleFullscreenChange(),
                'yt-navigate-finish': () => this.onPageChange(),
            };

            for (const [event, handler] of Object.entries(events)) {
                window.addEventListener(event, handler.bind(this), {
                    capture: true,
                    passive: true,
                });
            }

            let isResizeScheduled = false;
            window.addEventListener('resize', () => {
                if (isResizeScheduled) return;
                isResizeScheduled = true;
                requestAnimationFrame(() => {
                    this.updateAllStyles(true);
                    isResizeScheduled = false;
                });
            });
        },
    };

    App.init();
})();