Replace video feed with specified channel's video stream (Twitch, YouTube, or Kick) and provide draggable control panel functionality
当前为 
// ==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或关注我们的公众号极客氢云获取最新地址