// ==UserScript==
// @name Switcher Stream Channel 1.10.16
// @namespace http://tampermonkey.net/
// @version 1.10.16
// @description Replace video feed with specified channel's video stream and provide draggable control panel functionality
// @author Gullampis810
// @license MIT
// @match https://www.twitch.tv/*
// @icon https://github.com/sopernik566/icons/blob/main/switcher%20player%20icon.png?raw=true
// @run-at document-end
// ==/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') || '#333',
buttonColor: localStorage.getItem('buttonColor') || '#2b675f',
panelPosition: JSON.parse(localStorage.getItem('panelPosition')) || { top: '8px', left: '10px' },
isPanelHidden: false
};
// Сортировка массивов по алфавиту при инициализации
state.favoriteChannels.sort((a, b) => a.localeCompare(b));
state.channelHistory.sort((a, b) => a.localeCompare(b));
// Инициализация UI
const panel = createControlPanel();
const toggleButton = createToggleButton();
document.body.appendChild(panel);
document.body.appendChild(toggleButton);
// Настройка функционала
setPanelPosition(panel, state.panelPosition);
enableDrag(panel);
window.addEventListener('load', loadStream);
// Создание панели управления
function createControlPanel() {
const panel = document.createElement('div');
panel.className = 'switcher-panel';
Object.assign(panel.style, {
position: 'fixed',
width: '322px',
padding: '10px',
backgroundColor: state.panelColor,
color: '#fff',
border: '2px solid #ffffff',
borderRadius: '5px',
zIndex: '9999',
transition: 'opacity 0.3s ease, visibility 0s linear 0.3s',
cursor: 'move'
});
const content = document.createElement('div');
content.className = 'panel-content';
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
const title = createTitle('Switcher Channel');
const hideBtn = document.createElement('button');
hideBtn.textContent = '×';
hideBtn.style.cssText = `
background: none;
border: none;
color: #fff;
font-size: 20px;
cursor: pointer;
padding: 0 10px;
`;
hideBtn.addEventListener('click', togglePanel);
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 from Favorites', removeChannelFromFavorites, 'remove-fav-btn'),
createButton('Clear History', clearHistory, 'clear-history-btn'),
createColorPicker('Panel Color:', 'panel-color-picker', updatePanelColor),
createColorPicker('Button Color:', 'button-color-picker', updateButtonColor)
);
panel.append(header, content);
return panel;
}
// Кнопка переключения видимости с подсказкой
function createToggleButton() {
const button = document.createElement('button');
button.className = 'toggle-visibility';
Object.assign(button.style, {
position: 'fixed',
top: '9px',
left: '28%',
width: '44px',
height: '39px',
backgroundColor: state.buttonColor,
borderRadius: '10%',
border: 'none',
cursor: 'pointer',
zIndex: '10000',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
});
const img = document.createElement('img');
img.src = 'https://raw.githubusercontent.com/sopernik566/icons/4986e623628f56c95bd45004d6794820d874266d/eye_show.svg';
img.alt = 'Toggle visibility';
img.style.cssText = 'width: 25px; height: 25px;';
button.appendChild(img);
// Создаем элемент для подсказки
const tooltip = document.createElement('span');
tooltip.textContent = 'Show panel Switcher with channels ';
tooltip.style.cssText = `
visibility: hidden;
background-color: #ededed ;
color: #0c0e0e;
text-align: center;
border-radius: 5px;
padding: 5px;
position: absolute;
z-index: 10001;
top: 50px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
font-size: 12px;
`;
button.appendChild(tooltip);
// Показываем/скрываем подсказку при наведении
button.addEventListener('mouseover', () => {
tooltip.style.visibility = 'visible';
button.style.backgroundColor = '#3aa39b';
});
button.addEventListener('mouseout', () => {
tooltip.style.visibility = 'hidden';
button.style.backgroundColor = '#2b675f';
});
button.addEventListener('click', togglePanelVisibility);
return button;
}
// Элементы интерфейса
function createTitle(text) {
const title = document.createElement('h3');
title.textContent = text;
title.style.margin = '0';
return title;
}
function createChannelInput() {
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'type channel name';
input.style.cssText = `
width: 100%;
margin: 5px 0;
padding: 8px;
border-radius: 4px;
border: 1px solid #fff;
background-color: #1a3c34;
color: #fff;
`;
input.addEventListener('input', (e) => state.channelName = e.target.value.trim());
return input;
}
function createSelect(options, labelText, className) {
const container = document.createElement('div');
container.style.margin = '5px 0';
const label = document.createElement('label');
label.textContent = labelText;
label.style.display = 'block';
label.style.marginBottom = '2px';
const select = document.createElement('select');
select.className = className;
select.style.cssText = `
width: 100%;
padding: 8px;
border-radius: 4px;
background-color: #1a3c34;
color: #fff;
border: 1px solid #fff;
`;
updateOptions(select, options);
select.addEventListener('change', () => state.channelName = select.value);
container.append(label, select);
return container;
}
function createButton(text, onClick, className) {
const button = document.createElement('button');
button.textContent = text;
button.className = className;
button.style.cssText = `
width: 100%;
margin: 5px 0;
padding: 10px;
background-color: ${state.buttonColor};
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.2s ease;
`;
button.addEventListener('click', onClick);
button.addEventListener('mouseover', () => {
button.style.backgroundColor = lightenColor(state.buttonColor, 20);
button.style.boxShadow = '0 4px 6px rgba(0,0,0,0.2)';
});
button.addEventListener('mouseout', () => {
button.style.backgroundColor = state.buttonColor;
button.style.boxShadow = 'none';
});
return button;
}
function createColorPicker(labelText, className, onChange) {
const container = document.createElement('div');
container.style.margin = '5px 0';
const label = document.createElement('label');
label.textContent = labelText;
label.style.display = 'block';
label.style.marginBottom = '2px';
const picker = document.createElement('input');
picker.type = 'color';
picker.className = className;
picker.value = labelText.includes('Panel') ? state.panelColor : state.buttonColor;
picker.style.width = '100%';
picker.addEventListener('input', onChange);
container.append(label, picker);
return container;
}
// Функции обновления цветов
function updatePanelColor(e) {
state.panelColor = e.target.value;
panel.style.backgroundColor = state.panelColor;
localStorage.setItem('panelColor', state.panelColor);
}
function updateButtonColor(e) {
state.buttonColor = e.target.value;
localStorage.setItem('buttonColor', state.buttonColor);
updatePanelButtonsColor(); // Обновляем только кнопки внутри панели
}
function updatePanelButtonsColor() {
// Обновляем цвет только для кнопок внутри панели
panel.querySelectorAll('button').forEach(button => {
button.style.backgroundColor = state.buttonColor;
});
}
// Вспомогательная функция для осветления цвета
function lightenColor(hex, percent) {
const num = parseInt(hex.replace('#', ''), 16);
const r = Math.min(255, (num >> 16) + (255 * percent / 100));
const g = Math.min(255, ((num >> 8) & 0x00FF) + (255 * percent / 100));
const b = Math.min(255, (num & 0x0000FF) + (255 * percent / 100));
return `#${Math.round(r).toString(16).padStart(2, '0')}${Math.round(g).toString(16).padStart(2, '0')}${Math.round(b).toString(16).padStart(2, '0')}`;
}
// Функциональность
function togglePanel() {
state.isPanelHidden = !state.isPanelHidden;
panel.style.opacity = state.isPanelHidden ? '0' : '1';
panel.style.visibility = state.isPanelHidden ? 'hidden' : 'visible';
panel.style.transition = state.isPanelHidden
? 'opacity 0.3s ease, visibility 0s linear 0.3s'
: 'opacity 0.3s ease, visibility 0s linear 0s';
}
function togglePanelVisibility() {
const img = toggleButton.querySelector('img');
const isHidden = panel.style.visibility === 'hidden' || panel.style.display === 'none';
if (isHidden) {
panel.style.display = 'block';
panel.style.opacity = '1';
panel.style.visibility = 'visible';
state.isPanelHidden = false;
img.src = 'https://raw.githubusercontent.com/sopernik566/icons/4986e623628f56c95bd45004d6794820d874266d/eye_show.svg';
} else {
panel.style.opacity = '0';
panel.style.visibility = 'hidden';
state.isPanelHidden = true;
img.src = 'https://raw.githubusercontent.com/sopernik566/icons/4986e623628f56c95bd45004d6794820d874266d/eye_hidden_1024.svg';
}
}
function loadInputChannel() {
if (state.channelName) {
loadStream();
addChannelToHistory(state.channelName);
} else {
alert('Type a channel name.');
}
}
function loadSelectedChannel() {
if (state.channelName) {
loadStream();
addChannelToHistory(state.channelName);
} else {
alert('Select a channel.');
}
}
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(`Added ${state.channelName} to favorites!`);
updateOptions(document.querySelector('.favorites-select'), state.favoriteChannels);
} else if (!state.channelName) {
alert('Please enter a channel name to add to favorites.');
}
}
function removeChannelFromFavorites() {
if (!state.channelName) {
alert('Please select a channel to remove from favorites.');
return;
}
if (!state.favoriteChannels.includes(state.channelName)) {
alert(`${state.channelName} is not in your favorites.`);
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 loadStream() {
setTimeout(() => {
const player = document.querySelector('.video-player__container');
if (player) {
player.innerHTML = '';
const iframe = document.createElement('iframe');
iframe.src = `https://player.twitch.tv/?channel=${state.channelName}&parent=twitch.tv&quality=1080p&muted=false`;
iframe.style.cssText = 'width: 100%; height: 100%;';
iframe.allowFullscreen = true;
player.appendChild(iframe);
}
}, 2000);
}
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) {
select.innerHTML = '';
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.text = option;
select.appendChild(opt);
});
}
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 = 'opacity 0.3s ease, visibility 0s linear 0.3s';
});
}
function setPanelPosition(element, position) {
element.style.top = position.top;
element.style.left = position.left;
}
})();