Twitch Sidebar Thumbnail Preview

Hover over Channel in the Sidebar to see a Thumbnail Preview of the Stream on Twitch

  1. // ==UserScript==
  2. // @name Twitch Sidebar Thumbnail Preview
  3. // @name:de Twitch Seitenleiste Vorschaubild
  4. // @version 1.0.2
  5. // @description Hover over Channel in the Sidebar to see a Thumbnail Preview of the Stream on Twitch
  6. // @description:de Bewege den Mauszeiger über einen Kanal in der Seitenleiste, um ein Vorschaubild des Streams zu sehen auf Twitch
  7. // @icon https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png
  8. // @author TalkLounge (https://github.com/TalkLounge)
  9. // @namespace https://github.com/TalkLounge/twitch-sidebar-preview
  10. // @license MIT
  11. // @match https://www.twitch.tv/*
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. let cache = {}, eventCount = 0;
  17.  
  18. function newElement(tagName, attributes, content) {
  19. var tag = document.createElement(tagName);
  20. for (var key in attributes || {}) {
  21. if (attributes[key] !== undefined && attributes[key] !== null) {
  22. tag.setAttribute(key, attributes[key]);
  23. }
  24. }
  25. tag.innerHTML = content || "";
  26. return tag;
  27. }
  28.  
  29. async function addThumbnail(element, eventCountLocal) {
  30. if (element.querySelector(".side-nav-card__avatar--offline")) { // Channel is offline
  31. return;
  32. }
  33.  
  34. let dialog, timeoutCount = 0;
  35. do { // Wait until Popup is ready
  36. await new Promise(r => setTimeout(r, 10));
  37. timeoutCount++;
  38.  
  39. if (timeoutCount > 50 || eventCountLocal != eventCount) { // Dialog timeout or newer mouseenter event called
  40. return;
  41. }
  42.  
  43. dialog = document.querySelector(".tw-dialog-layer:has(.hidden-focusable-elem) p");
  44. } while (!dialog);
  45.  
  46. dialog.parentNode.style.width = "440px";
  47. dialog.parentNode.querySelector("img")?.remove();
  48. const channel = element.querySelector("[title]").textContent.toLowerCase();
  49. if (!cache[channel] || Date.now() - cache[channel] >= 30 * 1000) { // Cache Thumbnails for half minute
  50. cache[channel] = Date.now();
  51. }
  52. const img = newElement("img", { src: `https://static-cdn.jtvnw.net/previews-ttv/live_user_${channel}-440x248.jpg?t=${cache[channel]}` });
  53. dialog.parentNode.append(img);
  54. }
  55.  
  56. function addHoverEvent(element) {
  57. if ([...element.classList].includes("tsp")) { // Already added
  58. return;
  59. }
  60.  
  61. element.classList.add("tsp");
  62. element.addEventListener("mouseenter", () => {
  63. eventCount++;
  64. addThumbnail(element, eventCount);
  65. });
  66. }
  67.  
  68. function init() {
  69. const uls = document.querySelectorAll("nav .tw-transition-group");
  70. if (!uls.length || !uls[0].children.length) { // Page not ready
  71. return;
  72. }
  73.  
  74. if (interval) {
  75. clearInterval(interval);
  76. interval = undefined;
  77. window.setInterval(init, 5000);
  78. }
  79.  
  80. for (let i = 0; i < uls.length; i++) {
  81. if ([...uls[i].classList].includes("tsp")) { // Already observing channel list
  82. continue;
  83. }
  84. uls[i].classList.add("tsp");
  85.  
  86. for (let j = 0; j < uls[i].children.length; j++) {
  87. addHoverEvent(uls[i].children[j]);
  88. }
  89.  
  90. const observer = new MutationObserver((mutationList) => { // Check for new channels in channel list added by click on show more
  91. for (const mutation of mutationList) {
  92. for (let j = 0; j < mutation.addedNodes.length; j++) {
  93. addHoverEvent(mutation.addedNodes[j]);
  94. }
  95. }
  96. });
  97. observer.observe(uls[i], { childList: true });
  98. }
  99. }
  100.  
  101. let interval = window.setInterval(init, 500);
  102. })();