Skip to content

Instantly share code, notes, and snippets.

@dmtri

dmtri/script.js Secret

Last active September 8, 2023 16:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmtri/22208d811886c6c50fe98e4ea4da5570 to your computer and use it in GitHub Desktop.
Save dmtri/22208d811886c6c50fe98e4ea4da5570 to your computer and use it in GitHub Desktop.
TamperMonkey Display Popup Channel Desc Youtube
// ==UserScript==
// @name YouTube channel description popup on hover
// @namespace http://tampermonkey.net/
// @version 0.1
// @description YouTube channel description popup on channel name hover!
// @author @dmtri
// @match https://www.youtube.com/*
// @icon
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const MAGIC_NUMBER = 1500;
let allEventHandlers = [];
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const debounce = (mainFunction, delay) => {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
mainFunction(...args);
}, delay);
};
};
const initTag = '.youtube-popup-desc-init';
const profileIdentifierUrlContainer = '#container.ytd-channel-name';
// get the url from element with profileIdentifier
const getProfileUrl = (profileMetaDataElement) => {
const anchor = profileMetaDataElement.getElementsByTagName('a')[0];
return anchor.href;
};
const clearEvents = (profileMetaDataElement) => {
// clear all existing event handlers
allEventHandlers.forEach((handler) => {
profileMetaDataElement.forEach((element) => {
element.removeEventListener('mouseenter', handler);
});
});
allEventHandlers = [];
};
// display a popup when hover over the profileMetaData element
const init = (force = false) => {
if (!force && document.querySelector(initTag)) {
return;
}
const profileMetaDataElement = document.querySelectorAll(profileIdentifierUrlContainer);
if (!profileMetaDataElement || !profileMetaDataElement.length) {
return;
}
clearEvents(profileMetaDataElement);
const inlineHandler = async (e) => {
const element = e.target;
let isValidGesture = true;
const mouseLeaveHandler = () => {
isValidGesture = false;
};
element.removeEventListener('mouseleave', mouseLeaveHandler);
element.addEventListener('mouseleave', mouseLeaveHandler);
await wait(1500);
// valid gesture meaning a mouse enter event
// is not followed by a mouse leave event
// within 1.5 seconds
handler(element, isValidGesture);
};
allEventHandlers.push(inlineHandler);
profileMetaDataElement.forEach((element) => {
element.addEventListener('mouseenter', inlineHandler);
});
// append init tag to document
const initTagElement = document.createElement('div');
initTagElement.classList.add(initTag.replace('.', ''));
document.body.appendChild(initTagElement);
// init when scrolled down on home page
const grid = '#contents.ytd-rich-grid-renderer';
const callback = (mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
debouncedInit(true);
}
});
};
let observer;
if (observer) observer.disconnect();
observer = new MutationObserver(callback);
const targetNode = document.querySelector(grid);
const config = { childList: true, subtree: true };
observer.observe(targetNode, config);
};
const debouncedInit = debounce(init, MAGIC_NUMBER);
const handler = async (profileMetaDataElement, isValidGesture) => {
if (!isValidGesture) return;
// display a native dialog element
const dialog = document.createElement('dialog');
const url = getProfileUrl(profileMetaDataElement);
let desc = '';
// append a spinner next to a channel name, then close it after 3 sec
const spinner = document.createElement('div');
spinner.innerHTML = 'loading...';
profileMetaDataElement.appendChild(spinner);
setTimeout(() => {
spinner.remove();
}, 3000);
await fetch(url)
.then((response) => response.text())
.then((html) => {
// <meta property="og:description" content="This is a place for all the things that are awesome on stream. ">
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const meta = doc.querySelector('meta[property="og:description"]');
desc = !meta ? (desc = 'No desc available') : meta.getAttribute('content');
});
dialog.innerHTML = `
<div>
<h2>${desc}</h2>
<h1>
<a href="${url}">${url}</h1>
</h1>
</div>
`;
document.body.appendChild(dialog);
dialog.showModal();
setTimeout(() => {
dialog.close();
}, 3000);
};
// while there is no init tag in the document, keep trying to init
const tryInit = () => {
if (!document.querySelector(initTag)) {
setTimeout(() => {
init();
tryInit();
}, MAGIC_NUMBER);
}
};
tryInit();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment