NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name YouTube Unclutter
// @version 0.2
// @description This little script aims clean up the YouTube UI by removing unnecessary elements.
// @author TibixDev
// @match https://www.youtube.com/*
// @icon https://i.imgur.com/aAEvYbj.png
// @license MIT
// @homepageURL https://github.com/TibixDev/YTUnclutter
// ==/UserScript==
(async function () {
'use strict';
console.log("[YT-Unclutter] YouTube Unclutter script loaded");
// DJB2 hashing function
// Credit: https://gist.github.com/eplawless/52813b1d8ad9af510d85?permalink_comment_id=3367765#gistcomment-3367765
function djb2(str) {
let len = str.length
let h = 5381
for (let i = 0; i < len; i++) {
h = h * 33 ^ str.charCodeAt(i)
}
return h >>> 0
}
// Wait for DOM element function
// Credit: https://stackoverflow.com/a/61511955/10771609
function waitForElem(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
function waitForAllElemChildrenToMatchSelector(element, selector) {
return new Promise(resolve => {
function detect() {
let children = [...element.children];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
//if (!children[i].querySelector(selector)) {
// return false;
//}
if (!children[i].getElementsByTagName(selector).length) {
return false;
}
}
return true;
}
return false;
}
if (detect()) {
resolve();
}
const observer = new MutationObserver(mutations => {
if (detect()) {
resolve();
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
let titleElem = (await waitForElem("h1.title.ytd-video-primary-info-renderer>*"));
let title = '';
console.log("Title is: " + title);
let previousUrl = '';
const observer = new MutationObserver(async function (mutations) {
if (window.location.href !== previousUrl && title !== titleElem.innerText) {
console.log(`URL changed to ${window.location.href}`);
console.log(`Old title was ${title}, changed to ${titleElem.innerText}`);
title = titleElem.innerText;
previousUrl = window.location.href;
if (window.location.href.includes("watch?v=")) {
console.log("[YT-Unclutter] Video URL detected, uncluttering...");
waitForElem("#top-level-buttons-computed path")
.then(async () => {
console.log("[YT-Unclutter] Found first path element...");
await doUnclutter();
})
}
}
});
const config = { subtree: true, childList: true };
observer.observe(document, config);
async function doUnclutter() {
let ytButtonsParent = document.querySelector("#info>#menu-container>#menu>ytd-menu-renderer>#top-level-buttons-computed");
let ytButtons = [...ytButtonsParent.children];
await waitForAllElemChildrenToMatchSelector(ytButtonsParent, "path")
console.log("[YT-Unclutter] Detected path for all ytButtonsParent children");
console.log(`[YT-Unclutter] Found ${ytButtons.length} native YT buttons`);
console.log(ytButtons);
const buttonHashes = {
like: 4121615887,
dislike: 1508558179,
share: 3222078241,
download: 668597389,
thank: 3561708383,
clip: 1038096254,
save: 2447476316
}
let buttonsToRemove = ["share", "download", "thank", "clip"];
console.log(`[YT-Unclutter] Removing ${ytButtons.length} buttons`);
for (let i = 0; i < ytButtons.length; i++) {
let button = ytButtons[i];
console.log(`Iter #${i} | Button: ${!!button}`)
let buttonHash = djb2(button.getElementsByTagName("path")[0].getAttribute("d"));
console.log(`Button #${i} has hash: ${buttonHash}`);
for (let buttonType of buttonsToRemove) {
if (buttonHash === buttonHashes[buttonType]) {
console.log(`[YT-Unclutter] Removing ${buttonType} button (#${i} - ${buttonHash})`);
button.remove();
break;
}
}
}
console.log(ytButtons);
console.log("[YT-Unclutter] Uncluttering done");
}
})();