Switcher Stream Channel 1.10.19

Replace video feed with specified channel's video stream (Twitch, YouTube, or Kick) and provide draggable control panel functionality

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

// ==UserScript==
// @name         Switcher Stream Channel 1.10.19
// @version      1.10.19
// @license      MIT 
// @description  Replace video feed with specified channel's video stream (Twitch, YouTube, or Kick) and provide draggable control panel functionality
// @author       Gullampis810
// @match        https://www.twitch.tv/*
// @icon         https://wwwcdn.cincopa.com/blogres/wp-content/uploads/2019/08/bigstock-247687237.jpg
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    const state = {
        channelName: 'tapa_tapa_mateo',
        favoriteChannels: JSON.parse(localStorage.getItem('favoriteChannels')) || [],
        channelHistory: JSON.parse(localStorage.getItem('channelHistory')) || [],
        panelColor: localStorage.getItem('panelColor') || 'rgba(255, 255, 255, 0.15)',
        buttonColor: localStorage.getItem('buttonColor') || 'rgba(255, 255, 255, 0.3)',
        panelPosition: JSON.parse(localStorage.getItem('panelPosition')) || { top: '20px', left: '20px' },
        isPanelHidden: false
    };

    state.favoriteChannels.sort((a, b) => a.localeCompare(b));
    state.channelHistory.sort((a, b) => a.localeCompare(b));

    const panel = createControlPanel();
    const toggleButton = createToggleButton();
    document.body.appendChild(panel);
    document.body.appendChild(toggleButton);

    setPanelPosition(panel, state.panelPosition);
    enableDrag(panel);
    window.addEventListener('load', loadStream);

    // Вспомогательная функция для извлечения YouTube ID из ссылки
    function getYouTubeVideoId(url) {
        const regex = /(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=))([\w-]{11})/;
        const match = url.match(regex);
        return match ? match[1] : null;
    }

    // Вспомогательная функция для извлечения имени канала Kick из ссылки
    function getKickChannelName(url) {
        const regex = /(?:https?:\/\/)?(?:www\.)?kick\.com\/([\w-]+)/;
        const match = url.match(regex);
        return match ? match[1] : null;
    }

    // Обновленная функция loadStream
  function loadStream() {
    setTimeout(() => {
        const player = document.querySelector('.video-player__container');
        if (player) {
            player.innerHTML = '';
            
            // Проверка на HLS (.m3u8) поток
            if (state.channelName.endsWith('.m3u8') || state.channelName.includes('.m3u8')) {
            // Принудительная замена http на https для обхода Mixed Content
                state.channelName = state.channelName.replace(/^http:/, 'https:');
                const video = document.createElement('video');
                video.style.cssText = 'width: 100%; height: 100%; border-radius: 0px;';
                video.controls = true;
                video.autoplay = true; // Добавляем автозапуск
                video.muted = false; // Не muted по умолчанию
                
                if (Hls.isSupported()) {
                    const hls = new Hls({
                        enableWorker: true,
                        debug: true, // Включаем отладку для логов в консоли (отключите позже, если не нужно)
                                                 lowLatencyMode: true, // Для live-стримов, как Twitch
                        backBufferLength: 90, // Увеличиваем back-buffer для обхода "дыр"
                        maxBufferHole: 0.5, // ТOLERANCE для дыр в буфере
                        manifestLoadPolicy: {
                            default: {
                                maxTimeToFirstByteMs: Infinity,
                                maxLoadTimeMs: 10000,
                                timeoutRetry: { maxNumRetry: 2, retryDelayMs: 0, maxRetryDelayMs: 0 },
                                errorRetry: { maxNumRetry: 1, retryDelayMs: 1000, maxRetryDelayMs: 8000 }
                            }
                        },
                        playlistLoadPolicy: {
                            default: {
                                maxTimeToFirstByteMs: Infinity,
                                maxLoadTimeMs: 10000,
                                timeoutRetry: { maxNumRetry: 2, retryDelayMs: 0, maxRetryDelayMs: 0 },
                                errorRetry: { maxNumRetry: 2, retryDelayMs: 1000, maxRetryDelayMs: 8000 }
                            }
                        },
                        fragLoadPolicy: {
                            default: {
                                maxTimeToFirstByteMs: Infinity,
                                maxLoadTimeMs: 20000,
                                timeoutRetry: { maxNumRetry: 4, retryDelayMs: 0, maxRetryDelayMs: 0 },
                                errorRetry: { maxNumRetry: 6, retryDelayMs: 1000, maxRetryDelayMs: 8000 }
                            }
                        },
                    });
                    hls.loadSource(state.channelName);
                    hls.attachMedia(video);
                    hls.on(Hls.Events.MANIFEST_PARSED, () => {
                        video.play().catch(error => console.error('Play error:', error)); // Обработка ошибок автозапуска
                    });
                    hls.on(Hls.Events.ERROR, (event, data) => {
                        console.error('HLS error:', data.type, data.details, data.fatal); // Детальные логи ошибок
                        if (data.fatal) {
                            switch (data.type) {
                                case Hls.ErrorTypes.NETWORK_ERROR:
                                    console.error('Fatal network error - trying to recover');
                                    hls.startLoad();
                                    break;
                                case Hls.ErrorTypes.MEDIA_ERROR:
                                    console.error('Fatal media error - trying to recover');
                                    hls.recoverMediaError();
                                    break;
                                default:
                                    hls.destroy();
                                    break;
                            }
                        }
                    });
                    hls.on(Hls.Events.FRAG_LOADING, (event, data) => {
                        console.log('Loading fragment:', data.frag.url); // Лог для отслеживания циклов
                    });
                } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
                    video.src = state.channelName;
                    video.addEventListener('loadedmetadata', () => {
                        video.play().catch(error => console.error('Native play error:', error));
                    });
                } else {
                    console.error('HLS не поддерживается в этом браузере.');
                }
                
                player.appendChild(video);
            } else {
                // Оригинальная логика для Twitch, YouTube, Kick
                const iframe = document.createElement('iframe');
                iframe.style.cssText = 'width: 100%; height: 100%; border-radius: 12px;';
                iframe.allowFullscreen = true;

                const youtubeId = getYouTubeVideoId(state.channelName);
                const kickChannel = getKickChannelName(state.channelName);
                if (youtubeId) {
                    iframe.src = `https://www.youtube.com/embed/${youtubeId}?autoplay=1&mute=0`;
                } else if (kickChannel) {
                    iframe.src = `https://kick.com/${kickChannel}?autoplay=true&muted=false`;
                } else {
                    iframe.src = `https://player.twitch.tv/?channel=${state.channelName}&parent=twitch.tv&quality=1080p&muted=false`;
                }
                player.appendChild(iframe);
            }
        }
    }, 3000); // Увеличили задержку до 3 сек для стабильности
}

    // Обновление функции createChannelInput для поддержки YouTube и Kick ссылок
    function createChannelInput() {
        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'Enter channel name, YouTube URL, or Kick URL';
        Object.assign(input.style, {
            width: '100%',
            marginBottom: '16px',
            padding: '12px 16px',
            borderRadius: '12px',
            border: '1px solid rgba(255, 255, 255, 0.2)',
            backgroundColor: 'rgba(255, 255, 255, 0.1)',
            color: 'rgba(255, 255, 255, 0.9)',
            fontSize: '16px',
            backdropFilter: 'blur(10px)',
            boxShadow: '0 4px 15px rgba(0, 0, 0, 0.05)',
            transition: 'all 0.2s ease'
        });
        input.addEventListener('input', (e) => state.channelName = e.target.value.trim());
        input.addEventListener('focus', () => input.style.borderColor = 'rgba(255, 255, 255, 0.4)');
        input.addEventListener('blur', () => input.style.borderColor = 'rgba(255, 255, 255, 0.2)');
        return input;
    }

    // Остальные функции остаются без изменений
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.className = 'switcher-panel';
        Object.assign(panel.style, {
            position: 'fixed',
            width: '340px',
            padding: '20px',
            background: 'linear-gradient(45deg, #235550, #211831)',
            borderRadius: '20px',
            border: '1px solid rgba(255, 255, 255, 0.2)',
            boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
            zIndex: '9999',
            transition: 'transform 0.3s ease, opacity 0.3s ease',
            cursor: 'move'
        });
        panel.style.setProperty('background', 'linear-gradient(45deg, #235550, #211831)', 'important');

        state.panelColor = 'linear-gradient(45deg, #235550, #211831)';
        localStorage.setItem('panelColor', state.panelColor);

        const content = document.createElement('div');

        const header = document.createElement('div');
        Object.assign(header.style, {
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            marginBottom: '20px',
        });

        const title = createTitle('Channel Switcher v1.10.19');
        const hideBtn = document.createElement('button');
        hideBtn.textContent = '×';
        Object.assign(hideBtn.style, {
            width: '40px',
            height: '40px',
            border: 'none',
            borderRadius: '50%',
            fontSize: '24px',
            color: 'rgba(255, 255, 255, 0.8)',
            cursor: 'pointer',
            backdropFilter: 'blur(10px)',
            boxShadow: '0 4px 15px rgba(0, 0, 0, 0.1)',
            transition: 'all 0.2s ease',
            display: 'flex',
            justifyContent: 'center'
        });
        hideBtn.addEventListener('click', togglePanel);
        hideBtn.addEventListener('mouseover', () => hideBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.3)');
        hideBtn.addEventListener('mouseout', () => hideBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.2)');

        header.append(title, hideBtn);

        content.append(
            createChannelInput(),
            createButton('Play Channel', loadInputChannel, 'play-btn'),
            createSelect(state.favoriteChannels, 'Favorites', 'favorites-select'),
            createSelect(state.channelHistory, 'History', 'history-select'),
            createButton('Play Selected', loadSelectedChannel, 'play-selected-btn'),
            createButton('Add to Favorites', addChannelToFavorites, 'add-fav-btn'),
            createButton('Remove Favorite', removeChannelFromFavorites, 'remove-fav-btn'),
            createButton('Clear History', clearHistory, 'clear-history-btn'),
            createColorPicker('Button Color', 'button-color-picker', updateButtonColor)
        );

        panel.append(header, content);
        return panel;
    }

    function updatePanelColor(e) {
        const hex = e.target.value;
        state.panelColor = hex;
        panel.style.setProperty('background', hex, 'important');
        localStorage.setItem('panelColor', state.panelColor);
    }

    function createToggleButton() {
        const button = document.createElement('button');
        button.className = 'toggle-visibility';
        Object.assign(button.style, {
            position: 'fixed',
            top: '16px',
            left: '490px',
            width: '40px',
            height: '40px',
            backgroundColor: 'rgba(27, 173, 117, 0.2)',
            borderRadius: '50%',
            border: '1px solid rgba(80, 245, 203, 0.66)',
            cursor: 'pointer',
            zIndex: '10000',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            backdropFilter: 'blur(10px)',
            boxShadow: '0 4px 15px rgba(0, 255, 76, 0.1)',
            transition: 'all 0.2s ease'
        });

        const eyeShowSvg = `
            <svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                <path d="M8 4a4 4 0 0 1 4 4 4 4 0 0 1-4 4 4 4 0 0 1-4-4 4 4 0 0 1 4-4zm0 1.5A2.5 2.5 0 0 0 5.5 8 2.5 2.5 0 0 0 8 10.5 2.5 2.5 0 0 0 10.5 8 2.5 2.5 0 0 0 8 5.5zM16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zm-1.5 0s-2.5-4-6.5-4S1.5 8 1.5 8s2.5 4 6.5 4 6.5-4 6.5-4z"/>
            </svg>
        `;

        const eyeHideSvg = `
            <svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                <path d="M2.146 2.854a.5.5 0 1 1 .708-.708l10 10a.5.5 0 0 1-.708.708l-10-10zM8 4a4 4 0 0 1 4 4 4 4 0 0 1-.672 2.176l-.672-.672A2.5 2.5 0 0 0 10.5 8 2.5 2.5 0 0 0 8 5.5a2.5 2.5 0 0 0-1.672.672l-.672-.672A4 4 0 0 1 8 4zm6.5 4s-2.5-4-6.5-4c-.89 0-1.74.192-2.52.536l-.672-.672C5.74 3.298 6.81 3 8 3c5 0 8 5.5 8 5.5s-1.896 3.446-5 4.674l-.672-.672C12.604 11.274 14.5 8.828 14.5 8zM1.5 8s2.5-4 6.5-4c.89 0 1.74.192 2.52.536l.672.672C10.26 3.298 9.19 3 8 3 3 3 0 8.5 0 8.5s1.896 3.446 5 4.674l.672-.672C3.396 11.274 1.5 8.828 1.5 8z"/>
            </svg>
        `;

        const svgContainer = document.createElement('div');
        svgContainer.innerHTML = state.isPanelHidden ? eyeHideSvg : eyeShowSvg;
        Object.assign(svgContainer.style, {
            width: '28px',
            height: '28px',
            filter: 'brightness(100)'
        });

        button.appendChild(svgContainer);

        button.addEventListener('click', () => {
            togglePanelVisibility();
            svgContainer.innerHTML = state.isPanelHidden ? eyeHideSvg : eyeShowSvg;
        });
        button.addEventListener('mouseover', () => button.style.backgroundColor = 'rgba(255, 255, 255, 0.3)');
        button.addEventListener('mouseout', () => button.style.backgroundColor = 'rgba(255, 255, 255, 0.2)');

        return button;
    }

    function createTitle(text) {
        const title = document.createElement('h3');
        title.textContent = text;
        Object.assign(title.style, {
            margin: '0',
            fontSize: '20px',
            fontWeight: '500',
            color: 'rgba(255, 255, 255, 0.9)'
        });
        return title;
    }

    function createSelect(options, labelText, className) {
        const container = document.createElement('div');
        container.className = className;
        container.style.marginBottom = '16px';
        container.style.position = 'relative';

        const label = document.createElement('label');
        label.textContent = labelText;
        Object.assign(label.style, {
            display: 'block',
            marginBottom: '4px',
            fontSize: '12px',
            color: 'rgba(255, 255, 255, 0.7)',
            fontWeight: '500'
        });

        const selectBox = document.createElement('div');
        Object.assign(selectBox.style, {
    width: '100%',
    padding: '12px 16px',
    borderRadius: '12px',
    backgroundColor: 'rgba(255, 255, 255, 0.05)',
    color: 'rgba(255, 255, 255, 0.9)',
    border: '1px solid rgba(255, 255, 255, 0.2)',
    fontSize: '16px',
    backdropFilter: 'blur(15px)',
    boxShadow: '0 4px 15px rgba(0, 0, 0, 0.05)',
    cursor: 'pointer',
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    flexWrap: 'nowrap',  // Новое: запрещает перенос
    overflow: 'hidden'   // Новое: скрывает переполнение
});

        const selectedText = document.createElement('span');
        selectedText.textContent = options.length ? options[0] : 'No items';
        Object.assign(selectedText.style, {
            overflow: 'hidden',
            textOverflow: 'ellipsis',
            whiteSpace: 'nowrap',
            width: 'calc(100% - 20px)',  // Учитываем ширину стрелки (около 20px) и отступы
            display: 'block'  // Чтобы ellipsis работал корректно
        });

        const arrow = document.createElement('span');
        arrow.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="rgba(255,255,255,0.7)"><path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/></svg>';

        const dropdown = document.createElement('div');
        Object.assign(dropdown.style, {
            position: 'absolute',
            top: '100%',
            left: '0',
            width: '100%',
            maxHeight: '200px',
            overflowY: 'auto',
            backgroundColor: 'rgba(255, 255, 255, 0.1)',
            borderRadius: '12px',
            border: '1px solid rgba(255, 255, 255, 0.2)',
            backdropFilter: 'blur(15px)',
            boxShadow: '0 4px 15px rgba(0, 0, 0, 0.1)',
            display: 'none',
            zIndex: '10001'
        });

        options.forEach(option => {
            const item = document.createElement('div');
            Object.assign(item.style, {
                 padding: '10px 16px',
                 color: 'rgba(255, 255, 255, 0.9)',
                 cursor: 'pointer',
                 transition: 'background-color 0.2s ease',
                 overflow: 'hidden',
                 textOverflow: 'ellipsis',
                 whiteSpace: 'nowrap',
                 width: '100%',
                 display: 'flex',
                 justifyContent: 'space-between',
                 alignItems: 'center'
             });
            
             const textSpan = document.createElement('span');
Object.assign(textSpan.style, {
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
    flex: '1'
});
textSpan.textContent = option;

const deleteBtn = document.createElement('button');
 deleteBtn.innerHTML = `
    <svg xmlns="http://www.w3.org/2000/svg" 
         width="16" 
         height="16"
         shape-rendering="geometricPrecision" 
         text-rendering="geometricPrecision" 
         image-rendering="optimizeQuality" 
         fill-rule="evenodd" 
         clip-rule="evenodd" 
         viewBox="0 0 456 511.82">
         <path fill="rgba(255, 117, 117, 0.83)" 
               d="M48.42 140.13h361.99c17.36 0 29.82 9.78 28.08 28.17l-30.73 317.1c-1.23 13.36-8.99 26.42-25.3 26.42H76.34c-13.63-.73-23.74-9.75-25.09-24.14L20.79 168.99c-1.74-18.38 9.75-28.86 27.63-28.86zM24.49 38.15h136.47V28.1c0-15.94 10.2-28.1 27.02-28.1h81.28c17.3 0 27.65 11.77 27.65 28.01v10.14h138.66c.57 0 1.11.07 1.68.13 10.23.93 18.15 9.02 18.69 19.22.03.79.06 1.39.06 2.17v42.76c0 5.99-4.73 10.89-10.62 11.19-.54 0-1.09.03-1.63.03H11.22c-5.92 0-10.77-4.6-11.19-10.38 0-.72-.03-1.47-.03-2.23v-39.5c0-10.93 4.21-20.71 16.82-23.02 2.53-.45 5.09-.37 7.67-.37zm83.78 208.38c-.51-10.17 8.21-18.83 19.53-19.31 11.31-.49 20.94 7.4 21.45 17.57l8.7 160.62c.51 10.18-8.22 18.84-19.53 19.32-11.32.48-20.94-7.4-21.46-17.57l-8.69-160.63zm201.7-1.74c.51-10.17 10.14-18.06 21.45-17.57 11.32.48 20.04 9.14 19.53 19.31l-8.66 160.63c-.52 10.17-10.14 18.05-21.46 17.57-11.31-.48-20.04-9.14-19.53-19.32l8.67-160.62zm-102.94.87c0-10.23 9.23-18.53 20.58-18.53 11.34 0 20.58 8.3 20.58 18.53v160.63c0 10.23-9.24 18.53-20.58 18.53-11.35 0-20.58-8.3-20.58-18.53V245.66z"/>
    </svg>
`;
Object.assign(deleteBtn.style, {
    width: '20px',
    height: '20px',
    border: 'none',
    borderRadius: '50%',
    backgroundColor: 'transparent',
    color: 'rgba(255, 255, 255, 0.7)',
    fontSize: '16px',
    cursor: 'pointer',
    marginLeft: '8px',
    transition: 'color 0.2s ease'
});
deleteBtn.addEventListener('click', (e) => {
    e.stopPropagation();  // Предотвращаем клик по item
    const container = item.closest('.favorites-select, .history-select');
    const listType = container.className.includes('favorites') ? 'favorites' : 'history';
    removeFromList(option, listType);
});
deleteBtn.addEventListener('mouseover', () => deleteBtn.style.color = 'rgba(255, 255, 255, 1)');
deleteBtn.addEventListener('mouseout', () => deleteBtn.style.color = 'rgba(255, 255, 255, 0.7)');

item.appendChild(textSpan);
item.appendChild(deleteBtn);
            item.addEventListener('click', (e) => {
            if (e.target.tagName !== 'BUTTON') {
        state.channelName = option;
        selectedText.textContent = option;
        dropdown.style.display = 'none';
            }
        });
            item.addEventListener('mouseover', () => item.style.backgroundColor = 'rgba(255, 255, 255, 0.2)');
            item.addEventListener('mouseout', () => item.style.backgroundColor = 'transparent');
            dropdown.appendChild(item);
        });

        selectBox.append(selectedText, arrow);
        container.append(label, selectBox, dropdown);

        selectBox.addEventListener('click', () => {
            dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
        });

        document.addEventListener('click', (e) => {
            if (!container.contains(e.target)) {
                dropdown.style.display = 'none';
            }
        });

        return container;
    }

    function createButton(text, onClick, className) {
        const button = document.createElement('button');
        button.textContent = text;
        button.className = className;
        Object.assign(button.style, {
            width: '100%',
            marginBottom: '12px',
            padding: '14px 16px',
            backgroundColor: state.buttonColor,
            color: 'rgba(255, 255, 255, 0.9)',
            border: '1px solid rgba(255, 255, 255, 0.2)',
            borderRadius: '12px',
            fontSize: '16px',
            fontWeight: '500',
            cursor: 'pointer',
            backdropFilter: 'blur(10px)',
            boxShadow: '0 4px 15px rgba(0, 0, 0, 0.1)',
            transition: 'all 0.2s ease'
        });
        button.addEventListener('click', onClick);
        button.addEventListener('mouseover', () => button.style.backgroundColor = 'rgba(255, 255, 255, 0.4)');
        button.addEventListener('mouseout', () => button.style.backgroundColor = state.buttonColor);
        button.addEventListener('mousedown', () => button.style.transform = 'scale(0.98)');
        button.addEventListener('mouseup', () => button.style.transform = 'scale(1)');
        return button;
    }

    function createColorPicker(labelText, className, onChange) {
        const container = document.createElement('div');
        container.style.marginBottom = '16px';

        const label = document.createElement('label');
        label.textContent = labelText;
        Object.assign(label.style, {
            display: 'block',
            marginBottom: '4px',
            fontSize: '12px',
            color: 'rgba(255, 255, 255, 0.7)',
            fontWeight: '500'
        });

        const picker = document.createElement('input');
        picker.type = 'color';
        picker.className = className;
        picker.value = labelText.includes('Panel') ? rgbaToHex(state.panelColor) : rgbaToHex(state.buttonColor);
        Object.assign(picker.style, {
            width: '100%',
            height: '48px',
            padding: '0',
            border: '1px solid rgba(255, 255, 255, 0.2)',
            borderRadius: '12px',
            cursor: 'pointer',
            backdropFilter: 'blur(10px)',
            boxShadow: '0 4px 15px rgba(0, 0, 0, 0.05)',
            transition: 'all 0.2s ease',
            background: '#8b008b00',
        });
        picker.addEventListener('input', onChange);

        container.append(label, picker);
        return container;
    }

    function rgbaToHex(rgba) {
        const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
        if (!match) return rgba;
        const r = parseInt(match[1]).toString(16).padStart(2, '0');
        const g = parseInt(match[2]).toString(16).padStart(2, '0');
        const b = parseInt(match[3]).toString(16).padStart(2, '0');
        return `#${r}${g}${b}`;
    }

    function updateButtonColor(e) {
        const hex = e.target.value;
        state.buttonColor = `${hex}4D`;
        localStorage.setItem('buttonColor', state.buttonColor);
        panel.querySelectorAll('button:not(.toggle-visibility)').forEach(button => {
            button.style.backgroundColor = state.buttonColor;
        });
    }

    function togglePanel() {
        state.isPanelHidden = !state.isPanelHidden;
        panel.style.transform = state.isPanelHidden ? 'scale(0.95)' : 'scale(1)';
        panel.style.opacity = state.isPanelHidden ? '0' : '1';
        panel.style.pointerEvents = state.isPanelHidden ? 'none' : 'auto';
    }

    function togglePanelVisibility() {
        const svgContainer = document.querySelector('.toggle-visibility div');
        const isHidden = panel.style.opacity === '0' || !panel.style.opacity;

        if (isHidden) {
            panel.style.transform = 'scale(1)';
            panel.style.opacity = '1';
            panel.style.pointerEvents = 'auto';
            state.isPanelHidden = false;
            svgContainer.innerHTML = `
                <svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                    <path d="M8 4a4 4 0 0 1 4 4 4 4 0 0 1-4 4 4 4 0 0 1-4-4 4 4 0 0 1 4-4zm0 1.5A2.5 2.5 0 0 0 5.5 8 2.5 2.5 0 0 0 8 10.5 2.5 2.5 0 0 0 10.5 8 2.5 2.5 0 0 0 8 5.5zM16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zm-1.5 0s-2.5-4-6.5-4S1.5 8 1.5 8s2.5 4 6.5 4 6.5-4 6.5-4z"/>
                </svg>
            `;
        } else {
            panel.style.transform = 'scale(0.95)';
            panel.style.opacity = '0';
            panel.style.pointerEvents = 'none';
            state.isPanelHidden = true;
            svgContainer.innerHTML = `
                <svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                    <path d="M2.146 2.854a.5.5 0 1 1 .708-.708l10 10a.5.5 0 0 1-.708.708l-10-10zM8 4a4 4 0 0 1 4 4 4 4 0 0 1-.672 2.176l-.672-.672A2.5 2.5 0 0 0 10.5 8 2.5 2.5 0 0 0 8 5.5a2.5 2.5 0 0 0-1.672.672l-.672-.672A4 4 0 0 1 8 4zm6.5 4s-2.5-4-6.5-4c-.89 0-1.74.192-2.52.536l-.672-.672C5.74 3.298 6.81 3 8 3c5 0 8 5.5 8 5.5s-1.896 3.446-5 4.674l-.672-.672C12.604 11.274 14.5 8.828 14.5 8zM1.5 8s2.5-4 6.5-4c.89 0 1.74.192 2.52.536l.672.672C10.26 3.298 9.19 3 8 3 3 3 0 8.5 0 8.5s1.896 3.446 5 4.674l.672-.672C3.396 11.274 1.5 8.828 1.5 8z"/>
                </svg>
            `;
        }
    }

    function loadInputChannel() {
        if (state.channelName) {
            loadStream();
            addChannelToHistory(state.channelName);
        } else {
            alert('Пожалуйста, введите имя канала, YouTube-ссылку или Kick-ссылку.');
        }
    }

    function loadSelectedChannel() {
        if (state.channelName) {
            loadStream();
            addChannelToHistory(state.channelName);
        } else {
            alert('Пожалуйста, выберите канал, YouTube-ссылку или Kick-ссылку.');
        }
    }

    function addChannelToFavorites() {
        if (state.channelName && !state.favoriteChannels.includes(state.channelName)) {
            state.favoriteChannels.push(state.channelName);
            state.favoriteChannels.sort((a, b) => a.localeCompare(b));
            localStorage.setItem('favoriteChannels', JSON.stringify(state.favoriteChannels));
            alert(`Добавлено ${state.channelName} в избранное!`);
            updateOptions(document.querySelector('.favorites-select'), state.favoriteChannels);
        } else if (!state.channelName) {
            alert('Пожалуйста, введите имя канала, YouTube-ссылку или Kick-ссылку.');
        }
    }

    function removeChannelFromFavorites() {
        if (!state.channelName) {
            alert('Пожалуйста, выберите канал, YouTube-ссылку или Kick-ссылку для удаления.');
            return;
        }
        if (!state.favoriteChannels.includes(state.channelName)) {
            alert(`${state.channelName} отсутствует в избранном.`);
            return;
        }
        state.favoriteChannels = state.favoriteChannels.filter(ch => ch !== state.channelName);
        state.favoriteChannels.sort((a, b) => a.localeCompare(b));
        localStorage.setItem('favoriteChannels', JSON.stringify(state.favoriteChannels));
        updateOptions(document.querySelector('.favorites-select'), state.favoriteChannels);
    }

    function clearHistory() {
        state.channelHistory = [];
        localStorage.setItem('channelHistory', JSON.stringify(state.channelHistory));
        updateOptions(document.querySelector('.history-select'), state.channelHistory);
    }

    function addChannelToHistory(channel) {
        if (channel && !state.channelHistory.includes(channel)) {
            state.channelHistory.push(channel);
            state.channelHistory.sort((a, b) => a.localeCompare(b));
            localStorage.setItem('channelHistory', JSON.stringify(state.channelHistory));
            updateOptions(document.querySelector('.history-select'), state.channelHistory);
        }
    }

    function updateOptions(select, options) {
           const dropdown = select.querySelector('div[style*="position: absolute"]'); // Ищем dropdown по стилю (можно добавить class для точности)
    if (!dropdown) {
        console.error('Dropdown not found in select container');
        return;
    }
    dropdown.innerHTML = '';
    const selectedText = select.querySelector('span'); // Обновляем текст выбранного, если нужно
    if (options.length === 0) {
        if (selectedText) selectedText.textContent = 'No items';
        return;
    }
    if (selectedText) selectedText.textContent = options[0]; // Устанавливаем первый как выбранный по умолчанию
    options.forEach(option => {
        const item = document.createElement('div');
       Object.assign(item.style, {
                 padding: '10px 16px',
                 color: 'rgba(255, 255, 255, 0.9)',
                 cursor: 'pointer',
                 transition: 'background-color 0.2s ease',
                 overflow: 'hidden',
                 textOverflow: 'ellipsis',
                 whiteSpace: 'nowrap',
                 width: '100%'
          });
        item.textContent = option;
        item.addEventListener('click', () => {
            state.channelName = option;
            if (selectedText) selectedText.textContent = option;
            dropdown.style.display = 'none';
        });
        item.addEventListener('mouseover', () => item.style.backgroundColor = 'rgba(255, 255, 255, 0.2)');
        item.addEventListener('mouseout', () => item.style.backgroundColor = 'transparent');
        dropdown.appendChild(item);
    });
    }

    function enableDrag(element) {
        let isDragging = false, offsetX, offsetY;
        element.addEventListener('mousedown', (e) => {
            if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'SELECT') {
                isDragging = true;
                offsetX = e.clientX - element.getBoundingClientRect().left;
                offsetY = e.clientY - element.getBoundingClientRect().top;
                element.style.transition = 'none';
            }
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                const newLeft = e.clientX - offsetX;
                const newTop = e.clientY - offsetY;
                element.style.left = `${newLeft}px`;
                element.style.top = `${newTop}px`;
                localStorage.setItem('panelPosition', JSON.stringify({ top: `${newTop}px`, left: `${newLeft}px` }));
            }
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            element.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
        });
    }

    function setPanelPosition(element, position) {
        element.style.top = position.top;
        element.style.left = position.left;
    }
    function removeFromList(channel, listType) {
    if (listType === 'favorites') {
        if (state.favoriteChannels.includes(channel)) {
            state.favoriteChannels = state.favoriteChannels.filter(ch => ch !== channel);
            state.favoriteChannels.sort((a, b) => a.localeCompare(b));
            localStorage.setItem('favoriteChannels', JSON.stringify(state.favoriteChannels));
            updateOptions(document.querySelector('.favorites-select'), state.favoriteChannels);
            alert(`Удалено из избранного: ${channel}`);
        }
    } else if (listType === 'history') {
        if (state.channelHistory.includes(channel)) {
            state.channelHistory = state.channelHistory.filter(ch => ch !== channel);
            state.channelHistory.sort((a, b) => a.localeCompare(b));
            localStorage.setItem('channelHistory', JSON.stringify(state.channelHistory));
            updateOptions(document.querySelector('.history-select'), state.channelHistory);
            alert(`Удалено из истории: ${channel}`);
        }
    }
}
})();

QingJ © 2025

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