您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Elegantly switches between Twitch and Kick streams with advanced management features, custom styling, and improved performance
// ==UserScript== // @name TTK - Twitch To Kick: Multi Stream Auto Switcher // @namespace http://tampermonkey.net/ // @version 4.1.0 // @description Elegantly switches between Twitch and Kick streams with advanced management features, custom styling, and improved performance // @author Original: TheWhaleCow, Enhanced: Claude // @match https://www.twitch.tv/* // @grant GM_addStyle // @run-at document-start // ==/UserScript== (function() { 'use strict'; // =================== CONFIGURATION =================== const CONFIG = { storageKey: 'ttk-streamer-pairs', collapsedKey: 'ttk-collapsed', settingsVisibleKey: 'ttk-settings-visible', currentModeKey: 'ttk-current-mode', autoSwitchKey: 'ttk-auto-switch', checkInterval: 2000, initDelay: 1500, theme: { primary: '#9146FF', // Twitch Purple secondary: '#00B140', // Kick Green dark: '#18181B', // Dark background light: '#EFEFF1', // Light text border: '#3A3A3D', // Border color success: '#00B140', // Success color error: '#F43B47', // Error color highlight: '#772CE8', // Highlight color } }; // =================== DATA MANAGEMENT =================== const Storage = { get: (key, defaultValue = null) => { try { const data = localStorage.getItem(key); return data ? JSON.parse(data) : defaultValue; } catch (e) { console.error('TTK Storage error:', e); return defaultValue; } }, set: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { console.error('TTK Storage error:', e); return false; } }, getPairs: () => Storage.get(CONFIG.storageKey, []), savePairs: (pairs) => Storage.set(CONFIG.storageKey, pairs), isCollapsed: () => Storage.get(CONFIG.collapsedKey, false), setCollapsed: (state) => Storage.set(CONFIG.collapsedKey, state), isSettingsVisible: () => Storage.get(CONFIG.settingsVisibleKey, false), setSettingsVisible: (state) => Storage.set(CONFIG.settingsVisibleKey, state), getCurrentMode: () => Storage.get(CONFIG.currentModeKey, 'auto'), setCurrentMode: (mode) => Storage.set(CONFIG.currentModeKey, mode), isAutoSwitchEnabled: () => Storage.get(CONFIG.autoSwitchKey, true), setAutoSwitchEnabled: (state) => Storage.set(CONFIG.autoSwitchKey, state) }; // =================== STREAM MANAGEMENT =================== const StreamManager = { getCurrentChannel: () => { const match = window.location.pathname.match(/^\/([a-zA-Z0-9_]+)$/); return match ? match[1].toLowerCase() : null; }, getPairForCurrentChannel: () => { const currentChannel = StreamManager.getCurrentChannel(); if (!currentChannel) return null; const pairs = Storage.getPairs(); return pairs.find(p => p.twitch?.toLowerCase() === currentChannel); }, isTwitchPlayerOffline: () => { // Multiple selectors for better reliability return !!( document.querySelector('[data-test-selector="offline-channel-video"]') || document.querySelector('.channel-status-info [data-a-target="player-overlay-content-gate"]') || Array.from(document.querySelectorAll('.channel-info-content')).some(el => el.textContent.includes('offline') || el.textContent.includes('not available') ) ); }, kickStreamURL: (channel) => `https://player.kick.com/${channel}?muted=false`, twitchStreamURL: (channel) => `https://player.twitch.tv/?channel=${channel}&parent=twitch.tv&muted=false`, replacePlayerWithStream: (url) => { const player = document.querySelector('.video-player__container'); if (!player) return false; // Check if we already have our custom player const existingOverlay = document.getElementById('ttk-player-overlay'); if (existingOverlay) { const existingIframe = existingOverlay.querySelector('iframe'); if (existingIframe && existingIframe.src === url) return false; existingOverlay.innerHTML = ''; } else { // Create overlay const overlay = document.createElement('div'); overlay.id = 'ttk-player-overlay'; overlay.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 9999; background: #000; `; player.style.position = 'relative'; player.appendChild(overlay); } // Try to pause/mute all video and audio elements in the original player const videoElements = player.querySelectorAll('video'); videoElements.forEach(video => { if (video) { try { if (!video.paused) video.pause(); video.muted = true; video.volume = 0; // Disconnect any event listeners if possible if (video.pause) video.pause = () => {}; if (video.play) video.play = () => {}; } catch (e) { console.error('Error pausing video:', e); } } }); // Try to pause the player if it has a React instance try { const twitchPlayer = document.querySelector('[data-a-player-type="site"]'); if (twitchPlayer && twitchPlayer._reactInstance && typeof twitchPlayer._reactInstance.pausePlayer === 'function') { twitchPlayer._reactInstance.pausePlayer(); } } catch (e) { console.error('Error accessing Twitch player:', e); } // Create iframe in the overlay const iframe = document.createElement('iframe'); iframe.src = url; iframe.style.cssText = 'height:100%; width:100%; border:none;'; iframe.setAttribute('allowfullscreen', 'true'); document.getElementById('ttk-player-overlay').appendChild(iframe); UI.showNotification(`Stream changed to ${url.includes('kick') ? 'Kick' : 'Twitch'}`); return true; }, switchToTwitch: () => { const current = StreamManager.getCurrentChannel(); if (!current) return false; Storage.setCurrentMode('twitch'); UI.updateModeIndicator('twitch'); return StreamManager.replacePlayerWithStream(StreamManager.twitchStreamURL(current)); }, switchToKick: () => { const pair = StreamManager.getPairForCurrentChannel(); if (!pair || !pair.kick) return false; Storage.setCurrentMode('kick'); UI.updateModeIndicator('kick'); return StreamManager.replacePlayerWithStream(StreamManager.kickStreamURL(pair.kick)); }, autoSwitch: () => { if (!Storage.isAutoSwitchEnabled()) return; const currentChannel = StreamManager.getCurrentChannel(); if (!currentChannel) return; const pair = StreamManager.getPairForCurrentChannel(); if (!pair || !pair.kick) return; const currentMode = Storage.getCurrentMode(); // If mode is already set to kick or twitch, respect that choice if (currentMode === 'kick') { StreamManager.switchToKick(); return; } else if (currentMode === 'twitch') { StreamManager.switchToTwitch(); return; } // Auto mode - check if Twitch stream is offline if (StreamManager.isTwitchPlayerOffline()) { StreamManager.switchToKick(); } else { StreamManager.switchToTwitch(); } } }; // =================== USER INTERFACE =================== const UI = { styles: ` .ttk-container { position: fixed; right: 20px; top: 80px; display: flex; flex-direction: column; gap: 8px; z-index: 9999; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; transition: transform 0.3s ease; } .ttk-collapsed { transform: translateX(calc(100% + 10px)); } .ttk-button { padding: 8px 12px; font-size: 13px; font-weight: 600; border-radius: 6px; color: white; border: none; cursor: pointer; user-select: none; transition: all 0.2s ease; width: 160px; display: flex; align-items: center; gap: 6px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .ttk-button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } .ttk-button:active { transform: translateY(0); } .ttk-twitch-button { background-color: ${CONFIG.theme.primary}; } .ttk-twitch-button:hover { background-color: #772CE8; } .ttk-kick-button { background-color: ${CONFIG.theme.secondary}; } .ttk-kick-button:hover { background-color: #008A2D; } .ttk-auto-button { background-color: #555555; } .ttk-auto-button:hover { background-color: #666666; } .ttk-icon { font-size: 16px; display: inline-flex; } .ttk-indicator { width: 8px; height: 8px; border-radius: 50%; margin-left: auto; background-color: #888; } .ttk-indicator.active { background-color: #4ade80; box-shadow: 0 0 6px #4ade80; } .ttk-toggle { position: fixed; top: 120px; right: 20px; z-index: 10000; background: ${CONFIG.theme.dark}; color: ${CONFIG.theme.light}; border: 1px solid ${CONFIG.theme.border}; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; font-size: 14px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .ttk-toggle:hover { background: #333; transform: scale(1.1); } .ttk-settings-button { position: fixed; top: 160px; right: 20px; z-index: 10000; background: ${CONFIG.theme.dark}; color: ${CONFIG.theme.light}; border: 1px solid ${CONFIG.theme.border}; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; font-size: 14px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .ttk-settings-button:hover { background: #333; transform: scale(1.1); } .ttk-settings-panel { position: fixed; top: 200px; right: 20px; width: 320px; background: ${CONFIG.theme.dark}; color: ${CONFIG.theme.light}; border: 1px solid ${CONFIG.theme.border}; border-radius: 10px; padding: 16px; z-index: 10000; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); display: none; max-height: 60vh; overflow-y: auto; } .ttk-settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid ${CONFIG.theme.border}; } .ttk-settings-title { font-size: 16px; font-weight: 600; margin: 0; } .ttk-close-button { background: none; border: none; color: ${CONFIG.theme.light}; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; padding: 0; } .ttk-pair { background: #252528; border-radius: 6px; padding: 12px; margin-bottom: 12px; border: 1px solid ${CONFIG.theme.border}; } .ttk-pair-header { display: flex; justify-content: space-between; margin-bottom: 8px; } .ttk-pair-number { font-weight: 600; font-size: 14px; } .ttk-delete-pair { background: none; border: none; color: #f87171; cursor: pointer; font-size: 14px; padding: 0; } .ttk-input-group { margin-bottom: 8px; } .ttk-input-label { display: block; margin-bottom: 4px; font-size: 12px; color: #a0a0a5; } .ttk-input { width: 100%; padding: 8px; border-radius: 4px; border: 1px solid ${CONFIG.theme.border}; background: #1a1a1c; color: ${CONFIG.theme.light}; font-size: 13px; } .ttk-input:focus { outline: none; border-color: ${CONFIG.theme.highlight}; } .ttk-button-row { display: flex; gap: 8px; margin-top: 16px; } .ttk-action-button { flex: 1; padding: 8px 12px; border-radius: 4px; border: none; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 4px; } .ttk-primary-button { background: ${CONFIG.theme.primary}; color: white; } .ttk-primary-button:hover { background: #772CE8; } .ttk-secondary-button { background: #3A3A3D; color: ${CONFIG.theme.light}; } .ttk-secondary-button:hover { background: #4A4A4D; } .ttk-danger-button { background: #dc2626; color: white; } .ttk-danger-button:hover { background: #b91c1c; } .ttk-switch-container { display: flex; align-items: center; margin: 16px 0; justify-content: space-between; } .ttk-switch-label { font-size: 14px; } .ttk-switch { position: relative; display: inline-block; width: 46px; height: 24px; } .ttk-switch input { opacity: 0; width: 0; height: 0; } .ttk-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #3A3A3D; transition: .4s; border-radius: 34px; } .ttk-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .ttk-slider { background-color: ${CONFIG.theme.secondary}; } input:focus + .ttk-slider { box-shadow: 0 0 1px ${CONFIG.theme.secondary}; } input:checked + .ttk-slider:before { transform: translateX(22px); } .ttk-notification { position: fixed; bottom: 20px; right: 20px; background: rgba(0, 0, 0, 0.8); color: white; padding: 10px 16px; border-radius: 6px; font-size: 14px; z-index: 10001; opacity: 0; transform: translateY(10px); transition: all 0.3s ease; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; } .ttk-notification.show { opacity: 1; transform: translateY(0); } #ttk-player-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 9; background: #000; } `, init: () => { // Add styles if (typeof GM_addStyle !== 'undefined') { GM_addStyle(UI.styles); } else { const style = document.createElement('style'); style.textContent = UI.styles; document.head.appendChild(style); } UI.createButtons(); UI.createToggles(); UI.createSettingsPanel(); }, createButtons: () => { const container = document.createElement('div'); container.id = 'ttk-container'; container.className = 'ttk-container'; if (Storage.isCollapsed()) container.classList.add('ttk-collapsed'); // Kick button const kickBtn = document.createElement('button'); kickBtn.className = 'ttk-button ttk-kick-button'; kickBtn.innerHTML = '<span class="ttk-icon">⚡</span> Switch to Kick <span class="ttk-indicator" id="ttk-kick-indicator"></span>'; kickBtn.onclick = StreamManager.switchToKick; // Twitch button const twitchBtn = document.createElement('button'); twitchBtn.className = 'ttk-button ttk-twitch-button'; twitchBtn.innerHTML = '<span class="ttk-icon">👾</span> Switch to Twitch <span class="ttk-indicator" id="ttk-twitch-indicator"></span>'; twitchBtn.onclick = StreamManager.switchToTwitch; // Auto button const autoBtn = document.createElement('button'); autoBtn.className = 'ttk-button ttk-auto-button'; autoBtn.innerHTML = '<span class="ttk-icon">🔄</span> Auto Switch <span class="ttk-indicator" id="ttk-auto-indicator"></span>'; autoBtn.onclick = () => { Storage.setCurrentMode('auto'); UI.updateModeIndicator('auto'); StreamManager.autoSwitch(); }; container.appendChild(twitchBtn); container.appendChild(kickBtn); container.appendChild(autoBtn); document.body.appendChild(container); UI.updateModeIndicator(Storage.getCurrentMode()); }, updateModeIndicator: (mode) => { const twitchIndicator = document.getElementById('ttk-twitch-indicator'); const kickIndicator = document.getElementById('ttk-kick-indicator'); const autoIndicator = document.getElementById('ttk-auto-indicator'); if (twitchIndicator) twitchIndicator.classList.toggle('active', mode === 'twitch'); if (kickIndicator) kickIndicator.classList.toggle('active', mode === 'kick'); if (autoIndicator) autoIndicator.classList.toggle('active', mode === 'auto'); }, createToggles: () => { // Collapse toggle button const toggleBtn = document.createElement('button'); toggleBtn.className = 'ttk-toggle'; toggleBtn.id = 'ttk-toggle'; toggleBtn.innerText = Storage.isCollapsed() ? '❯' : '❮'; toggleBtn.onclick = () => { const container = document.getElementById('ttk-container'); const isCollapsed = container.classList.contains('ttk-collapsed'); container.classList.toggle('ttk-collapsed', !isCollapsed); toggleBtn.innerText = isCollapsed ? '❮' : '❯'; Storage.setCollapsed(!isCollapsed); }; document.body.appendChild(toggleBtn); // Settings button const settingsBtn = document.createElement('button'); settingsBtn.className = 'ttk-settings-button'; settingsBtn.innerText = '⚙️'; settingsBtn.onclick = () => { const panel = document.getElementById('ttk-settings-panel'); const isVisible = panel.style.display === 'block'; panel.style.display = isVisible ? 'none' : 'block'; Storage.setSettingsVisible(!isVisible); if (!isVisible) { UI.refreshPairsList(); } }; document.body.appendChild(settingsBtn); }, createSettingsPanel: () => { const panel = document.createElement('div'); panel.id = 'ttk-settings-panel'; panel.className = 'ttk-settings-panel'; if (Storage.isSettingsVisible()) panel.style.display = 'block'; // Header const headerHTML = ` <div class="ttk-settings-header"> <h3 class="ttk-settings-title">TTK Stream Switcher Settings</h3> <button class="ttk-close-button" id="ttk-close-settings">×</button> </div> `; // Auto-switch toggle const autoSwitchHTML = ` <div class="ttk-switch-container"> <span class="ttk-switch-label">Enable auto-switching</span> <label class="ttk-switch"> <input type="checkbox" id="ttk-auto-switch-toggle" ${Storage.isAutoSwitchEnabled() ? 'checked' : ''}> <span class="ttk-slider"></span> </label> </div> `; // Pairs list container const pairsListHTML = ` <div id="ttk-pairs-container"></div> `; // Action buttons const buttonsHTML = ` <div class="ttk-button-row"> <button id="ttk-add-pair" class="ttk-action-button ttk-primary-button"> <span>+</span> Add Pair </button> <button id="ttk-save-all" class="ttk-action-button ttk-secondary-button"> <span>💾</span> Save All </button> </div> `; panel.innerHTML = headerHTML + autoSwitchHTML + pairsListHTML + buttonsHTML; document.body.appendChild(panel); // Event listeners document.getElementById('ttk-close-settings').onclick = () => { panel.style.display = 'none'; Storage.setSettingsVisible(false); }; document.getElementById('ttk-auto-switch-toggle').onchange = (e) => { Storage.setAutoSwitchEnabled(e.target.checked); }; document.getElementById('ttk-add-pair').onclick = () => { const pairs = Storage.getPairs(); pairs.push({ twitch: '', kick: '' }); Storage.savePairs(pairs); UI.refreshPairsList(); }; document.getElementById('ttk-save-all').onclick = () => { UI.savePairs(); }; UI.refreshPairsList(); }, refreshPairsList: () => { const container = document.getElementById('ttk-pairs-container'); if (!container) return; container.innerHTML = ''; const pairs = Storage.getPairs(); if (pairs.length === 0) { container.innerHTML = ` <div style="text-align: center; padding: 20px; color: #a0a0a5;"> No streamer pairs added yet. Click "Add Pair" to get started. </div> `; return; } pairs.forEach((pair, index) => { const pairElement = document.createElement('div'); pairElement.className = 'ttk-pair'; pairElement.innerHTML = ` <div class="ttk-pair-header"> <span class="ttk-pair-number">Pair #${index + 1}</span> <button class="ttk-delete-pair" data-index="${index}">Delete</button> </div> <div class="ttk-input-group"> <label class="ttk-input-label">Twitch Username</label> <input type="text" class="ttk-input ttk-twitch-input" data-index="${index}" value="${pair.twitch || ''}"> </div> <div class="ttk-input-group"> <label class="ttk-input-label">Kick Username</label> <input type="text" class="ttk-input ttk-kick-input" data-index="${index}" value="${pair.kick || ''}"> </div> `; container.appendChild(pairElement); }); // Add event listeners to delete buttons document.querySelectorAll('.ttk-delete-pair').forEach(btn => { btn.onclick = (e) => { const index = parseInt(e.target.getAttribute('data-index')); const pairs = Storage.getPairs(); pairs.splice(index, 1); Storage.savePairs(pairs); UI.refreshPairsList(); }; }); }, savePairs: () => { const twitchInputs = document.querySelectorAll('.ttk-twitch-input'); const kickInputs = document.querySelectorAll('.ttk-kick-input'); const pairs = []; for (let i = 0; i < twitchInputs.length; i++) { const twitch = twitchInputs[i].value.trim(); const kick = kickInputs[i].value.trim(); if (twitch || kick) { pairs.push({ twitch, kick }); } } Storage.savePairs(pairs); UI.showNotification('Settings saved successfully!'); }, showNotification: (message, duration = 3000) => { // Remove any existing notification const existingNotification = document.querySelector('.ttk-notification'); if (existingNotification) { existingNotification.remove(); } const notification = document.createElement('div'); notification.className = 'ttk-notification'; notification.textContent = message; document.body.appendChild(notification); // Force reflow before adding the show class notification.offsetHeight; notification.classList.add('show'); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => notification.remove(), 300); }, duration); } }; // =================== INITIALIZATION =================== function initialize() { UI.init(); // Set up auto-switch monitoring setInterval(() => { const currentChannel = StreamManager.getCurrentChannel(); if (currentChannel) { StreamManager.autoSwitch(); } }, CONFIG.checkInterval); // Initial check setTimeout(() => { StreamManager.autoSwitch(); }, CONFIG.initDelay); } // Execute when DOM is fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();