Better YouTube Theater Mode

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.

Instalar este script¿?
Script recomendado por el autor

Puede que también te guste YouTube Music Opus Codec.

Instalar este script
// ==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();
})();