// ==UserScript==
// @name YouTube Channel Hover Popup
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Display a hover popup with channel info on YouTube after dynamic content load, with immediate loading indicator
// @author @dmtri
// @match https://www.youtube.com/*
// @grant none
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/474832/YouTube%20Channel%20Hover%20Popup.user.js
// @updateURL https://update.greasyfork.icu/scripts/474832/YouTube%20Channel%20Hover%20Popup.meta.js
// ==/UserScript==
(function() {
'use strict';
let policy = null
const createPopup = () => {
const popup = document.createElement('div');
popup.style.position = 'fixed';
popup.style.zIndex = '1000';
popup.style.width = '300px';
popup.style.background = 'white';
popup.style.border = '1px solid black';
popup.style.borderRadius = '8px';
popup.style.padding = '16px';
popup.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
popup.style.display = 'none';
popup.style.fontSize = '16px';
document.body.appendChild(popup);
return popup;
};
const popup = createPopup();
const showLoadingPopup = (popup, x, y) => {
policy = trustedTypes.createPolicy('default2', {
createHTML: (string) => string, // Allow all HTML strings
});
popup.innerHTML = policy.createHTML('Loading...');
popup.style.left = `${x}px`;
popup.style.top = `${y}px`;
popup.style.display = 'block';
};
const updatePopupContent = (popup, content) => {
popup.innerHTML = policy.createHTML(content);
const closeButton = document.createElement('button');
closeButton.textContent = 'X';
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '10px';
closeButton.style.border = 'none';
closeButton.style.background = 'none';
closeButton.style.cursor = 'pointer';
closeButton.style.color = '#333';
closeButton.style.fontSize = '16px';
closeButton.style.fontWeight = 'bold';
closeButton.onclick = () => popup.style.display = 'none';
popup.appendChild(closeButton);
};
const fetchChannelInfo = async (url) => {
try {
const response = await fetch(url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(policy.createHTML(html), 'text/html');
const meta = doc.querySelector('meta[property="og:description"]');
const description = meta ? meta.getAttribute('content') : 'No description available.';
return `Description: ${description}
`;
} catch (error) {
return 'Failed to load description.';
}
};
const throttle = (func, limit) => {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
};
const observeDOM = () => {
const observer = new MutationObserver((mutations, obs) => {
setTimeout(() => {
const channelElements = document.querySelectorAll('.ytd-channel-name#text-container');
if (channelElements.length) {
init(channelElements);
obs.disconnect(); // Stop observing after successful initialization
}
}, 1000);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('[YouTube Channel Hover Popup] - observeDOM')
};
const init = (channelElements) => {
console.log('YouTube Channel Hover Popup - init', { channelElements } )
channelElements.forEach(channelElement => {
let popupTimeout;
channelElement.addEventListener('mouseenter', async (e) => {
clearTimeout(popupTimeout);
popupTimeout = setTimeout(async () => {
const url = channelElement.querySelector('a').href;
showLoadingPopup(popup, e.clientX, e.clientY + 20);
const content = await fetchChannelInfo(url);
updatePopupContent(popup, content);
}, 500);
});
channelElement.addEventListener('mouseleave', () => {
clearTimeout(popupTimeout);
popup.style.display = 'none';
});
const throttledMouseMove = throttle((e) => {
if (popup.style.display !== 'none') {
popup.style.left = `${e.clientX}px`;
popup.style.top = `${e.clientY + 20}px`;
}
}, 100); // Update popup position at most every 100ms
channelElement.addEventListener('mousemove', throttledMouseMove);
});
};
setTimeout(() => {
const channelElements = document.querySelectorAll('.ytd-channel-name#text-container');
init(channelElements)
}, 5000); // Start observing DOM for changes
setTimeout(() => {
observeDom()
}, 15000); // Start observing DOM for changes
})();