Twitch Offchat Stream Viewer

Watch another stream while chilling in an offline chat

当前为 2025-11-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitch Offchat Stream Viewer
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Watch another stream while chilling in an offline chat
// @author       benno2503
// @match        https://www.twitch.tv/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        #offchat-embed-wrapper {
            box-sizing: border-box !important;
        }
        #offchat-embed-wrapper iframe,
        #offchat-twitch-iframe {
            box-sizing: border-box !important;
            border: none !important;
        }
    `;
    document.head.appendChild(style);

    const IGNORED_PATHS = [
        'directory', 'following', 'settings', 'subscriptions',
        'inventory', 'wallet', 'drops', 'videos', 'p', 'search',
        'downloads', 'turbo', 'jobs', 'about', 'prime', 'bits'
    ];

    let config = { lastStreams: GM_getValue('lastStreams', []) };
    let currentChannel = '';
    let controlPanel = null;
    let chatControlPanel = null;
    let openChats = []; // Array of { channel, window, minimizeButton, isMinimized }
    let openStreams = []; // Array of { id, type: 'twitch'|'youtube', displayName }
    let focusedStreamIndex = null; // Index of focused stream in spotlight mode
    let streamContainer = null; // Container for all streams
    let currentPanelMode = 'stream'; // 'stream' or 'chat'
    let lastChannel = null;
    let buttonCheckInterval = null;
    let streamWrapperCache = {}; // Cache stream wrappers (including iframes) to prevent reloading
    let resizeUpdateBound = null; // Bound resize update function to prevent duplicates

    function isChannelPage() {
        const path = window.location.pathname.split('/')[1]?.toLowerCase();
        if (!path) return false;
        if (IGNORED_PATHS.includes(path)) return false;
        return /^[a-z0-9_]+$/.test(path);
    }

    function getCurrentChannel() {
        const path = window.location.pathname.split('/')[1];
        if (path && isChannelPage()) return path.toLowerCase();
        return null;
    }

    function extractChannelName(input) {
        if (!input) return null;
        input = input.trim();
        const urlMatch = input.match(/twitch\.tv\/([a-zA-Z0-9_]+)/);
        if (urlMatch) return urlMatch[1].toLowerCase();
        return input.toLowerCase().replace(/[^a-z0-9_]/g, '');
    }

    function extractYouTubeVideoId(input) {
        if (!input) return null;
        input = input.trim();

        // Match various YouTube URL formats
        const patterns = [
            /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
            /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
            /youtube\.com\/v\/([a-zA-Z0-9_-]{11})/
        ];

        for (const pattern of patterns) {
            const match = input.match(pattern);
            if (match) return match[1];
        }

        // If it looks like a video ID (11 chars), return it
        if (/^[a-zA-Z0-9_-]{11}$/.test(input)) {
            return input;
        }

        return null;
    }

    function isYouTubeUrl(input) {
        return input && (input.includes('youtube.com') || input.includes('youtu.be'));
    }

    function makeDraggable(element, handle) {
        let isDragging = false;
        let startX, startY, startLeft, startTop;

        handle.style.cursor = 'grab';

        handle.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
            isDragging = true;
            handle.style.cursor = 'grabbing';
            startX = e.clientX;
            startY = e.clientY;
            const rect = element.getBoundingClientRect();
            startLeft = rect.left;
            startTop = rect.top;
            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            element.style.left = (startLeft + dx) + 'px';
            element.style.top = (startTop + dy) + 'px';
            element.style.right = 'auto';
            element.style.bottom = 'auto';
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            handle.style.cursor = 'grab';
        });
    }

    async function createEmbedPlayer(input) {
        if (!input) return;

        let streamData;

        // Check if it's a YouTube URL
        if (isYouTubeUrl(input)) {
            const videoId = extractYouTubeVideoId(input);
            if (!videoId) {
                alert("Invalid YouTube URL!");
                return;
            }

            // Check if already open
            if (openStreams.some(s => s.id === videoId && s.type === 'youtube')) {
                alert("This YouTube video is already open!");
                return;
            }

            // Fetch video title
            let displayName = `YT: ${videoId.substring(0, 8)}...`;
            try {
                const response = await fetch(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`);
                if (response.ok) {
                    const data = await response.json();
                    displayName = data.title || displayName;
                }
            } catch (e) {
                console.log('Could not fetch YouTube title:', e);
            }

            streamData = {
                id: videoId,
                type: 'youtube',
                displayName: displayName
            };
        } else {
            // Twitch channel
            const channel = extractChannelName(input);
            if (!channel) return;

            // Check if already open
            if (openStreams.some(s => s.id === channel && s.type === 'twitch')) {
                alert("This stream is already open!");
                return;
            }

            streamData = {
                id: channel,
                type: 'twitch',
                displayName: channel
            };
        }

        // Limit to 4 streams
        if (openStreams.length >= 4) {
            alert("Maximum 4 streams allowed!");
            return;
        }

        // Save to recent (only Twitch channels)
        if (streamData.type === 'twitch') {
            config.lastStreams = config.lastStreams.filter(s => s !== streamData.id);
            config.lastStreams.unshift(streamData.id);
            if (config.lastStreams.length > 10) config.lastStreams.pop();
            GM_setValue('lastStreams', config.lastStreams);
        }

        openStreams.push(streamData);
        renderStreamContainer();
        updateControlPanel();
    }

    function removeStream(streamId, streamType) {
        const index = openStreams.findIndex(s => s.id === streamId && s.type === streamType);
        if (index === -1) return;

        // Remove from cache
        const streamKey = `${streamType}-${streamId}`;
        const wrapper = streamWrapperCache[streamKey];
        if (wrapper && wrapper.parentNode) {
            wrapper.remove();
        }
        delete streamWrapperCache[streamKey];

        openStreams.splice(index, 1);

        // Reset focus if we removed the focused stream
        if (focusedStreamIndex === index) {
            focusedStreamIndex = null;
        } else if (focusedStreamIndex !== null && focusedStreamIndex > index) {
            focusedStreamIndex--;
        }

        if (openStreams.length === 0) {
            removeStreamContainer();
        } else {
            renderStreamContainer();
        }
        updateControlPanel();
    }

    function removeStreamContainer() {
        if (streamContainer) {
            streamContainer.remove();
            streamContainer = null;
        }
        if (resizeUpdateBound) {
            window.removeEventListener('resize', resizeUpdateBound);
            window.removeEventListener('scroll', resizeUpdateBound);
            resizeUpdateBound = null;
        }
        openStreams = [];
        focusedStreamIndex = null;
        streamWrapperCache = {}; // Clear wrapper cache
    }

    function toggleStreamFocus(index) {
        if (focusedStreamIndex === index) {
            focusedStreamIndex = null; // Return to grid
        } else {
            focusedStreamIndex = index; // Spotlight this stream
        }
        renderStreamContainer();
    }

    function renderStreamContainer() {
        if (openStreams.length === 0) {
            removeStreamContainer();
            return;
        }

        const playerContainer = document.querySelector('[data-a-target="video-player"]') ||
                               document.querySelector('.video-player__container') ||
                               document.querySelector('.video-player') ||
                               document.querySelector('.persistent-player');

        const numStreams = openStreams.length;
        const containerPadding = numStreams === 1 ? '0px' : '8px';

        let containerRect;
        if (playerContainer) {
            containerRect = playerContainer.getBoundingClientRect();
        } else {
            const chatWidth = 340;
            const navHeight = 50;
            containerRect = {
                top: navHeight,
                left: 0,
                width: window.innerWidth - chatWidth,
                height: window.innerHeight - navHeight
            };
        }

        // Check if container exists
        let existing = document.getElementById('offchat-embed-wrapper');

        if (!existing) {
            // Create new container only if it doesn't exist
            streamContainer = document.createElement('div');
            streamContainer.id = 'offchat-embed-wrapper';

            if (playerContainer) {
                streamContainer.style.cssText = `
                    position: fixed !important;
                    top: ${containerRect.top}px !important;
                    left: ${containerRect.left}px !important;
                    width: ${containerRect.width}px !important;
                    height: ${containerRect.height}px !important;
                    z-index: 1000 !important;
                    background: #0e0e10 !important;
                    overflow: hidden !important;
                    box-sizing: border-box !important;
                    display: block !important;
                    padding: ${containerPadding} !important;
                    gap: 8px !important;
                `;
            } else {
                streamContainer.style.cssText = `
                    position: fixed;
                    top: ${containerRect.top}px;
                    left: 0;
                    width: calc(100vw - 340px);
                    height: calc(100vh - 50px);
                    z-index: 999;
                    background: #0e0e10;
                    overflow: hidden;
                    padding: ${containerPadding};
                    box-sizing: border-box;
                `;
            }

            document.body.appendChild(streamContainer);
        } else {
            // Update existing container styling
            streamContainer = existing;
            streamContainer.style.padding = containerPadding;

            if (playerContainer) {
                streamContainer.style.top = containerRect.top + 'px';
                streamContainer.style.left = containerRect.left + 'px';
                streamContainer.style.width = containerRect.width + 'px';
                streamContainer.style.height = containerRect.height + 'px';
            }
        }

        // Calculate layout based on number of streams and focus mode
        const isSpotlight = focusedStreamIndex !== null;

        // Track which stream keys are currently active
        const activeStreamKeys = new Set();

        openStreams.forEach((stream, index) => {
            const streamKey = `${stream.type}-${stream.id}`;
            activeStreamKeys.add(streamKey);

            // Check if wrapper already exists in cache
            let streamWrapper = streamWrapperCache[streamKey];

            if (!streamWrapper) {
                // Create new wrapper and iframe
                streamWrapper = document.createElement('div');
                streamWrapper.className = 'offchat-stream-wrapper';
                streamWrapper.dataset.streamKey = streamKey;

                const iframe = document.createElement('iframe');

                // Set iframe source based on stream type
                if (stream.type === 'youtube') {
                    iframe.src = `https://www.youtube.com/embed/${stream.id}?autoplay=1&mute=0&rel=0`;
                } else {
                    iframe.src = `https://player.twitch.tv/?channel=${stream.id}&parent=www.twitch.tv&muted=false&autoplay=true`;
                }

                iframe.setAttribute('allowfullscreen', 'true');
                iframe.setAttribute('allow', 'autoplay; fullscreen');

                iframe.style.width = '100%';
                iframe.style.height = '100%';
                iframe.style.border = 'none';

                streamWrapper.appendChild(iframe);
                streamWrapperCache[streamKey] = streamWrapper;

                // Append to container immediately after creation
                streamContainer.appendChild(streamWrapper);
            }

            // Update wrapper position and styling using individual properties
            const styleProps = getStreamWrapperStyleProps(index, numStreams, isSpotlight, containerRect);

            streamWrapper.style.position = styleProps.position;
            streamWrapper.style.top = styleProps.top;
            streamWrapper.style.left = styleProps.left;
            streamWrapper.style.width = styleProps.width;
            streamWrapper.style.height = styleProps.height;
            streamWrapper.style.border = styleProps.border;
            streamWrapper.style.borderRadius = styleProps.borderRadius;
            streamWrapper.style.boxSizing = styleProps.boxSizing;
            streamWrapper.style.overflow = styleProps.overflow;
            streamWrapper.style.transition = styleProps.transition;
            streamWrapper.style.zIndex = styleProps.zIndex || '1';
            streamWrapper.style.cursor = 'pointer';

            streamWrapper.onclick = () => toggleStreamFocus(index);

            // Update iframe border radius only
            const iframe = streamWrapper.querySelector('iframe');
            if (iframe) {
                iframe.style.borderRadius = numStreams === 1 ? '0' : '8px';
            }
        });

        // Remove any wrappers that are no longer needed
        Object.keys(streamWrapperCache).forEach(key => {
            if (!activeStreamKeys.has(key)) {
                const wrapper = streamWrapperCache[key];
                if (wrapper.parentNode) {
                    wrapper.remove();
                }
                delete streamWrapperCache[key];
            }
        });

        // Update on resize - only add listeners once
        if (playerContainer && !resizeUpdateBound) {
            resizeUpdateBound = () => {
                if (!streamContainer || !playerContainer) return;

                const rect = playerContainer.getBoundingClientRect();
                streamContainer.style.top = rect.top + 'px';
                streamContainer.style.left = rect.left + 'px';
                streamContainer.style.width = rect.width + 'px';
                streamContainer.style.height = rect.height + 'px';

                const numStreams = openStreams.length;
                const isSpotlight = focusedStreamIndex !== null;

                // Re-calculate stream positions
                openStreams.forEach((stream, index) => {
                    const streamKey = `${stream.type}-${stream.id}`;
                    const wrapper = streamWrapperCache[streamKey];
                    if (wrapper) {
                        const styleProps = getStreamWrapperStyleProps(index, numStreams, isSpotlight, rect);
                        wrapper.style.position = styleProps.position;
                        wrapper.style.top = styleProps.top;
                        wrapper.style.left = styleProps.left;
                        wrapper.style.width = styleProps.width;
                        wrapper.style.height = styleProps.height;
                        wrapper.style.border = styleProps.border;
                        wrapper.style.borderRadius = styleProps.borderRadius;
                        wrapper.onclick = () => toggleStreamFocus(index);
                        wrapper.style.cursor = 'pointer';
                    }
                });
            };
            window.addEventListener('resize', resizeUpdateBound);
            window.addEventListener('scroll', resizeUpdateBound);
        }
    }

    function getStreamWrapperStyleProps(index, numStreams, isSpotlight, containerRect) {
        const padding = 8;
        const gap = 8;
        const borderWidth = 3;
        const availableWidth = containerRect.width - (padding * 2);
        const availableHeight = containerRect.height - (padding * 2);

        if (isSpotlight) {
            // Spotlight mode: one large, others as thumbnails in corner
            if (index === focusedStreamIndex) {
                // Large focused stream
                return {
                    position: 'absolute',
                    top: padding + 'px',
                    left: padding + 'px',
                    width: availableWidth + 'px',
                    height: availableHeight + 'px',
                    border: borderWidth + 'px solid #9147ff',
                    borderRadius: '12px',
                    boxSizing: 'border-box',
                    overflow: 'hidden',
                    transition: 'all 0.3s ease'
                };
            } else {
                // Small thumbnail streams
                const thumbnailWidth = 200;
                const thumbnailHeight = 113; // 16:9 ratio
                const thumbnailIndex = index > focusedStreamIndex ? index - 1 : index;
                const thumbnailX = padding + gap;
                const thumbnailY = padding + gap + (thumbnailIndex * (thumbnailHeight + gap));

                return {
                    position: 'absolute',
                    top: thumbnailY + 'px',
                    left: thumbnailX + 'px',
                    width: thumbnailWidth + 'px',
                    height: thumbnailHeight + 'px',
                    border: borderWidth + 'px solid #bf94ff',
                    borderRadius: '8px',
                    boxSizing: 'border-box',
                    overflow: 'hidden',
                    transition: 'all 0.3s ease',
                    zIndex: '10'
                };
            }
        } else {
            // Grid mode
            if (numStreams === 1) {
                // Single stream - no border, fill completely
                return {
                    position: 'absolute',
                    top: '0',
                    left: '0',
                    width: containerRect.width + 'px',
                    height: containerRect.height + 'px',
                    border: 'none',
                    borderRadius: '0',
                    boxSizing: 'border-box',
                    overflow: 'hidden',
                    transition: 'all 0.3s ease'
                };
            } else if (numStreams === 2) {
                // Top/bottom layout
                const streamHeight = (availableHeight - gap) / 2;
                const top = index === 0 ? padding : padding + streamHeight + gap;

                return {
                    position: 'absolute',
                    top: top + 'px',
                    left: padding + 'px',
                    width: availableWidth + 'px',
                    height: streamHeight + 'px',
                    border: borderWidth + 'px solid #9147ff',
                    borderRadius: '12px',
                    boxSizing: 'border-box',
                    overflow: 'hidden',
                    transition: 'all 0.3s ease'
                };
            } else {
                // 2x2 grid for 3-4 streams
                const streamWidth = (availableWidth - gap) / 2;
                const streamHeight = (availableHeight - gap) / 2;
                const col = index % 2;
                const row = Math.floor(index / 2);
                const left = padding + (col * (streamWidth + gap));
                const top = padding + (row * (streamHeight + gap));

                return {
                    position: 'absolute',
                    top: top + 'px',
                    left: left + 'px',
                    width: streamWidth + 'px',
                    height: streamHeight + 'px',
                    border: borderWidth + 'px solid #9147ff',
                    borderRadius: '12px',
                    boxSizing: 'border-box',
                    overflow: 'hidden',
                    transition: 'all 0.3s ease'
                };
            }
        }
    }

    function removeEmbedPlayer() {
        removeStreamContainer();
        updateControlPanel();
    }

    function createChatWindow(channelInput) {
        if (!channelInput) return;

        const channel = extractChannelName(channelInput);
        if (!channel) return;

        // Check if chat for this channel is already open
        const existingChat = openChats.find(c => c.channel === channel);
        if (existingChat) {
            // If minimized, show it. Otherwise, just focus it.
            if (existingChat.isMinimized) {
                showChatWindow(channel);
            } else {
                existingChat.window.style.zIndex = '10002';
            }
            return;
        }

        config.lastStreams = config.lastStreams.filter(s => s !== channel);
        config.lastStreams.unshift(channel);
        if (config.lastStreams.length > 10) config.lastStreams.pop();
        GM_setValue('lastStreams', config.lastStreams);

        // Calculate position offset based on number of open chats
        const offset = openChats.length * 30;

        const chatWindow = document.createElement('div');
        chatWindow.id = `offchat-chat-window-${channel}`;
        chatWindow.style.cssText = `
            position: fixed;
            top: ${100 + offset}px;
            right: ${20 + offset}px;
            width: 340px;
            height: 500px;
            background: #18181b;
            border: 1px solid #2f2f35;
            border-radius: 6px;
            z-index: 10002;
            box-shadow: 0 8px 24px rgba(0,0,0,0.5);
            display: flex;
            flex-direction: column;
            overflow: auto;
            resize: both;
            min-width: 250px;
            min-height: 300px;
            max-width: 90vw;
            max-height: 90vh;
        `;

        const chatHeader = document.createElement('div');
        chatHeader.id = 'offchat-chat-header';
        chatHeader.style.cssText = `
            padding: 10px 12px;
            background: #1f1f23;
            border-bottom: 1px solid #2f2f35;
            display: flex;
            align-items: center;
            justify-content: space-between;
            user-select: none;
            cursor: move;
        `;
        chatHeader.innerHTML = `
            <div style="display: flex; align-items: center; gap: 8px;">
                <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" style="color: #bf94ff;">
                    <path d="M7.828 13L10 15.172 12.172 13H15a1 1 0 001-1V4a1 1 0 00-1-1H5a1 1 0 00-1 1v8a1 1 0 001 1h2.828zM10 18l-3-3H5a3 3 0 01-3-3V4a3 3 0 013-3h10a3 3 0 013 3v8a3 3 0 01-3 3h-2l-3 3z"/>
                </svg>
                <span style="font-weight: 600; font-size: 13px; color: #dedee3;">${channel}</span>
            </div>
            <div style="display: flex; gap: 8px;">
                <button class="offchat-minimize-chat-btn" data-channel="${channel}" style="
                    background: none;
                    border: none;
                    color: #898395;
                    cursor: pointer;
                    font-size: 16px;
                    padding: 0;
                    line-height: 1;
                " title="Minimize">−</button>
                <button class="offchat-close-chat-btn" data-channel="${channel}" style="
                    background: none;
                    border: none;
                    color: #898395;
                    cursor: pointer;
                    font-size: 16px;
                    padding: 0;
                    line-height: 1;
                " title="Close">×</button>
            </div>
        `;

        const chatIframe = document.createElement('iframe');
        chatIframe.id = 'offchat-chat-iframe';
        chatIframe.src = `https://www.twitch.tv/embed/${channel}/chat?parent=www.twitch.tv&darkpopout`;
        chatIframe.style.cssText = `
            width: 100%;
            border: none;
            flex: 1 1 auto;
            min-height: 0;
        `;

        chatWindow.appendChild(chatHeader);
        chatWindow.appendChild(chatIframe);
        document.body.appendChild(chatWindow);

        makeDraggable(chatWindow, chatHeader);

        const minimizeBtn = chatWindow.querySelector('.offchat-minimize-chat-btn');
        const closeBtn = chatWindow.querySelector('.offchat-close-chat-btn');

        if (minimizeBtn) {
            minimizeBtn.onclick = () => minimizeChatWindow(channel);
        }

        if (closeBtn) {
            closeBtn.onclick = () => removeChatWindow(channel);
        }

        // Add to openChats array
        openChats.push({
            channel: channel,
            window: chatWindow,
            minimizeButton: null,
            isMinimized: false
        });

        updateControlPanel();
    }

    function removeChatWindow(channel) {
        const chatIndex = openChats.findIndex(c => c.channel === channel);
        if (chatIndex === -1) return;

        const chat = openChats[chatIndex];
        if (chat.window) chat.window.remove();
        if (chat.minimizeButton) chat.minimizeButton.remove();

        openChats.splice(chatIndex, 1);
        updateControlPanel();
    }

    function minimizeChatWindow(channel) {
        const chat = openChats.find(c => c.channel === channel);
        if (!chat) return;

        chat.window.style.display = 'none';
        chat.isMinimized = true;
        createChatMinimizeButton(channel);
        updateControlPanel();
    }

    function showChatWindow(channel) {
        const chat = openChats.find(c => c.channel === channel);
        if (!chat) return;

        chat.window.style.display = 'flex';
        chat.window.style.zIndex = '10002';
        chat.isMinimized = false;
        if (chat.minimizeButton) {
            chat.minimizeButton.remove();
            chat.minimizeButton = null;
        }
        updateControlPanel();
    }

    function createChatMinimizeButton(channel) {
        const chat = openChats.find(c => c.channel === channel);
        if (!chat) return;

        // Find index to calculate position
        const chatIndex = openChats.findIndex(c => c.channel === channel);
        const offset = chatIndex * 40; // 40px spacing for minimize buttons

        const minimizeButton = document.createElement('button');
        minimizeButton.id = `offchat-chat-minimize-btn-${channel}`;
        minimizeButton.title = `Chat: ${channel}`;
        minimizeButton.innerHTML = `
            <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
                <path d="M7.828 13L10 15.172 12.172 13H15a1 1 0 001-1V4a1 1 0 00-1-1H5a1 1 0 00-1 1v8a1 1 0 001 1h2.828zM10 18l-3-3H5a3 3 0 01-3-3V4a3 3 0 013-3h10a3 3 0 013 3v8a3 3 0 01-3 3h-2l-3 3z"/>
            </svg>
        `;

        minimizeButton.style.cssText = `
            display: inline-flex !important;
            align-items: center;
            justify-content: center;
            width: 30px;
            height: 30px;
            background: rgba(24, 24, 27, 0.95);
            border: 1px solid #2f2f35;
            border-radius: 4px;
            color: #bf94ff;
            cursor: pointer;
            transition: background-color 0.1s, color 0.1s;
            padding: 0;
            position: fixed;
            top: ${100 + offset}px;
            right: 20px;
            z-index: 10001;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
        `;

        minimizeButton.onmouseenter = () => {
            minimizeButton.style.background = 'rgba(83, 83, 95, 0.48)';
            minimizeButton.style.color = '#fff';
        };
        minimizeButton.onmouseleave = () => {
            minimizeButton.style.background = 'transparent';
            minimizeButton.style.color = '#dedee3';
        };
        minimizeButton.onclick = () => showChatWindow(channel);

        document.body.appendChild(minimizeButton);
        chat.minimizeButton = minimizeButton;
    }

    function createControlPanel() {
        if (controlPanel) controlPanel.remove();

        controlPanel = document.createElement('div');
        controlPanel.id = 'offchat-control-panel';
        controlPanel.style.cssText = `
            position: fixed;
            bottom: 100px;
            right: 370px;
            background: #18181b;
            border: 1px solid #2f2f35;
            border-radius: 6px;
            z-index: 10001;
            font-family: 'Inter', 'Roobert', sans-serif;
            color: #efeff1;
            width: 240px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.5);
            overflow: hidden;
        `;

        document.body.appendChild(controlPanel);
        updateControlPanel();

        const header = controlPanel.querySelector('#offchat-header');
        if (header) makeDraggable(controlPanel, header);
    }

    function updateControlPanel() {
        if (!controlPanel) return;

        controlPanel.innerHTML = `
            <div id="offchat-header" style="
                padding: 10px 12px;
                background: #1f1f23;
                border-bottom: 1px solid #2f2f35;
                display: flex;
                align-items: center;
                justify-content: space-between;
                user-select: none;
                cursor: move;
            ">
                <div style="display: flex; align-items: center; gap: 8px;">
                    <button id="offchat-stream-icon" style="
                        background: none;
                        border: none;
                        color: ${currentPanelMode === 'stream' ? '#bf94ff' : '#898395'};
                        cursor: pointer;
                        padding: 0;
                        display: flex;
                        align-items: center;
                        transition: color 0.1s ease-in;
                    " title="Stream Viewer">
                        <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
                            <path d="M5 3a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2V5a2 2 0 00-2-2H5zm0 1.5h10a.5.5 0 01.5.5v10a.5.5 0 01-.5.5H5a.5.5 0 01-.5-.5V5a.5.5 0 01.5-.5z"/>
                            <path d="M8 7l5 3-5 3V7z"/>
                        </svg>
                    </button>
                    <button id="offchat-chat-icon" style="
                        background: none;
                        border: none;
                        color: ${currentPanelMode === 'chat' ? '#bf94ff' : '#898395'};
                        cursor: pointer;
                        padding: 0;
                        display: flex;
                        align-items: center;
                        transition: color 0.1s ease-in;
                    " title="Chat Viewer">
                        <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
                            <path d="M7.828 13L10 15.172 12.172 13H15a1 1 0 001-1V4a1 1 0 00-1-1H5a1 1 0 00-1 1v8a1 1 0 001 1h2.828zM10 18l-3-3H5a3 3 0 01-3-3V4a3 3 0 013-3h10a3 3 0 013 3v8a3 3 0 01-3 3h-2l-3 3z"/>
                        </svg>
                    </button>
                    <span style="font-weight: 600; font-size: 13px; color: #dedee3;">${currentPanelMode === 'stream' ? 'Stream Viewer' : 'Chat Viewer'}</span>
                </div>
                <button id="offchat-close-panel" style="
                    background: none;
                    border: none;
                    color: #898395;
                    cursor: pointer;
                    font-size: 16px;
                    padding: 0;
                    line-height: 1;
                " title="Minimize">−</button>
            </div>
            <div style="padding: 12px;">
                ${currentPanelMode === 'stream' && openStreams.length > 0 ? `
                    <div style="margin-bottom: 8px;">
                        <div style="font-size: 11px; color: #898395; margin-bottom: 6px;">Open Streams (${openStreams.length}/4)</div>
                        ${openStreams.map((stream, index) => `
                            <div style="display: flex; align-items: center; gap: 4px; margin-bottom: 4px; padding: 6px; background: rgba(83, 83, 95, 0.2); border-radius: 4px;">
                                <span style="flex: 1; font-size: 12px; color: #efeff1;">${stream.displayName}${index === focusedStreamIndex ? ' ⭐' : ''}</span>
                                <button class="offchat-focus-stream-btn" data-index="${index}" style="
                                    padding: 4px 8px;
                                    height: 24px;
                                    background: rgba(83, 83, 95, 0.38);
                                    border: none;
                                    border-radius: 12px;
                                    color: #efeff1;
                                    cursor: pointer;
                                    font-size: 11px;
                                    font-weight: 600;
                                    transition: background-color 0.1s ease-in;
                                ">${index === focusedStreamIndex ? 'Grid' : 'Focus'}</button>
                                <button class="offchat-remove-stream-btn" data-id="${stream.id}" data-type="${stream.type}" style="
                                    padding: 4px 8px;
                                    height: 24px;
                                    background: rgba(83, 83, 95, 0.38);
                                    border: none;
                                    border-radius: 12px;
                                    color: #efeff1;
                                    cursor: pointer;
                                    font-size: 11px;
                                    font-weight: 600;
                                    transition: background-color 0.1s ease-in;
                                ">✕</button>
                            </div>
                        `).join('')}
                    </div>
                    ${openStreams.length < 4 ? `
                        <input type="text" id="offchat-channel-input"
                            placeholder="Add another stream..."
                            style="
                                width: 100%;
                                padding: 8px 10px;
                                height: 30px;
                                background: rgba(14, 14, 16, 1);
                                border: 2px solid #464649;
                                border-radius: 4px;
                                color: #efeff1;
                                margin-bottom: 8px;
                                box-sizing: border-box;
                                font-size: 13px;
                                font-family: inherit;
                                outline: none;
                                transition: border-color 0.1s ease-in, background-color 0.1s ease-in;
                            ">
                        <button id="offchat-embed-btn" style="
                            width: 100%;
                            padding: 5px 10px;
                            height: 30px;
                            background: #9147ff;
                            border: none;
                            border-radius: 4px;
                            color: #fff;
                            cursor: pointer;
                            font-weight: 600;
                            font-size: 13px;
                            font-family: inherit;
                            transition: background-color 0.1s ease-in, color 0.1s ease-in;
                            display: inline-flex;
                            align-items: center;
                            justify-content: center;
                        ">+ Add Stream</button>
                    ` : ''}
                ` : currentPanelMode === 'chat' && openChats.length > 0 ? `
                    <div style="margin-bottom: 8px;">
                        <div style="font-size: 11px; color: #898395; margin-bottom: 6px;">Open Chats (${openChats.length})</div>
                        ${openChats.map(chat => `
                            <div style="display: flex; align-items: center; gap: 4px; margin-bottom: 4px; padding: 6px; background: rgba(83, 83, 95, 0.2); border-radius: 4px;">
                                <span style="flex: 1; font-size: 12px; color: #efeff1;">${chat.channel}</span>
                                <button class="offchat-toggle-chat-btn" data-channel="${chat.channel}" style="
                                    padding: 4px 8px;
                                    height: 24px;
                                    background: rgba(83, 83, 95, 0.38);
                                    border: none;
                                    border-radius: 12px;
                                    color: #efeff1;
                                    cursor: pointer;
                                    font-size: 11px;
                                    font-weight: 600;
                                    transition: background-color 0.1s ease-in;
                                ">${chat.isMinimized ? 'Show' : 'Hide'}</button>
                                <button class="offchat-remove-chat-btn" data-channel="${chat.channel}" style="
                                    padding: 4px 8px;
                                    height: 24px;
                                    background: rgba(83, 83, 95, 0.38);
                                    border: none;
                                    border-radius: 12px;
                                    color: #efeff1;
                                    cursor: pointer;
                                    font-size: 11px;
                                    font-weight: 600;
                                    transition: background-color 0.1s ease-in;
                                ">✕</button>
                            </div>
                        `).join('')}
                    </div>
                    <input type="text" id="offchat-channel-input"
                        placeholder="Open another chat..."
                        style="
                            width: 100%;
                            padding: 8px 10px;
                            height: 30px;
                            background: rgba(14, 14, 16, 1);
                            border: 2px solid #464649;
                            border-radius: 4px;
                            color: #efeff1;
                            margin-bottom: 8px;
                            box-sizing: border-box;
                            font-size: 13px;
                            font-family: inherit;
                            outline: none;
                            transition: border-color 0.1s ease-in, background-color 0.1s ease-in;
                        ">
                    <button id="offchat-embed-btn" style="
                        width: 100%;
                        padding: 5px 10px;
                        height: 30px;
                        background: #9147ff;
                        border: none;
                        border-radius: 4px;
                        color: #fff;
                        cursor: pointer;
                        font-weight: 600;
                        font-size: 13px;
                        font-family: inherit;
                        transition: background-color 0.1s ease-in, color 0.1s ease-in;
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                    ">+ Open Chat</button>
                ` : `
                    <input type="text" id="offchat-channel-input"
                        placeholder="${currentPanelMode === 'stream' ? 'twitch.tv/... or channel name' : 'Chat: twitch.tv/... or channel name'}"
                        style="
                            width: 100%;
                            padding: 8px 10px;
                            height: 30px;
                            background: rgba(14, 14, 16, 1);
                            border: 2px solid #464649;
                            border-radius: 4px;
                            color: #efeff1;
                            margin-bottom: 8px;
                            box-sizing: border-box;
                            font-size: 13px;
                            font-family: inherit;
                            outline: none;
                            transition: border-color 0.1s ease-in, background-color 0.1s ease-in;
                        ">
                    <button id="offchat-embed-btn" style="
                        width: 100%;
                        padding: 5px 10px;
                        height: 30px;
                        background: #9147ff;
                        border: none;
                        border-radius: 4px;
                        color: #fff;
                        cursor: pointer;
                        font-weight: 600;
                        font-size: 13px;
                        font-family: inherit;
                        transition: background-color 0.1s ease-in, color 0.1s ease-in;
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                    ">${currentPanelMode === 'stream' ? 'Watch' : 'Open Chat'}</button>
                    ${config.lastStreams.length > 0 ? `
                        <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #2f2f35;">
                            <div style="font-size: 11px; color: #898395; margin-bottom: 6px;">Recent</div>
                            <div style="display: flex; flex-wrap: wrap; gap: 4px;">
                                ${config.lastStreams.slice(0, 6).map(s => `
                                    <button class="offchat-recent-btn" data-channel="${s}" style="
                                        padding: 4px 10px;
                                        height: 24px;
                                        background: rgba(83, 83, 95, 0.38);
                                        border: none;
                                        border-radius: 12px;
                                        color: #efeff1;
                                        cursor: pointer;
                                        font-size: 12px;
                                        font-weight: 600;
                                        font-family: inherit;
                                        transition: background-color 0.1s ease-in;
                                        display: inline-flex;
                                        align-items: center;
                                        justify-content: center;
                                    ">${s}</button>
                                `).join('')}
                            </div>
                        </div>
                    ` : ''}
                `}
            </div>
        `;

        const closePanel = document.getElementById('offchat-close-panel');
        const embedBtn = document.getElementById('offchat-embed-btn');
        const channelInput = document.getElementById('offchat-channel-input');
        const focusStreamBtns = document.querySelectorAll('.offchat-focus-stream-btn');
        const removeStreamBtns = document.querySelectorAll('.offchat-remove-stream-btn');
        const toggleChatBtns = document.querySelectorAll('.offchat-toggle-chat-btn');
        const removeChatBtns = document.querySelectorAll('.offchat-remove-chat-btn');
        const recentBtns = document.querySelectorAll('.offchat-recent-btn');
        const header = document.getElementById('offchat-header');
        const streamIcon = document.getElementById('offchat-stream-icon');
        const chatIcon = document.getElementById('offchat-chat-icon');

        if (streamIcon) {
            streamIcon.onclick = () => {
                currentPanelMode = 'stream';
                updateControlPanel();
            };
            streamIcon.onmouseenter = () => streamIcon.style.color = '#a970ff';
            streamIcon.onmouseleave = () => streamIcon.style.color = currentPanelMode === 'stream' ? '#bf94ff' : '#898395';
        }

        if (chatIcon) {
            chatIcon.onclick = () => {
                currentPanelMode = 'chat';
                updateControlPanel();
            };
            chatIcon.onmouseenter = () => chatIcon.style.color = '#a970ff';
            chatIcon.onmouseleave = () => chatIcon.style.color = currentPanelMode === 'chat' ? '#bf94ff' : '#898395';
        }

        if (closePanel) {
            closePanel.onclick = () => {
                controlPanel.style.display = 'none';
                createShowButton();
            };
        }

        if (embedBtn && channelInput) {
            embedBtn.onclick = () => {
                const value = channelInput.value.trim();
                if (value) {
                    if (currentPanelMode === 'stream') {
                        createEmbedPlayer(value);
                    } else {
                        createChatWindow(value);
                    }
                } else {
                    channelInput.style.borderColor = '#9147ff';
                    channelInput.focus();
                }
            };
            embedBtn.onmouseenter = () => embedBtn.style.background = '#772ce8';
            embedBtn.onmouseleave = () => embedBtn.style.background = '#9147ff';
            channelInput.onkeydown = (e) => { if (e.key === 'Enter') embedBtn.click(); };
            channelInput.onfocus = () => {
                channelInput.style.borderColor = '#a970ff';
                channelInput.style.background = 'rgba(14, 14, 16, 1)';
            };
            channelInput.onblur = () => {
                channelInput.style.borderColor = '#464649';
                channelInput.style.background = 'rgba(14, 14, 16, 1)';
            };
        }

        focusStreamBtns.forEach(btn => {
            const index = parseInt(btn.dataset.index);
            btn.onclick = () => toggleStreamFocus(index);
            btn.onmouseenter = () => btn.style.background = 'rgba(83, 83, 95, 0.48)';
            btn.onmouseleave = () => btn.style.background = 'rgba(83, 83, 95, 0.38)';
        });

        removeStreamBtns.forEach(btn => {
            const streamId = btn.dataset.id;
            const streamType = btn.dataset.type;
            btn.onclick = () => removeStream(streamId, streamType);
            btn.onmouseenter = () => btn.style.background = 'rgba(83, 83, 95, 0.48)';
            btn.onmouseleave = () => btn.style.background = 'rgba(83, 83, 95, 0.38)';
        });

        toggleChatBtns.forEach(btn => {
            const channel = btn.dataset.channel;
            btn.onclick = () => {
                const chat = openChats.find(c => c.channel === channel);
                if (chat) {
                    if (chat.isMinimized) {
                        showChatWindow(channel);
                    } else {
                        minimizeChatWindow(channel);
                    }
                }
            };
            btn.onmouseenter = () => btn.style.background = 'rgba(83, 83, 95, 0.48)';
            btn.onmouseleave = () => btn.style.background = 'rgba(83, 83, 95, 0.38)';
        });

        removeChatBtns.forEach(btn => {
            const channel = btn.dataset.channel;
            btn.onclick = () => removeChatWindow(channel);
            btn.onmouseenter = () => btn.style.background = 'rgba(83, 83, 95, 0.48)';
            btn.onmouseleave = () => btn.style.background = 'rgba(83, 83, 95, 0.38)';
        });

        recentBtns.forEach(btn => {
            btn.onclick = () => {
                if (currentPanelMode === 'stream') {
                    createEmbedPlayer(btn.dataset.channel);
                } else {
                    createChatWindow(btn.dataset.channel);
                }
            };
            btn.onmouseenter = () => btn.style.background = 'rgba(83, 83, 95, 0.48)';
            btn.onmouseleave = () => btn.style.background = 'rgba(83, 83, 95, 0.38)';
        });

        if (header) makeDraggable(controlPanel, header);
    }

    function createShowButton() {
        let btn = document.getElementById('offchat-show-btn');
        if (btn && document.body.contains(btn)) {
            btn.style.display = 'inline-flex';
            btn.style.visibility = 'visible';
            return;
        }

        if (btn) btn.remove();

        btn = document.createElement('button');
        btn.id = 'offchat-show-btn';
        btn.title = 'Offchat Viewer';
        btn.innerHTML = `
            <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
                <path d="M5 3a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2V5a2 2 0 00-2-2H5zm0 1.5h10a.5.5 0 01.5.5v10a.5.5 0 01-.5.5H5a.5.5 0 01-.5-.5V5a.5.5 0 01.5-.5z"/>
                <path d="M8 7l5 3-5 3V7z"/>
            </svg>
        `;

        btn.style.cssText = `
            display: inline-flex !important;
            align-items: center;
            justify-content: center;
            width: 30px;
            height: 30px;
            background: transparent;
            border: none;
            border-radius: 4px;
            color: #dedee3;
            cursor: pointer;
            transition: background-color 0.1s, color 0.1s;
            padding: 0;
            margin-left: 5px;
            vertical-align: middle;
            visibility: visible !important;
            opacity: 1 !important;
            z-index: 9999;
        `;
        btn.onmouseenter = () => {
            btn.style.background = 'rgba(83, 83, 95, 0.48)';
            btn.style.color = '#fff';
        };
        btn.onmouseleave = () => {
            btn.style.background = 'transparent';
            btn.style.color = '#dedee3';
        };
        btn.onclick = () => {
            if (controlPanel) {
                controlPanel.style.display = 'block';
                btn.style.display = 'none';
            }
        };

        const insertButton = () => {
            // Remove any existing button first
            const existingBtn = document.getElementById('offchat-show-btn');
            if (existingBtn && existingBtn !== btn) {
                existingBtn.remove();
            }

            // Try multiple selectors for the share button
            const shareBtn = document.querySelector('[data-a-target="share-button"]') ||
                            document.querySelector('[aria-label="Share"]') ||
                            document.querySelector('button[aria-label*="Share"]') ||
                            document.querySelector('button[data-test-selector="share-button"]');

            if (shareBtn && shareBtn.parentNode) {
                shareBtn.parentNode.insertBefore(btn, shareBtn.nextSibling);
                console.log('[Offchat] Button inserted next to share button');
                return true;
            }

            // Try channel header buttons area
            const channelButtons = document.querySelector('.channel-header__user-tab-content .tw-align-items-center') ||
                                 document.querySelector('[data-a-target="channel-header-right"]') ||
                                 document.querySelector('.channel-info-content__action-container');

            if (channelButtons) {
                channelButtons.appendChild(btn);
                console.log('[Offchat] Button inserted in channel buttons area');
                return true;
            }

            // Try metadata layout
            const metadataLayout = document.querySelector('.metadata-layout__secondary-button-spacing');
            if (metadataLayout) {
                metadataLayout.appendChild(btn);
                console.log('[Offchat] Button inserted in metadata layout');
                return true;
            }

            return false;
        };

        if (!insertButton()) {
            // Keep trying very aggressively
            let attempts = 0;
            const retryInterval = setInterval(() => {
                attempts++;

                if (document.body.contains(btn)) {
                    // Button exists, try to relocate it to proper position
                    const shareBtn = document.querySelector('[data-a-target="share-button"]');
                    if (shareBtn && shareBtn.parentNode) {
                        // Check if button is not already next to share button
                        if (shareBtn.nextSibling !== btn) {
                            // Remove from current position and insert next to share
                            btn.remove();
                            shareBtn.parentNode.insertBefore(btn, shareBtn.nextSibling);
                            console.log('[Offchat] Button relocated next to share button');
                        }
                        // Button is in correct position, stop trying
                        clearInterval(retryInterval);
                        return;
                    }
                } else {
                    // Button doesn't exist, try to insert it
                    if (insertButton()) {
                        clearInterval(retryInterval);
                        return;
                    }

                    // After 5 attempts, use fallback position
                    if (attempts === 5) {
                        btn.style.cssText += `
                            position: fixed !important;
                            top: 60px !important;
                            right: 20px !important;
                            z-index: 10001 !important;
                            box-shadow: 0 2px 8px rgba(0,0,0,0.5) !important;
                            background: rgba(24, 24, 27, 0.95) !important;
                        `;
                        document.body.appendChild(btn);
                        console.log('[Offchat] Using fallback position (top-right)');
                        // Don't stop trying, continue to relocate if possible
                    }
                }

                // Give up after 60 attempts (30 seconds)
                if (attempts > 60) {
                    clearInterval(retryInterval);
                    console.log('[Offchat] Stopped trying to relocate button after 60 attempts');
                }
            }, 500);
        }
    }

    function cleanup() {
        removeStreamContainer();
        // Remove all chat windows
        [...openChats].forEach(chat => removeChatWindow(chat.channel));
        if (controlPanel) { controlPanel.remove(); controlPanel = null; }
        const showBtn = document.getElementById('offchat-show-btn');
        if (showBtn) showBtn.remove();
        if (buttonCheckInterval) { clearInterval(buttonCheckInterval); buttonCheckInterval = null; }
        currentPanelMode = 'stream';
        streamWrapperCache = {}; // Clear wrapper cache
        if (resizeUpdateBound) {
            window.removeEventListener('resize', resizeUpdateBound);
            window.removeEventListener('scroll', resizeUpdateBound);
            resizeUpdateBound = null;
        }
    }

    function init() {
        if (!isChannelPage()) return;
        currentChannel = getCurrentChannel();
        if (!currentChannel) return;
        lastChannel = currentChannel;

        setTimeout(() => {
            createControlPanel();
            controlPanel.style.display = 'none';
            createShowButton();

            // More frequent monitoring (every 1 second)
            buttonCheckInterval = setInterval(() => {
                const btn = document.getElementById('offchat-show-btn');
                if (!btn || !document.body.contains(btn)) {
                    console.log('[Offchat] Button missing, recreating...');
                    createShowButton();
                } else {
                    // Check if button is visible
                    const styles = window.getComputedStyle(btn);
                    if (styles.display === 'none' || styles.visibility === 'hidden' || styles.opacity === '0') {
                        console.log('[Offchat] Button hidden, making visible...');
                        btn.style.display = 'inline-flex';
                        btn.style.visibility = 'visible';
                        btn.style.opacity = '1';
                    }

                    // Try to relocate if not next to share button
                    const shareBtn = document.querySelector('[data-a-target="share-button"]');
                    if (shareBtn && shareBtn.parentNode && shareBtn.nextSibling !== btn) {
                        const currentPos = btn.style.position;
                        // Only relocate if not in fallback fixed position
                        if (currentPos !== 'fixed') {
                            btn.remove();
                            shareBtn.parentNode.insertBefore(btn, shareBtn.nextSibling);
                            console.log('[Offchat] Button relocated to correct position');
                        }
                    }
                }
            }, 1000);
        }, 2000);
    }

    let lastUrl = location.href;

    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            const newChannel = getCurrentChannel();

            if (newChannel !== lastChannel) {
                cleanup();
                lastChannel = newChannel;
                setTimeout(init, 1500);
            } else if (newChannel) {
                // Tab switched on same channel - recreate button
                console.log('[Offchat] Tab changed, ensuring button exists');
                setTimeout(() => {
                    const btn = document.getElementById('offchat-show-btn');
                    if (!btn || !document.body.contains(btn)) {
                        createShowButton();
                    } else {
                        // Button exists but might be in wrong place, try to relocate
                        const shareBtn = document.querySelector('[data-a-target="share-button"]');
                        if (shareBtn && shareBtn.parentNode && !shareBtn.parentNode.contains(btn)) {
                            btn.remove();
                            createShowButton();
                        }
                    }
                }, 1500);
            }
        }
    }).observe(document, { subtree: true, childList: true });

    GM_registerMenuCommand('Clear Recent Streams', () => {
        config.lastStreams = [];
        GM_setValue('lastStreams', []);
        updateControlPanel();
    });

    GM_registerMenuCommand('Show Panel', () => {
        if (controlPanel) {
            controlPanel.style.display = 'block';
            const showBtn = document.getElementById('offchat-show-btn');
            if (showBtn) showBtn.style.display = 'none';
        }
    });

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();