Switcher Stream Channel 1.10.17

Replace video feed with specified channel's video stream and provide draggable control panel functionality

  1. // ==UserScript==
  2. // @name Switcher Stream Channel 1.10.17
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.10.17
  5. // @license MIT
  6. // @description Replace video feed with specified channel's video stream and provide draggable control panel functionality
  7. // @author Gullampis810
  8. // @match https://www.twitch.tv/*
  9. // @icon https://github.com/sopernik566/icons/blob/main/switcher%20player%20icon.png?raw=true
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const state = {
  17. channelName: 'tapa_tapa_mateo',
  18. favoriteChannels: JSON.parse(localStorage.getItem('favoriteChannels')) || [],
  19. channelHistory: JSON.parse(localStorage.getItem('channelHistory')) || [],
  20. panelColor: localStorage.getItem('panelColor') || 'rgba(255, 255, 255, 0.15)',
  21. buttonColor: localStorage.getItem('buttonColor') || 'rgba(255, 255, 255, 0.3)',
  22. panelPosition: JSON.parse(localStorage.getItem('panelPosition')) || { top: '20px', left: '20px' },
  23. isPanelHidden: false
  24. };
  25.  
  26. state.favoriteChannels.sort((a, b) => a.localeCompare(b));
  27. state.channelHistory.sort((a, b) => a.localeCompare(b));
  28.  
  29. const panel = createControlPanel();
  30. const toggleButton = createToggleButton();
  31. document.body.appendChild(panel);
  32. document.body.appendChild(toggleButton);
  33.  
  34. setPanelPosition(panel, state.panelPosition);
  35. enableDrag(panel);
  36. window.addEventListener('load', loadStream);
  37.  
  38. function createControlPanel() {
  39. const panel = document.createElement('div');
  40. panel.className = 'switcher-panel';
  41. Object.assign(panel.style, {
  42. position: 'fixed',
  43. width: '340px',
  44. padding: '20px',
  45. backgroundColor: state.panelColor,
  46. backdropFilter: 'blur(10px)',
  47. borderRadius: '20px',
  48. border: '1px solid rgba(255, 255, 255, 0.2)',
  49. boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
  50. zIndex: '9999',
  51. transition: 'transform 0.3s ease, opacity 0.3s ease',
  52. cursor: 'move'
  53. });
  54.  
  55. const content = document.createElement('div');
  56.  
  57. const header = document.createElement('div');
  58. Object.assign(header.style, {
  59. display: 'flex',
  60. justifyContent: 'space-between',
  61. alignItems: 'center',
  62. marginBottom: '20px',
  63. });
  64.  
  65. const title = createTitle('Channel Switcher v1.10.17');
  66. const hideBtn = document.createElement('button');
  67. hideBtn.textContent = '×';
  68. Object.assign(hideBtn.style, {
  69. width: '40px',
  70. height: '40px',
  71. border: 'none',
  72. borderRadius: '50%',
  73. fontSize: '24px',
  74. color: 'rgba(255, 255, 255, 0.8)',
  75. cursor: 'pointer',
  76. backdropFilter: 'blur(10px)',
  77. boxShadow: '0 4px 15px rgba(0, 0, 0, 0.1)',
  78. transition: 'all 0.2s ease',
  79. display: 'flex',
  80. justifyContent: 'center'
  81. });
  82. hideBtn.addEventListener('click', togglePanel);
  83. hideBtn.addEventListener('mouseover', () => hideBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.3)');
  84. hideBtn.addEventListener('mouseout', () => hideBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.2)');
  85.  
  86. header.append(title, hideBtn);
  87.  
  88. content.append(
  89. createChannelInput(),
  90. createButton('Play Channel', loadInputChannel, 'play-btn'),
  91. createSelect(state.favoriteChannels, 'Favorites', 'favorites-select'),
  92. createSelect(state.channelHistory, 'History', 'history-select'),
  93. createButton('Play Selected', loadSelectedChannel, 'play-selected-btn'),
  94. createButton('Add to Favorites', addChannelToFavorites, 'add-fav-btn'),
  95. createButton('Remove Favorite', removeChannelFromFavorites, 'remove-fav-btn'),
  96. createButton('Clear History', clearHistory, 'clear-history-btn'),
  97. createColorPicker('Panel Color', 'panel-color-picker', updatePanelColor),
  98. createColorPicker('Button Color', 'button-color-picker', updateButtonColor)
  99. );
  100.  
  101. panel.append(header, content);
  102. return panel;
  103. }
  104.  
  105. function createToggleButton() {
  106. const button = document.createElement('button');
  107. button.className = 'toggle-visibility';
  108. Object.assign(button.style, {
  109. position: 'fixed',
  110. top: '16px',
  111. left: '490px',
  112. width: '40px',
  113. height: '40px',
  114. backgroundColor: ' #8b008b00;',
  115. borderRadius: '50%',
  116. border: '1px solid rgba(255, 255, 255, 0.2)',
  117. cursor: 'pointer',
  118. zIndex: '10000',
  119. display: 'flex',
  120. alignItems: 'center',
  121. justifyContent: 'center',
  122. backdropFilter: 'blur(10px)',
  123. boxShadow: '0 4px 15px rgba(0, 0, 0, 0.1)',
  124. transition: 'all 0.2s ease'
  125. });
  126.  
  127. const img = document.createElement('img');
  128. img.src = 'https://raw.githubusercontent.com/sopernik566/icons/4986e623628f56c95bd45004d6794820d874266d/eye_show.svg';
  129. img.alt = 'Toggle visibility';
  130. img.style.cssText = 'width: 28px; height: 28px; filter: brightness(100);';
  131.  
  132. button.appendChild(img);
  133. button.addEventListener('click', togglePanelVisibility);
  134. button.addEventListener('mouseover', () => button.style.backgroundColor = 'rgba(255, 255, 255, 0.3)');
  135. button.addEventListener('mouseout', () => button.style.backgroundColor = 'rgba(255, 255, 255, 0.2)');
  136.  
  137. return button;
  138. }
  139.  
  140. function createTitle(text) {
  141. const title = document.createElement('h3');
  142. title.textContent = text;
  143. Object.assign(title.style, {
  144. margin: '0',
  145. fontSize: '20px',
  146. fontWeight: '500',
  147. color: 'rgba(255, 255, 255, 0.9)'
  148. });
  149. return title;
  150. }
  151.  
  152. function createChannelInput() {
  153. const input = document.createElement('input');
  154. input.type = 'text';
  155. input.placeholder = 'Enter channel name';
  156. Object.assign(input.style, {
  157. width: '100%',
  158. marginBottom: '16px',
  159. padding: '12px 16px',
  160. borderRadius: '12px',
  161. border: '1px solid rgba(255, 255, 255, 0.2)',
  162. backgroundColor: 'rgba(255, 255, 255, 0.1)',
  163. color: 'rgba(255, 255, 255, 0.9)',
  164. fontSize: '16px',
  165. backdropFilter: 'blur(10px)',
  166. boxShadow: '0 4px 15px rgba(0, 0, 0, 0.05)',
  167. transition: 'all 0.2s ease'
  168. });
  169. input.addEventListener('input', (e) => state.channelName = e.target.value.trim());
  170. input.addEventListener('focus', () => input.style.borderColor = 'rgba(255, 255, 255, 0.4)');
  171. input.addEventListener('blur', () => input.style.borderColor = 'rgba(255, 255, 255, 0.2)');
  172. return input;
  173. }
  174.  
  175. function createSelect(options, labelText, className) {
  176. const container = document.createElement('div');
  177. container.style.marginBottom = '16px';
  178. container.style.position = 'relative';
  179.  
  180. const label = document.createElement('label');
  181. label.textContent = labelText;
  182. Object.assign(label.style, {
  183. display: 'block',
  184. marginBottom: '4px',
  185. fontSize: '12px',
  186. color: 'rgba(255, 255, 255, 0.7)',
  187. fontWeight: '500'
  188. });
  189.  
  190. const selectBox = document.createElement('div');
  191. Object.assign(selectBox.style, {
  192. width: '100%',
  193. padding: '12px 16px',
  194. borderRadius: '12px',
  195. backgroundColor: 'rgba(255, 255, 255, 0.05)',
  196. color: 'rgba(255, 255, 255, 0.9)',
  197. border: '1px solid rgba(255, 255, 255, 0.2)',
  198. fontSize: '16px',
  199. backdropFilter: 'blur(15px)',
  200. boxShadow: '0 4px 15px rgba(0, 0, 0, 0.05)',
  201. cursor: 'pointer',
  202. display: 'flex',
  203. justifyContent: 'space-between',
  204. alignItems: 'center'
  205. });
  206.  
  207. const selectedText = document.createElement('span');
  208. selectedText.textContent = options.length ? options[0] : 'No items';
  209.  
  210. const arrow = document.createElement('span');
  211. 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>';
  212.  
  213. const dropdown = document.createElement('div');
  214. Object.assign(dropdown.style, {
  215. position: 'absolute',
  216. top: '100%',
  217. left: '0',
  218. width: '100%',
  219. maxHeight: '200px',
  220. overflowY: 'auto',
  221. backgroundColor: 'rgba(255, 255, 255, 0.1)',
  222. borderRadius: '12px',
  223. border: '1px solid rgba(255, 255, 255, 0.2)',
  224. backdropFilter: 'blur(15px)',
  225. boxShadow: '0 4px 15px rgba(0, 0, 0, 0.1)',
  226. display: 'none',
  227. zIndex: '10001'
  228. });
  229.  
  230. options.forEach(option => {
  231. const item = document.createElement('div');
  232. Object.assign(item.style, {
  233. padding: '10px 16px',
  234. color: 'rgba(255, 255, 255, 0.9)',
  235. cursor: 'pointer',
  236. transition: 'background-color 0.2s ease'
  237. });
  238. item.textContent = option;
  239. item.addEventListener('click', () => {
  240. state.channelName = option;
  241. selectedText.textContent = option;
  242. dropdown.style.display = 'none';
  243. });
  244. item.addEventListener('mouseover', () => item.style.backgroundColor = 'rgba(255, 255, 255, 0.2)');
  245. item.addEventListener('mouseout', () => item.style.backgroundColor = 'transparent');
  246. dropdown.appendChild(item);
  247. });
  248.  
  249. selectBox.append(selectedText, arrow);
  250. container.append(label, selectBox, dropdown);
  251.  
  252. selectBox.addEventListener('click', () => {
  253. dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
  254. });
  255.  
  256. document.addEventListener('click', (e) => {
  257. if (!container.contains(e.target)) {
  258. dropdown.style.display = 'none';
  259. }
  260. });
  261.  
  262. return container;
  263. }
  264.  
  265. function createButton(text, onClick, className) {
  266. const button = document.createElement('button');
  267. button.textContent = text;
  268. button.className = className;
  269. Object.assign(button.style, {
  270. width: '100%',
  271. marginBottom: '12px',
  272. padding: '14px 16px',
  273. backgroundColor: state.buttonColor,
  274. color: 'rgba(255, 255, 255, 0.9)',
  275. border: '1px solid rgba(255, 255, 255, 0.2)',
  276. borderRadius: '12px',
  277. fontSize: '16px',
  278. fontWeight: '500',
  279. cursor: 'pointer',
  280. backdropFilter: 'blur(10px)',
  281. boxShadow: '0 4px 15px rgba(0, 0, 0, 0.1)',
  282. transition: 'all 0.2s ease'
  283. });
  284. button.addEventListener('click', onClick);
  285. button.addEventListener('mouseover', () => button.style.backgroundColor = 'rgba(255, 255, 255, 0.4)');
  286. button.addEventListener('mouseout', () => button.style.backgroundColor = state.buttonColor);
  287. button.addEventListener('mousedown', () => button.style.transform = 'scale(0.98)');
  288. button.addEventListener('mouseup', () => button.style.transform = 'scale(1)');
  289. return button;
  290. }
  291.  
  292. function createColorPicker(labelText, className, onChange) {
  293. const container = document.createElement('div');
  294. container.style.marginBottom = '16px';
  295.  
  296. const label = document.createElement('label');
  297. label.textContent = labelText;
  298. Object.assign(label.style, {
  299. display: 'block',
  300. marginBottom: '4px',
  301. fontSize: '12px',
  302. color: 'rgba(255, 255, 255, 0.7)',
  303. fontWeight: '500'
  304. });
  305.  
  306. const picker = document.createElement('input');
  307. picker.type = 'color';
  308. picker.className = className;
  309. picker.value = labelText.includes('Panel') ? rgbaToHex(state.panelColor) : rgbaToHex(state.buttonColor);
  310. Object.assign(picker.style, {
  311. width: '100%',
  312. height: '48px',
  313. padding: '0',
  314. border: '1px solid rgba(255, 255, 255, 0.2)',
  315. borderRadius: '12px',
  316. cursor: 'pointer',
  317. backdropFilter: 'blur(10px)',
  318. boxShadow: '0 4px 15px rgba(0, 0, 0, 0.05)',
  319. transition: 'all 0.2s ease',
  320. background: '#8b008b00',
  321. });
  322. picker.addEventListener('input', onChange);
  323.  
  324. container.append(label, picker);
  325. return container;
  326. }
  327.  
  328. function rgbaToHex(rgba) {
  329. const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
  330. if (!match) return rgba;
  331. const r = parseInt(match[1]).toString(16).padStart(2, '0');
  332. const g = parseInt(match[2]).toString(16).padStart(2, '0');
  333. const b = parseInt(match[3]).toString(16).padStart(2, '0');
  334. return `#${r}${g}${b}`;
  335. }
  336.  
  337. function updatePanelColor(e) {
  338. const hex = e.target.value;
  339. state.panelColor = `${hex}33`; // Добавляем прозрачность (0.2 в hex = 33)
  340. panel.style.backgroundColor = state.panelColor;
  341. localStorage.setItem('panelColor', state.panelColor);
  342. }
  343.  
  344. function updateButtonColor(e) {
  345. const hex = e.target.value;
  346. state.buttonColor = `${hex}4D`; // Прозрачность 0.3 в hex = 4D
  347. localStorage.setItem('buttonColor', state.buttonColor);
  348. panel.querySelectorAll('button:not(.toggle-visibility)').forEach(button => {
  349. button.style.backgroundColor = state.buttonColor;
  350. });
  351. }
  352.  
  353. function togglePanel() {
  354. state.isPanelHidden = !state.isPanelHidden;
  355. panel.style.transform = state.isPanelHidden ? 'scale(0.95)' : 'scale(1)';
  356. panel.style.opacity = state.isPanelHidden ? '0' : '1';
  357. panel.style.pointerEvents = state.isPanelHidden ? 'none' : 'auto';
  358. }
  359.  
  360. function togglePanelVisibility() {
  361. const img = toggleButton.querySelector('img');
  362. const isHidden = panel.style.opacity === '0' || !panel.style.opacity;
  363.  
  364. if (isHidden) {
  365. panel.style.transform = 'scale(1)';
  366. panel.style.opacity = '1';
  367. panel.style.pointerEvents = 'auto';
  368. state.isPanelHidden = false;
  369. img.src = 'https://raw.githubusercontent.com/sopernik566/icons/4986e623628f56c95bd45004d6794820d874266d/eye_show.svg';
  370. } else {
  371. panel.style.transform = 'scale(0.95)';
  372. panel.style.opacity = '0';
  373. panel.style.pointerEvents = 'none';
  374. state.isPanelHidden = true;
  375. img.src = 'https://raw.githubusercontent.com/sopernik566/icons/4986e623628f56c95bd45004d6794820d874266d/eye_hidden_1024.svg';
  376. }
  377. }
  378.  
  379. function loadInputChannel() {
  380. if (state.channelName) {
  381. loadStream();
  382. addChannelToHistory(state.channelName);
  383. } else {
  384. alert('Please enter a channel name.');
  385. }
  386. }
  387.  
  388. function loadSelectedChannel() {
  389. if (state.channelName) {
  390. loadStream();
  391. addChannelToHistory(state.channelName);
  392. } else {
  393. alert('Please select a channel.');
  394. }
  395. }
  396.  
  397. function addChannelToFavorites() {
  398. if (state.channelName && !state.favoriteChannels.includes(state.channelName)) {
  399. state.favoriteChannels.push(state.channelName);
  400. state.favoriteChannels.sort((a, b) => a.localeCompare(b));
  401. localStorage.setItem('favoriteChannels', JSON.stringify(state.favoriteChannels));
  402. alert(`Added ${state.channelName} to favorites!`);
  403. updateOptions(document.querySelector('.favorites-select'), state.favoriteChannels);
  404. } else if (!state.channelName) {
  405. alert('Please enter a channel name.');
  406. }
  407. }
  408.  
  409. function removeChannelFromFavorites() {
  410. if (!state.channelName) {
  411. alert('Please select a channel to remove.');
  412. return;
  413. }
  414. if (!state.favoriteChannels.includes(state.channelName)) {
  415. alert(`${state.channelName} is not in favorites.`);
  416. return;
  417. }
  418. state.favoriteChannels = state.favoriteChannels.filter(ch => ch !== state.channelName);
  419. state.favoriteChannels.sort((a, b) => a.localeCompare(b));
  420. localStorage.setItem('favoriteChannels', JSON.stringify(state.favoriteChannels));
  421. updateOptions(document.querySelector('.favorites-select'), state.favoriteChannels);
  422. }
  423.  
  424. function clearHistory() {
  425. state.channelHistory = [];
  426. localStorage.setItem('channelHistory', JSON.stringify(state.channelHistory));
  427. updateOptions(document.querySelector('.history-select'), state.channelHistory);
  428. }
  429.  
  430. function loadStream() {
  431. setTimeout(() => {
  432. const player = document.querySelector('.video-player__container');
  433. if (player) {
  434. player.innerHTML = '';
  435. const iframe = document.createElement('iframe');
  436. iframe.src = `https://player.twitch.tv/?channel=${state.channelName}&parent=twitch.tv&quality=1080p&muted=false`;
  437. iframe.style.cssText = 'width: 100%; height: 100%; border-radius: 12px;';
  438. iframe.allowFullscreen = true;
  439. player.appendChild(iframe);
  440. }
  441. }, 2000);
  442. }
  443.  
  444. function addChannelToHistory(channel) {
  445. if (channel && !state.channelHistory.includes(channel)) {
  446. state.channelHistory.push(channel);
  447. state.channelHistory.sort((a, b) => a.localeCompare(b));
  448. localStorage.setItem('channelHistory', JSON.stringify(state.channelHistory));
  449. updateOptions(document.querySelector('.history-select'), state.channelHistory);
  450. }
  451. }
  452.  
  453. function updateOptions(select, options) {
  454. select.innerHTML = options.length ? '' : '<option>No items</option>';
  455. options.forEach(option => {
  456. const opt = document.createElement('option');
  457. opt.value = option;
  458. opt.text = option;
  459. select.appendChild(opt);
  460. });
  461. }
  462.  
  463. function enableDrag(element) {
  464. let isDragging = false, offsetX, offsetY;
  465. element.addEventListener('mousedown', (e) => {
  466. if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'SELECT') {
  467. isDragging = true;
  468. offsetX = e.clientX - element.getBoundingClientRect().left;
  469. offsetY = e.clientY - element.getBoundingClientRect().top;
  470. element.style.transition = 'none';
  471. }
  472. });
  473.  
  474. document.addEventListener('mousemove', (e) => {
  475. if (isDragging) {
  476. const newLeft = e.clientX - offsetX;
  477. const newTop = e.clientY - offsetY;
  478. element.style.left = `${newLeft}px`;
  479. element.style.top = `${newTop}px`;
  480. localStorage.setItem('panelPosition', JSON.stringify({ top: `${newTop}px`, left: `${newLeft}px` }));
  481. }
  482. });
  483.  
  484. document.addEventListener('mouseup', () => {
  485. isDragging = false;
  486. element.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
  487. });
  488. }
  489.  
  490. function setPanelPosition(element, position) {
  491. element.style.top = position.top;
  492. element.style.left = position.left;
  493. }
  494. })();