// ==UserScript==
// @name YouTube Quick Actions
// @description Adds quick-action buttons like Hide, Save to Playlist, Not Interested, and Donβt Recommend
// @version 1.6.3
// @match https://www.youtube.com/*
// @license Unlicense
// @icon https://www.youtube.com/s/desktop/c722ba88/img/logos/favicon_144x144.png
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @compatible firefox
// @namespace https://greasyfork.org/users/1223791
// @downloadURL https://update.greasyfork.icu/scripts/533514/YouTube%20Quick%20Actions.user.js
// @updateURL https://update.greasyfork.icu/scripts/533514/YouTube%20Quick%20Actions.meta.js
// ==/UserScript==
"use strict";
console.log("π«‘ [Youtube Quick Actions] Script initialized");
const css = String.raw;
const style = css`
:root {
--color-primary: rgba(252, 146, 205, 1);
--color-secondary: rgba(33, 225, 255, 1) ;
}
#quick-actions {
position: absolute;
display: none;
flex-direction: column;
gap: 0.2em;
align-items: flex-start;
}
.location-01 {
top: 0.8em;
left: 0.8em;
}
.location-02 {
top: 0.4em;
left: 0.4em;
}
.qa-button {
background-color: rgba(0, 0, 0, 0.9);
/* box-shadow: inset 2px 3px 5px #000, 0px 0px 8px #d0d0d02e; */
z-index: 1000;
border: 1px solid #f0f0f05c;
width: 26px;
height: 26px;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
flex-shrink: unset;
}
.qa-button:hover {
border: 1px solid rgba(255, 255, 255, 0.2);
opacity: 0.9;
background-color: rgba(55, 55, 55, 0.9);
}
.qa-icon {
width: 1em;
height: 1em;
vertical-align: -0.125em;
}
YTD-RICH-ITEM-RENDERER:hover:not(:has(ytd-rich-grid-media[is-dismissed])):not(:has(.ytDismissibleItemReplacedContent)) #quick-actions,
YTD-COMPACT-VIDEO-RENDERER:hover:not([is-dismissed]):not(:has(#dismissed-content)) #quick-actions,
YTM-SHORTS-LOCKUP-VIEW-MODEL-V2:hover:not(:has(.ytDismissibleItemReplacedContent)) #quick-actions,
YT-LOCKUP-VIEW-MODEL:hover:not(:has(.ytDismissibleItemReplacedContent)) #quick-actions,
YTD-PLAYLIST-VIDEO-RENDERER:hover #quick-actions,
YTD-VIDEO-RENDERER:hover #quick-actions,
YTD-GRID-VIDEO-RENDERER:hover #quick-actions {
display: flex;
}
/*
#dismissible:hover:not(:has(ytm-shorts-lockup-view-model-v2)) > #quick-actions {
display: flex;
}
*/
YT-LOCKUP-VIEW-MODEL:hover:has(#quick-actions),
YTD-PLAYLIST-VIDEO-RENDERER:hover:has(#quick-actions) {
position: relative;
}
.fancy {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(
45deg,
var(--color-primary) 17%,
var(--color-secondary) 100%
);
background-size: 400% auto;
background-position: 0% 50%;
animation: animate-gradient 12s linear infinite;
font-weight: bold!important;
}
@keyframes animate-gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
`;
GM_addStyle(style);
/* -------------------------------------------------------------------------- */
/* Variables */
/* -------------------------------------------------------------------------- */
// Elem to search for
const normalVideoTagName = "YTD-RICH-ITEM-RENDERER";
const searchVideoTagName = "YTD-VIDEO-RENDERER";
const gridVideoTagName = "YTD-GRID-VIDEO-RENDERER";
const compactVideoTagName = "YTD-COMPACT-VIDEO-RENDERER";
const shortsV2VideoTagName = "YTM-SHORTS-LOCKUP-VIEW-MODEL-V2";
const shortsVideoTagName = "YTM-SHORTS-LOCKUP-VIEW-MODEL";
const compactPlaylistContainer = "YTD-ITEM-SECTION-RENDERER";
const compactPlaylistSelector = ".yt-lockup-view-model-wiz";
const playlistVideoTagName = "YT-LOCKUP-VIEW-MODEL";
const playlistVideoTagName2 = "YTD-PLAYLIST-VIDEO-RENDERER";
const memberVideoTagName = "YTD-MEMBERSHIP-BADGE-RENDERER";
const memberVideoSelector = ".badge-style-type-members-only";
const thumbnailElementSelector = "img.yt-core-image";
const normalHamburgerMenuSelector = "button#button.style-scope.yt-icon-button";
const shortsAndPlaylistHamburgerMenuSelector = "button.yt-spec-button-shape-next";
const dropdownMenuTagName = "TP-YT-IRON-DROPDOWN";
const popupMenuItemsSelector = "yt-formatted-string.style-scope.ytd-menu-service-item-renderer, yt-list-item-view-model[role='menuitem']";
//Menu Extractions / Properties Path
const searchMenuPropertyPath = "menu.menuRenderer.items";
const gridMenuPropertyPath = "menu.menuRenderer.items";
const shortsMenuPropertyPath = "content.shortsLockupViewModel.menuOnTap.innertubeCommand.showSheetCommand.panelLoadingStrategy.inlineContent.sheetViewModel.content.listViewModel.listItems";
const shortsV2MenuPropertyPath = "menuOnTap.innertubeCommand.showSheetCommand.panelLoadingStrategy.inlineContent.sheetViewModel.content.listViewModel.listItems";
const normalMenuPropertyPath = "content.videoRenderer.menu.menuRenderer.items";
const playlistMenuPropertyPath = "content.lockupViewModel.metadata.lockupMetadataViewModel.menuButton.buttonViewModel.onTap.innertubeCommand.showSheetCommand.panelLoadingStrategy.inlineContent.sheetViewModel.content.listViewModel.listItems";
const playlistMenuPropertyPath2 = "menu.menuRenderer.items";
const compactPlaylistMenuPropertyPath = "metadata.lockupMetadataViewModel.menuButton.buttonViewModel.onTap.innertubeCommand.showSheetCommand.panelLoadingStrategy.inlineContent.sheetViewModel.content.listViewModel.listItems";
const compactMenuPropertyPath = "menu.menuRenderer.items";
const membersOnlyMenuPropertyPath = "content.feedEntryRenderer.item.videoRenderer.menu.menuRenderer.items";
const membersOnlyMenuPropertyPath2 = "content.videoRenderer.menu.menuRenderer.items";
const availableMenuItemsList1 = "listItemViewModel?.title?.content";
const availableMenuItemsList2 = "menuServiceItemRenderer?.text?.runs?.[0]?.text";
const normalVideoRichThumbnailPath = "content?.videoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails?.[0]?.url";
const normalVideoThumbnailPath = "content?.videoRenderer?.thumbnail?.thumbnails";
const compactVideoRichThumbnailPath = "richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails?.[0]?.url";
const compactVideoThumbnailPath = "thumbnail?.thumbnails";
//
const notInterestedIcon = ``;
const frownIcon = ``;
const saveIcon = ``;
const dontRecommendChannelIcon = ``;
const hideIcon = ``;
const pooIcon = ``;
const mehIcon = ``;
const trashIcon = ``;
/* -------------------------------------------------------------------------- */
/* Functions */
/* -------------------------------------------------------------------------- */
/* ----------------------------- Menu Commmands ----------------------------- */
let isLoggingEnabled = GM_getValue("isLoggingEnabled", false);
let optRichThumbnail = GM_getValue("optRichThumbnail", true);
const menuCommands = [
{
label: () => `Rich Thumbnail: ${optRichThumbnail ? "ON" : "OFF"}`,
toggle: function toggleRichThumbnail()
{
optRichThumbnail = !optRichThumbnail;
GM_setValue("optRichThumbnail", optRichThumbnail);
updateMenuCommands();
window.location.reload(true);
},
id: undefined,
},
{
label: () => `Logging: ${isLoggingEnabled ? "ON" : "OFF"}`,
toggle: function toggleLogging()
{
isLoggingEnabled = !isLoggingEnabled;
GM_setValue("isLoggingEnabled", isLoggingEnabled);
updateMenuCommands();
window.location.reload(true);
},
id: undefined,
}
];
function registerMenuCommands()
{
for (const command of menuCommands)
{
command.id = GM_registerMenuCommand(command.label(), command.toggle);
}
}
function updateMenuCommands()
{
for (const command of menuCommands)
{
if (command.id)
{
GM_unregisterMenuCommand(command.id);
}
command.id = GM_registerMenuCommand(command.label(), command.toggle);
}
}
function toggleRichThumbnail()
{
optRichThumbnail = !optRichThumbnail;
GM_setValue("toggle5050Endorsement", optRichThumbnail);
updateMenuCommands();
window.location.reload(true);
}
function toggleLogging()
{
isLoggingEnabled = !isLoggingEnabled;
GM_setValue("isLoggingEnabled", isLoggingEnabled);
updateMenuCommands();
window.location.reload(true);
}
registerMenuCommands();
/* ---------------------------- Menu Commands End --------------------------- */
function log(...args)
{
if (isLoggingEnabled)
{
console.log(...args);
}
}
function getByPathReduce(target, path)
{
return path.split('.').reduce((result, key) => result?.[key], target) ?? [];
}
//Same result as getByPathReduce()
function getByPathFunction(object, path)
{
try
{
return new Function('object', `return object.${path}`)(object) ?? [];
} catch
{
return [];
}
}
function getDataProperty(origin, videoType)
{
const childQuerySelectors = {
"shorts-v2": shortsVideoTagName,
"compact-playlist": compactPlaylistSelector,
};
const selector = childQuerySelectors[videoType];
const target = selector ? origin.querySelector(selector) : origin;
return target?.data;
}
function getMenuList(target)
{
return target.map(item =>
{
const first = getByPathFunction(item, availableMenuItemsList1);
if (first.length) return first;
const second = getByPathFunction(item, availableMenuItemsList2);
if (second.length) return second;
return null;
}).filter(Boolean);
}
function findElemInParentDomTree(originElem, targetSelector)
{
log(`π Starting search from:`, originElem);
let node = originElem;
while (node)
{
log(`π Checking ancestor:`, node);
const found = Array.from(node.children).find(
(child) => child.matches(targetSelector) || child.querySelector(targetSelector)
);
if (found)
{
const result = found.matches(targetSelector) ? found : found.querySelector(targetSelector);
log(`β
Found target:`, result);
return result;
}
node = node.parentElement;
}
log("β οΈ No matching element found.");
return null;
}
function getVisibleElem(targetSelector)
{
const elements = document.querySelectorAll(targetSelector);
for (const element of elements)
{
const rect = element.getBoundingClientRect();
if (element.offsetParent !== null && rect.width > 0 && rect.height > 0)
{
log("π Menu is visible and ready:", element);
return element;
}
}
log("β οΈ No visible menu found.");
return null;
}
async function waitUntil(conditionFunction, { interval = 100, timeout = 3000 } = {})
{
const startTime = Date.now();
while (Date.now() - startTime < timeout)
{
const result = conditionFunction();
if (result) return result;
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error("β° Timeout: Target element is not visible in time");
}
function retryClick(element, { maxAttempts = 5, interval = 300 } = {})
{
return new Promise((resolve) =>
{
let attempts = 0;
function tryClick()
{
if (!element || attempts >= maxAttempts)
{
log("β οΈ Retry failed or element missing.");
return resolve();
}
const rect = element.getBoundingClientRect();
const isVisible = rect.width > 0 && rect.height > 0;
if (isVisible)
{
element.dispatchEvent(
new MouseEvent("click", {
view: document.defaultView,
bubbles: true,
cancelable: true,
}),
);
log("π Clicked matching menu item");
return resolve();
} else
{
attempts++;
setTimeout(tryClick, interval);
}
}
tryClick();
});
}
function appendButtons(element, menuItems, type, position)
{
let className, titleText, icon;
let buttonsToAppend = [];
const finalMenuItems = [...new Set(menuItems)];
//If menu is empty, proceed and still append the container to prevent looping of menu data probe.
//Probe will only skip if #quick-action exist.
for (const item of finalMenuItems)
{
if (!item) continue;
let className;
let titleText;
let icon;
if (item.startsWith("Remove from "))
{
className = "remove";
titleText = "Remove from playlist";
icon = trashIcon;
} else
{
switch (item)
{
case "Not interested":
className = "not_interested";
titleText = "Not interested";
icon = notInterestedIcon;
break;
case "Don't recommend channel":
className = "dont_recommend_channel";
titleText = "Don't recommend channel";
icon = dontRecommendChannelIcon;
break;
case "Hide":
className = "hide";
titleText = "Hide video";
icon = hideIcon;
break;
case "Save to playlist":
className = "save";
titleText = "Save to playlist";
icon = saveIcon;
break;
default:
continue;
}
}
buttonsToAppend.push(
``,
);
}
const buttonsContainer = document.createElement("div");
buttonsContainer.id = "quick-actions";
buttonsContainer.classList.add(position, type);
buttonsContainer.innerHTML = buttonsToAppend.join("");
//element.insertAdjacentElement("afterend", buttonsContainer);
const exist = element.querySelector("#quick-actions");
if (exist) return;
element.insertAdjacentElement("beforeend", buttonsContainer);
}
function onPageChange(callback)
{
const listenerMap = new Map();
['pushState', 'replaceState'].forEach(method =>
{
const original = history[method];
const wrapped = function (...args)
{
const result = original.apply(this, args);
window.dispatchEvent(new Event('spa-route-change'));
return result;
};
history[method] = wrapped;
listenerMap.set(method, original);
});
const onSpaRouteChange = () => callback('spa', window.location.href);
const onPopState = () => window.dispatchEvent(new Event('spa-route-change'));
const onYtAction = (event) =>
{
const actionName = event?.detail?.actionName;
if (actionName === 'yt-history-pop' || actionName === 'yt-navigate')
{
callback('yt', window.location.href);
}
};
window.addEventListener('spa-route-change', onSpaRouteChange);
window.addEventListener('popstate', onPopState);
document.addEventListener('yt-action', onYtAction);
return function cleanup()
{
for (const [method, original] of listenerMap.entries())
{
history[method] = original;
}
window.removeEventListener('spa-route-change', onSpaRouteChange);
window.removeEventListener('popstate', onPopState);
document.removeEventListener('yt-action', onYtAction);
};
}
/* -------------------------------------------------------------------------- */
/* Listeners */
/* -------------------------------------------------------------------------- */
// Remove all existing quick-action elements. On certain pages, like channel tabs, content is updated in-place
// without removing the grid/container. If not cleared, old quick-action buttons will remain attached to unrelated items.
// This ensures that if the content is updated, new hover actions will fetch fresh, relevant data.
// I have not take a closer look at yt-made events. propably have some things we can customized and fire to speed things up
// skip querying and fired the action straight up via their internal events
let richThumbnailDisabler = new Date(Date.now() + 360 * 60 * 1000);
onPageChange((source, url) =>
{
richThumbnailDisabler = new Date(Date.now() + 360 * 60 * 1000);
});
document.addEventListener("yt-action", (event) =>
{
if (event.detail.actionName === "ytd-update-grid-state-action")
{
log("π Page updated.");
document.querySelectorAll("#quick-actions").forEach((element) => element.remove());
}
});
let opThumbnail, riThumbnail;
document.addEventListener("mouseover", (event) =>
{
const path = event.composedPath();
for (let element of path)
{
if (
(element.tagName === normalVideoTagName ||
element.tagName === compactVideoTagName ||
element.tagName === shortsV2VideoTagName ||
element.tagName === searchVideoTagName ||
element.tagName === gridVideoTagName ||
element.tagName === playlistVideoTagName ||
element.tagName === playlistVideoTagName2) &&
!element.querySelector("#quick-actions")
)
{
let type, data;
// Determine element type
// Hierarchy might need tweaking to simplify detection. nah this whole listener block,
// cause i'm already confused which tag is needed for which video, what need extra query, then which path
// and specific video type wont get shown unless specific step is done, even then rarely replicable to debug
// some of this type no longer valid as i go, cause i can't keep track no more
if (element.tagName === shortsV2VideoTagName)
{
type = "shorts-v2";
}
else if (element.tagName === playlistVideoTagName && element.parentElement.parentElement.tagName === compactPlaylistContainer)
{
type = "compact-playlist";
}
else if (element.tagName === gridVideoTagName)
{
type = "grid-video";
}
else if (element.tagName === searchVideoTagName)
{
type = "search-video";
}
else if (element.tagName === playlistVideoTagName && element.parentElement.parentElement.tagName === normalVideoTagName)
{
//hover listener will land on playlistVideoTagName instead of normalVideoTagName for playlist/mixes on homepage
//so manually change back to normalVideoTagName as data is there.
element = element.parentElement.parentElement;
if (element.querySelector("#quick-actions")) return;
type = "playlist";
}
else if (element.tagName === playlistVideoTagName2)
{
type = "playlist2";
}
else
{
const isShort = element.querySelector(shortsVideoTagName) !== null;
const isPlaylist = element.querySelector(playlistVideoTagName) !== null;
const isMemberOnly =
element.querySelector(memberVideoTagName) !== null ||
element.querySelector(memberVideoSelector) !== null;
type = isShort ? "shorts" :
element.tagName === compactVideoTagName ? "compact" :
isPlaylist ? "collection" :
isMemberOnly ? "members_only" :
"normal";
}
log("β Video Elem: ", element.tagName, element);
log("βΉοΈ Video Type: ", type);
data = getDataProperty(element, type);
const thumbnailElement = element.querySelector(thumbnailElementSelector);
const thumbnailSize =
thumbnailElement?.getClientRects?.().length > 0
? parseInt(thumbnailElement.getClientRects()[0].width)
: 100;
log("πΌοΈ Thumbnail Size: ", thumbnailSize);
const containerPosition = thumbnailSize < 211 ? "location-02" : "location-01";
if (!data)
{
log("β οΈ No props data found.");
return;
}
log("π₯ Video Props: ", data);
let menulist;
switch (type)
{
case "normal":
menulist = getByPathFunction(data, normalMenuPropertyPath);
break;
case "search-video":
menulist = getByPathFunction(data, searchMenuPropertyPath);
break;
case "grid-video":
menulist = getByPathFunction(data, gridMenuPropertyPath);
break;
case "shorts":
menulist = getByPathFunction(data, shortsMenuPropertyPath);
break;
case "shorts-v2":
menulist = getByPathFunction(data, shortsV2MenuPropertyPath);
break;
case "compact":
menulist = getByPathFunction(data, compactMenuPropertyPath);
break;
case "collection":
menulist = getByPathFunction(data, playlistMenuPropertyPath);
break;
case "playlist":
menulist = getByPathFunction(data, playlistMenuPropertyPath);
break;
case "playlist2":
menulist = getByPathFunction(data, playlistMenuPropertyPath2);
break;
case "compact-playlist":
menulist = getByPathFunction(data, compactPlaylistMenuPropertyPath);
break;
case "members_only":
menulist = getByPathFunction(data, membersOnlyMenuPropertyPath);
if (!menulist.length)
{
menulist = getByPathFunction(data, membersOnlyMenuPropertyPath2);
}
break;
default:
menulist = getByPathFunction(data, normalMenuPropertyPath);
break;
}
const menulistItems = getMenuList(menulist);
log("π Menu items: ", menulistItems);
appendButtons(element, menulistItems, type, containerPosition);
//Rich Thumbnails
//Rich thumbnail is hardcoded on dataset and expired after 6 hours therefore
//we'll disable it after 6 hours from page first loaded.
//on error check is also added to prevent gray default to be added if rich thumbnail is expired
if (optRichThumbnail && Date.now() < richThumbnailDisabler)
{
log("πΈ Rich Thumbnails: ", Date.now() < richThumbnailDisabler);
let hoverToken = null;
const mouseOverHandler = async (event) =>
{
const token = Symbol();
hoverToken = token;
const currentThumbnail = element.querySelector("img.yt-core-image");
const thumbailData = getDataProperty(element, type);
const normalRichThumbnail = getByPathFunction(thumbailData, normalVideoRichThumbnailPath);
const compactRichThumbnail = getByPathFunction(thumbailData, compactVideoRichThumbnailPath);
const richThumbnail =
(typeof normalRichThumbnail === 'string' && normalRichThumbnail) ||
(typeof compactRichThumbnail === 'string' && compactRichThumbnail) ||
undefined;
function isImageValid(url)
{
return new Promise((resolve) =>
{
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = url;
});
}
const isValid = await isImageValid(richThumbnail);
if (richThumbnail && isValid && hoverToken === token)
{
currentThumbnail.src = richThumbnail;
}
};
const mouseOutHandler = (event) =>
{
hoverToken = null;
const currentThumbnail = element.querySelector("img.yt-core-image");
const thumbnailData = getDataProperty(element, type);
const normalThumbnails = getByPathFunction(thumbnailData, normalVideoThumbnailPath);
const compactThumbnails = getByPathFunction(thumbnailData, compactVideoThumbnailPath);
const biggestNormalThumbnail = normalThumbnails.at(-1)?.url;
const biggestCompactThumbnail = compactThumbnails.at(-1)?.url;
const staticThumbnail = biggestNormalThumbnail || biggestCompactThumbnail;
if (staticThumbnail)
{
currentThumbnail.src = staticThumbnail;
}
};
element.addEventListener("mouseenter", mouseOverHandler, true);
element.addEventListener("mouseleave", mouseOutHandler, true);
setTimeout(() =>
{
element.removeEventListener("mouseover", mouseOverHandler, true);
element.removeEventListener("mouseout", mouseOutHandler, true);
log("πΈ Rich Thumbnails: disabled after timeout");
}, richThumbnailDisabler - Date.now());
}
}
}
}, true);
document.addEventListener("click", async function (event)
{
const button = event.target.closest(".qa-button");
if (!button) return;
event.stopPropagation();
event.stopImmediatePropagation();
event.preventDefault();
const actionType = button.dataset.icon;
let response;
switch (actionType)
{
case "not_interested":
response = "Not interested";
log("π΄ Marking as not interested");
break;
case "dont_recommend_channel":
response = "Don't recommend channel";
log("π« Don't recommend channel");
break;
case "hide":
response = "Hide";
log("ποΈ Hiding video");
break;
case "remove":
response = "Remove from";
log("ποΈ Remove from playlist");
break;
case "save":
response = "Save to playlist";
log("π Saving to playlist");
break;
default:
log("β οΈ Unknown action");
}
let menupath;
if (button.parentElement.parentElement.tagName === shortsV2VideoTagName || button.parentElement.parentElement.querySelector(playlistVideoTagName))
{
menupath = shortsAndPlaylistHamburgerMenuSelector;
}
else if (button.parentElement.classList.contains("shorts"))
{
//shorts but not inside shortsv2 container idk where i found this its gone now crazy i was crazy once
//been a while, probably safe to remove now.
alert("shorts!");
menupath = shortsAndPlaylistHamburgerMenuSelector;
}
else if (button.parentElement.classList.contains("compact-playlist"))
{
menupath = shortsAndPlaylistHamburgerMenuSelector;
}
else
{
menupath = normalHamburgerMenuSelector;
}
const menus = findElemInParentDomTree(button, menupath);
if (!menus)
{
log("β Menu button not found.");
return;
}
menus.dispatchEvent(new MouseEvent("click", { bubbles: true }));
log("π Button clicked, waiting for menu...");
try
{
const visibleMenu = await waitUntil(() => getVisibleElem(dropdownMenuTagName), {
interval: 100,
timeout: 3000,
});
if (visibleMenu)
{
try
{
const targetItem = await waitUntil(
() =>
{
const items = visibleMenu.querySelectorAll(popupMenuItemsSelector);
return items.length > 0 ? items : null;
},
{
interval: 100,
timeout: 5000,
},
);
if (targetItem)
{
log("π Target items found:", targetItem);
for (const item of targetItem)
{
if (
item.textContent === response ||
(response === "Remove from" && item.textContent.startsWith("Remove from"))
)
{
log(`β
Matched: (${response} = ${item.textContent})`);
log(`β
`, item);
const button = item;
await retryClick(button, { maxAttempts: 5, interval: 300 }).finally(() =>
{
document.body.click();
});
break;
} else
{
log(`β Not a match: (${response} = ${item.textContent})`);
}
}
}
} catch (error)
{
log("π !", error.message);
//document.body.click()
}
}
//setTimeout(() => document.body.click(), 200);
} catch (error)
{
log("π !!", error.message);
//document.body.click()
}
});
/* -------------------------------------------------------------------------- */
/* This script is brought to you in support of FIFTY FIFTY π */
/* -------------------------------------------------------------------------- */
if ("π")
{
const selectorsToWatch = ['a', 'yt-formatted-string'];
const observedElements = new WeakMap();
function observeTextContentChanges(element)
{
if (observedElements.has(element)) return;
const elementObserver = new MutationObserver(() =>
{
const hasText = element.textContent.trim() === "FIFTY FIFTY Official";
element.classList.toggle("fancy", hasText);
});
observedElements.set(element, elementObserver);
elementObserver.observe(element, { characterData: true, childList: true, subtree: true });
}
document.querySelectorAll(selectorsToWatch.join(',')).forEach(element =>
{
if (element.textContent.trim() === "FIFTY FIFTY Official") element.classList.add("fancy");
observeTextContentChanges(element);
});
const observer = new MutationObserver((mutations) =>
{
for (const mutation of mutations)
{
for (const node of mutation.addedNodes)
{
if (node.nodeType !== 1) continue;
for (const selector of selectorsToWatch)
{
const elements = node.matches(selector) ? [node] : node.querySelectorAll(selector);
elements.forEach(element =>
{
if (element.textContent.trim() === "FIFTY FIFTY Official") element.classList.add("fancy");
observeTextContentChanges(element);
});
}
}
for (const node of mutation.removedNodes)
{
if (node.nodeType === 1 && observedElements.has(node))
{
observedElements.get(node).disconnect();
observedElements.delete(node);
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}