YouTube Channel Hover Popup

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

当前为 2024-05-08 提交的版本,查看 最新版本

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