Twitch Offchat Stream Viewer

Watch another stream/video while chilling in an offline chat

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Twitch Offchat Stream Viewer
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Watch another stream/video 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;

            if (channel === currentChannel) {
                alert("That's the channel you're already in!");
                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;

        if (channel === currentChannel) {
            alert("That's the channel you're already in!");
            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();
    }

})();