YouTube Channel Hover Popup

Display a hover popup with channel info on YouTube after dynamic content load, with immediate loading indicator

  1. // ==UserScript==
  2. // @name YouTube Channel Hover Popup
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description Display a hover popup with channel info on YouTube after dynamic content load, with immediate loading indicator
  6. // @author @dmtri
  7. // @match https://www.youtube.com/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14. let policy = null
  15.  
  16. const createPopup = () => {
  17. const popup = document.createElement('div');
  18. popup.style.position = 'fixed';
  19. popup.style.zIndex = '1000';
  20. popup.style.width = '300px';
  21. popup.style.background = 'white';
  22. popup.style.border = '1px solid black';
  23. popup.style.borderRadius = '8px';
  24. popup.style.padding = '16px';
  25. popup.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
  26. popup.style.display = 'none';
  27. popup.style.fontSize = '16px';
  28. document.body.appendChild(popup);
  29. return popup;
  30. };
  31.  
  32. const popup = createPopup();
  33.  
  34. const showLoadingPopup = (popup, x, y) => {
  35. policy = trustedTypes.createPolicy('default2', {
  36. createHTML: (string) => string, // Allow all HTML strings
  37. });
  38.  
  39. popup.innerHTML = policy.createHTML('<strong>Loading...</strong>');
  40. popup.style.left = `${x}px`;
  41. popup.style.top = `${y}px`;
  42. popup.style.display = 'block';
  43. };
  44.  
  45. const updatePopupContent = (popup, content) => {
  46. popup.innerHTML = policy.createHTML(content);
  47. const closeButton = document.createElement('button');
  48. closeButton.textContent = 'X';
  49. closeButton.style.position = 'absolute';
  50. closeButton.style.top = '5px';
  51. closeButton.style.right = '10px';
  52. closeButton.style.border = 'none';
  53. closeButton.style.background = 'none';
  54. closeButton.style.cursor = 'pointer';
  55. closeButton.style.color = '#333';
  56. closeButton.style.fontSize = '16px';
  57. closeButton.style.fontWeight = 'bold';
  58. closeButton.onclick = () => popup.style.display = 'none';
  59. popup.appendChild(closeButton);
  60. };
  61.  
  62. const fetchChannelInfo = async (url) => {
  63. try {
  64. const response = await fetch(url);
  65. const html = await response.text();
  66. const parser = new DOMParser();
  67. const doc = parser.parseFromString(policy.createHTML(html), 'text/html');
  68. const meta = doc.querySelector('meta[property="og:description"]');
  69. const description = meta ? meta.getAttribute('content') : 'No description available.';
  70. return `<strong>Description:</strong> ${description}<br>`;
  71. } catch (error) {
  72. return 'Failed to load description.';
  73. }
  74. };
  75.  
  76. const throttle = (func, limit) => {
  77. let lastFunc;
  78. let lastRan;
  79. return function() {
  80. const context = this;
  81. const args = arguments;
  82. if (!lastRan) {
  83. func.apply(context, args);
  84. lastRan = Date.now();
  85. } else {
  86. clearTimeout(lastFunc);
  87. lastFunc = setTimeout(function() {
  88. if ((Date.now() - lastRan) >= limit) {
  89. func.apply(context, args);
  90. lastRan = Date.now();
  91. }
  92. }, limit - (Date.now() - lastRan));
  93. }
  94. };
  95. };
  96.  
  97. const observeDOM = () => {
  98. const observer = new MutationObserver((mutations, obs) => {
  99. setTimeout(() => {
  100. const channelElements = document.querySelectorAll('.ytd-channel-name#text-container');
  101. if (channelElements.length) {
  102. init(channelElements);
  103. obs.disconnect(); // Stop observing after successful initialization
  104. }
  105. }, 1000);
  106. });
  107. observer.observe(document.body, {
  108. childList: true,
  109. subtree: true
  110. });
  111. console.log('[YouTube Channel Hover Popup] - observeDOM')
  112. };
  113.  
  114. const init = (channelElements) => {
  115. console.log('YouTube Channel Hover Popup - init', { channelElements } )
  116. channelElements.forEach(channelElement => {
  117. let popupTimeout;
  118.  
  119. channelElement.addEventListener('mouseenter', async (e) => {
  120. clearTimeout(popupTimeout);
  121. popupTimeout = setTimeout(async () => {
  122. const url = channelElement.querySelector('a').href;
  123. showLoadingPopup(popup, e.clientX, e.clientY + 20);
  124. const content = await fetchChannelInfo(url);
  125. updatePopupContent(popup, content);
  126. }, 500);
  127. });
  128.  
  129. channelElement.addEventListener('mouseleave', () => {
  130. clearTimeout(popupTimeout);
  131. popup.style.display = 'none';
  132. });
  133.  
  134. const throttledMouseMove = throttle((e) => {
  135. if (popup.style.display !== 'none') {
  136. popup.style.left = `${e.clientX}px`;
  137. popup.style.top = `${e.clientY + 20}px`;
  138. }
  139. }, 100); // Update popup position at most every 100ms
  140.  
  141. channelElement.addEventListener('mousemove', throttledMouseMove);
  142. });
  143. };
  144.  
  145. setTimeout(() => {
  146. const channelElements = document.querySelectorAll('.ytd-channel-name#text-container');
  147. init(channelElements)
  148. }, 5000); // Start observing DOM for changes
  149. setTimeout(() => {
  150. observeDom()
  151. }, 15000); // Start observing DOM for changes
  152. })();