// ==UserScript==
// @name YouTube Quick Actions
// @description QUICK BOII
// @version 1.0.0
// @match https://www.youtube.com/*
// @license Unlicense
// Icon https://www.youtube.com/s/desktop/c722ba88/img/logos/favicon_144x144.png
// @grant GM_addStyle
// @compatible firefox
// @namespace https://greasyfork.org/users/1223791
// ==/UserScript==
"use strict";
console.log("🫡 [Youtube Quick Actions] Script initialized");
const css = String.raw;
const style = css`
#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 {
display: flex;
}
/*
#dismissible:hover:not(:has(ytm-shorts-lockup-view-model-v2)) > #quick-actions {
display: flex;
}
*/
ytm-shorts-lockup-view-model-v2:hover:not(:has(.ytDismissibleItemReplacedContent)) #quick-actions {
display: flex;
}
.xhover {
display: flex;!important
}
.xhide {
display: none;
}
`;
GM_addStyle(style);
/* -------------------------------------------------------------------------- */
/* Variables */
/* -------------------------------------------------------------------------- */
let isLoggingEnabled = false;
// Elem to search for
const normalVideoTagName = "YTD-RICH-ITEM-RENDERER";
const compactVideoTagName = "YTD-COMPACT-VIDEO-RENDERER";
const shortsV2VideoTagName = "YTM-SHORTS-LOCKUP-VIEW-MODEL-V2";
const shortsVideoTagName = "YTM-SHORTS-LOCKUP-VIEW-MODEL";
const playlistVideoTagName = "YT-LOCKUP-VIEW-MODEL";
const memberVideoTagName = "YTD-MEMBERSHIP-BADGE-RENDERER";
const thumbnailElementSelector = "img.yt-core-image";
const normalHamburgerMenuSelector = "button#button.style-scope.yt-icon-button";
const shortsAndPlaylistHamburgerMenuSelector = "button.yt-spec-button-shape-next";
const dropdownMenuSelector = "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 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 compactMenuPropertyPath = "menu.menuRenderer.items";
const membersOnlyMenuPropertyPath = "content.feedEntryRenderer.item.videoRenderer.menu.menuRenderer.items";
const availableMenuItemsList1 = "listItemViewModel?.title?.content";
const availableMenuItemsList2 = "menuServiceItemRenderer?.text?.runs?.[0]?.text";
//Icons = Font Awesome by @fontawesome - https://fontawesome.com */
const notInterestedIcon = `<svg class="qa-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM176.4 176a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm128 32a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm-122 174.5c-12.4 5.2-26.5-4.1-21.1-16.4c16-36.6 52.4-62.1 94.8-62.1s78.8 25.6 94.8 62.1c5.4 12.3-8.7 21.6-21.1 16.4c-22.4-9.5-47.4-14.8-73.7-14.8s-51.3 5.3-73.7 14.8z"/></svg>`;
const saveIcon = `<svg class="qa-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><path d="M512 416c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96C0 60.7 28.7 32 64 32l128 0c20.1 0 39.1 9.5 51.2 25.6l19.2 25.6c6 8.1 15.5 12.8 25.6 12.8l160 0c35.3 0 64 28.7 64 64l0 256zM232 376c0 13.3 10.7 24 24 24s24-10.7 24-24l0-64 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-64 0 0-64c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 64-64 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l64 0 0 64z"/></svg>`;
const dontRecommendChannelIcon = `<svg class="qa-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><path d="M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5zm45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5L412.5 367.2zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>`;
const hideIcon = `<svg class="qa-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor"><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/></svg>`;
/* -------------------------------------------------------------------------- */
/* Functions */
/* -------------------------------------------------------------------------- */
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,
};
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)];
for (const item of finalMenuItems)
{
if (!item) continue;
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(
`<button class="qa-button ${className}" data-icon="${className}" title="${titleText}" data-text="${titleText}">${icon}</button>`,
);
}
const buttonsContainer = document.createElement("div");
buttonsContainer.id = "quick-actions";
buttonsContainer.classList.add(position, type);
buttonsContainer.innerHTML = buttonsToAppend.join("");
//element.insertAdjacentElement("afterend", buttonsContainer);
element.insertAdjacentElement("beforeend", buttonsContainer);
}
/* -------------------------------------------------------------------------- */
/* Listeners */
/* -------------------------------------------------------------------------- */
document.addEventListener("mouseover", (event) =>
{
const path = event.composedPath();
for (let element of path)
{
if (
(element.tagName === normalVideoTagName ||
element.tagName === compactVideoTagName ||
element.tagName === shortsV2VideoTagName) &&
!element.querySelector("#quick-actions")
)
{
let type, data;
// Determine element type
if (element.tagName === shortsV2VideoTagName)
{
type = "shorts-v2";
} else
{
const isShort = element.querySelector(shortsVideoTagName) !== null;
const isPlaylist = element.querySelector(playlistVideoTagName) !== null;
const isMemberOnly = element.querySelector(memberVideoTagName) !== null;
type = isShort ? "shorts" :
element.tagName === compactVideoTagName ? "compact" :
isPlaylist ? "collection" :
isMemberOnly ? "members_only" :
"normal";
}
data = getDataProperty(element, type);
log("ℹ️ Video Type: ", type);
const thumbnailElement = element.querySelector(thumbnailElementSelector);
const thumbnailSize = parseInt(thumbnailElement.getClientRects("width")[0].width) || 100;
log("🖼️ Thumbnail Size: ", thumbnailSize);
const containerPosition = thumbnailSize < 200 ? "location-02" : "location-01";
if (!data)
{
log("⚠️ No props data found.");
return;
}
log("🎥 Video Props: ", data);
// Process menus based on video type
let menulist;
switch (type)
{
case "normal":
menulist = getByPathFunction(data, normalMenuPropertyPath);
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 "members_only":
menulist = getByPathFunction(data, membersOnlyMenuPropertyPath);
break;
default:
menulist = getByPathFunction(data, normalMenuPropertyPath);
break;
}
const menulistItems = getMenuList(menulist);
log("📃 Menu items: ", menulistItems);
appendButtons(element, menulistItems, type, containerPosition);
}
}
}, 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 "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
alert("shorts!");
menupath = null;
}
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(dropdownMenuSelector), {
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)
{
log(`✅ Matched: (${response} = ${item.textContent})`);
log(`✅`, item);
const btn = item;
await retryClick(btn, { 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()
}
});