// ==UserScript== // @name YouTube Quick Actions (Hide, Not Interested, Don’t Recommend, Save to Playlist) // @description Adds quick-action buttons like Hide, Save to Playlist, Not Interested, and Don’t Recommend // @version 2.0.1.1 // @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 // @run-at document-start // @compatible firefox // @require https://cdnjs.cloudflare.com/ajax/libs/loglevel/1.9.2/loglevel.min.js // @namespace https://greasyfork.org/users/1223791 // @downloadURL https://update.greasyfork.icu/scripts/533514/YouTube%20Quick%20Actions%20%28Hide%2C%20Not%20Interested%2C%20Don%E2%80%99t%20Recommend%2C%20Save%20to%20Playlist%29.user.js // @updateURL https://update.greasyfork.icu/scripts/533514/YouTube%20Quick%20Actions%20%28Hide%2C%20Not%20Interested%2C%20Don%E2%80%99t%20Recommend%2C%20Save%20to%20Playlist%29.meta.js // ==/UserScript== (function () { "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.2rem; align-items: flex-start; } #quick-actions.location-01 { top: 0.5rem; left: 0.5rem; } #quick-actions.location-02 { top: 0.4rem; left: 0.4rem; } #quick-actions.location-01 .qa-button { width: 3rem; } #quick-actions.location-02 .qa-button { width: 2.6rem; } #quick-actions .qa-button { background-color: rgba(0, 0, 0, 0.8); z-index: 1000; border: 1px solid rgba(255, 255, 255, 0.02); height: auto; display: flex; justify-content: center; align-items: center; color: white; font-size: 1rem; font-weight: bold; border-radius: 4px; cursor: pointer; flex-shrink: unset; padding: 0.5rem; pointer-events: auto !important; } #quick-actions .qa-button.circle { border-radius: 50%; } #quick-actions .qa-button:hover { border: 1px solid rgba(255, 255, 255, 0.02); background-color: rgba(0, 0, 0, 1); } #quick-actions .qa-icon { width: 100%; height: 100%; max-width: 100%; max-height: 100%; } :is( ytd-grid-video-renderer, ytd-video-renderer, ytd-rich-item-renderer, yt-lockup-view-model, ytm-shorts-lockup-view-model-v2, ytd-compact-video-renderer, ytd-rich-grid-media, ytm-shorts-lockup-view-model, ytd-playlist-video-renderer):has(.yt-spec-button-shape-next--enable-backdrop-filter-experiment:not([aria-label="Notify me"])) .qa-button.frosted { background-color: rgba(0, 0, 0, 0.3) !important; backdrop-filter: blur(4px); box-shadow: 0px 0px 1px 0px rgba(255, 255, 255, 0.1); border: 0px solid #ffffff; } :is( ytd-grid-video-renderer, ytd-video-renderer, ytd-rich-item-renderer, yt-lockup-view-model, ytm-shorts-lockup-view-model-v2, ytd-compact-video-renderer, ytd-rich-grid-media, ytm-shorts-lockup-view-model, ytd-playlist-video-renderer):has(.yt-spec-button-shape-next--enable-backdrop-filter-experiment:not([aria-label="Notify me"])) .qa-button.frosted:hover { opacity: 1; background: rgba(40, 40, 40, 0.6)!important; border: 0px solid #ffffff; } :is(ytd-grid-video-renderer, ytd-video-renderer, ytd-rich-item-renderer, yt-lockup-view-model, ytm-shorts-lockup-view-model-v2, ytd-compact-video-renderer, ytd-rich-grid-media, ytm-shorts-lockup-view-model, ytd-playlist-video-renderer):hover:has(ytd-menu-renderer, button-view-model, .yt-spec-button-shape-next__icon):not([is-dismissed]):not(:has(ytd-rich-grid-media[is-dismissed])):not(:has(.ytDismissibleItemReplacedContent)) #quick-actions { display: flex; } :is(yt-lockup-view-model, 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; letter-spacing: .1rem; } @keyframes animate-gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } `; GM_addStyle(style); /* -------------------------------------------------------------------------- */ /* Variables */ /* -------------------------------------------------------------------------- */ //Selectors to probe for prop const tags = ["ytd-video-renderer", "ytd-rich-item-renderer", "ytm-shorts-lockup-view-model-v2", "yt-lockup-view-model", "ytd-playlist-video-renderer", "ytd-grid-video-renderer", "ytd-compact-movie-renderer", "ytd-compact-video-renderer", "ytd-rich-grid-media", "ytm-shorts-lockup-view-model"]; //"YTD-MEMBERSHIP-BADGE-RENDERER"; //Available menu items obj from prop const menu_items_from_props = [ "menu.menuRenderer.items", "content.listViewModel.listItems" ]; //Individual menu items from obj const menu_items_1 = "listItemViewModel?.title?.content"; const menu_items_2 = "menuServiceItemRenderer?.text?.runs?.[0]?.text"; const menu_items_3 = "menuNavigationItemRenderer?.text?.runs?.[0]?.text"; //Thumbnail size const thumbnail_elem_selector = "img.ytCoreImageHost"; //Action Button selectors const action_button_selectors = ["button-view-model", ".shortsLockupViewModelHostOutsideMetadataMenu", "yt-icon-button"]; //Dropdown Menu const dropdown_menu_tag_name = "TP-YT-IRON-DROPDOWN"; //Dropdown Menu Items const popup_menu_items_selector = "yt-formatted-string.style-scope.ytd-menu-service-item-renderer, yt-list-item-view-model[role='menuitem'], yt-formatted-string.ytd-menu-navigation-item-renderer"; // YT Event Names const yt_update_action_names = [ //"ytd-update-grid-state-action", //"ytd-rich-item-index-update-action", "yt-reload-continuation-items-command" ]; // Icons by Remix Icon (c) Remix Design Licensed under Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0 - https://github.com/Remix-Design/remixicon/blob/master/License const icon_not_interested = ``; const icon_hide = ``; const icon_save = ``; const icon_dont_recommend = ``; const downloadIcon = ``; const icon_trash = ``; /* -------------------------------------------------------------------------- */ /* Functions */ /* -------------------------------------------------------------------------- */ /* ----------------------------- Menu Commmands ----------------------------- */ let is_logging_enabled = GM_getValue("is_logging_enabled", false); let use_frosted = GM_getValue("use_frosted", true); let use_circle = GM_getValue("use_circle", false); const menu_commands = [ { label: () => `Frosted Button: ${use_frosted ? "βœ… ON" : "❌ OFF"}`, toggle: () => { use_frosted = !use_frosted; GM_setValue("use_frosted", use_frosted); window.location.reload(true); } }, { label: () => `Circle Button: ${use_circle ? "βœ… ON" : "❌ OFF"}`, toggle: () => { use_circle = !use_circle; GM_setValue("use_circle", use_circle); window.location.reload(true); } }, { label: () => `Logs: ${is_logging_enabled ? "βœ… ON" : "❌ OFF"}`, toggle: () => { is_logging_enabled = !is_logging_enabled; GM_setValue("is_logging_enabled", is_logging_enabled); window.location.reload(true); } } ]; function registermenu_commands() { for (const command of menu_commands) { GM_registerMenuCommand(command.label(), command.toggle); } } registermenu_commands(); is_logging_enabled ? log.enableAll() : log.disableAll(); /* ---------------------------- Menu Commands End --------------------------- */ function append_buttons(element, menulist_items, position) { const final_menulist_items = [...new Set(menulist_items)]; const buttons_to_append = []; for (const item of final_menulist_items) { if (!item) continue; let class_name; let title_text; let icon; if (item.startsWith("Remove from ")) { class_name = "remove"; title_text = "Remove from playlist"; icon = icon_trash; } else { switch (item) { case "Not interested": class_name = "not_interested"; title_text = "Not interested"; icon = icon_not_interested; break; case "Don't recommend channel": class_name = "dont_recommend_channel"; title_text = "Don't recommend channel"; icon = icon_dont_recommend; break; case "Hide": class_name = "hide"; title_text = "Hide video"; icon = icon_hide; break; case "Save to playlist": class_name = "save"; title_text = "Save to playlist"; icon = icon_save; break; default: continue; } } const set_frosted = use_frosted ? " frosted" : ""; const set_circle = use_circle ? " circle" : ""; buttons_to_append.push( ``, ); } const buttons_container = document.createElement("div"); buttons_container.id = "quick-actions"; buttons_container.classList.add(position); buttons_container.innerHTML = buttons_to_append.join(""); if (!element.querySelector("#quick-actions")) { element.insertAdjacentElement("beforeend", buttons_container); } } function get_prop(target, path) { try { return new Function('object', `return object.${path}`)(target) ?? []; } catch { return []; } } function get_menu_list(target) { if (!target) return; return target.map(item => { const paths = [menu_items_1, menu_items_2, menu_items_3]; for (const path of paths) { const result = get_prop(item, path); if (result.length) return result; } return null; }).filter(Boolean); } function enable_fallback_tooltip(selector) { let active_tooltip = null; let current_elem = null; const create_tooltip = (text) => { const tooltip = document.createElement("div"); tooltip.id = "quick-action-tooltip"; Object.assign(tooltip.style, { position: "fixed", zIndex: "999999", background: "rgba(0,0,0,0.8)", color: "#fff", padding: ".6rem 1rem", borderRadius: ".8rem", fontSize: "1.5rem", pointerEvents: "none" }); tooltip.textContent = text; document.body.appendChild(tooltip); return tooltip; }; document.addEventListener("pointerenter", event => { const target_elem = event.target.closest(selector); if (!target_elem) return; if (current_elem === target_elem) return; document.querySelectorAll("#quick-action-tooltip").forEach((element) => element.remove()); const tooltip_text = target_elem.getAttribute("title") || target_elem.getAttribute("data-text"); if (!tooltip_text) return; if (target_elem.hasAttribute("title")) { target_elem.setAttribute("data-original-title", tooltip_text); target_elem.removeAttribute("title"); } current_elem = target_elem; active_tooltip = create_tooltip(tooltip_text); const update_position = e => { if (!active_tooltip) return; active_tooltip.style.left = e.clientX + 8 + "px"; active_tooltip.style.top = e.clientY + 8 + "px"; }; current_elem._tooltip_move = update_position; document.addEventListener("pointermove", update_position); }, true); document.addEventListener("pointerleave", event => { const left_elem = event.target.closest(selector); if (!left_elem) return; if (event.relatedTarget && left_elem.contains(event.relatedTarget)) { return; } if (active_tooltip) { document.removeEventListener("pointermove", left_elem._tooltip_move); active_tooltip.remove(); active_tooltip = null; delete left_elem._tooltip_move; } if (left_elem.hasAttribute("data-original-title")) { left_elem.setAttribute("title", left_elem.getAttribute("data-original-title")); left_elem.removeAttribute("data-original-title"); } current_elem = null; }, true); }; function get_value_by_path(obj, path) { return path.split(".").reduce((acc, key) => { if (acc && typeof acc === "object" && key in acc) { return acc[key]; } return undefined; }, obj); } function find_target_path_value(obj) { if (obj == null || typeof obj !== "object") return null; for (const path of menu_items_from_props) { const value = get_value_by_path(obj, path); if (Array.isArray(value)) { log.log(`πŸŽ‰ Menu Obj Path Used: ${path}`); return value; } } for (const key in obj) { if (obj[key] && typeof obj[key] === "object") { const found = find_target_path_value(obj[key]); if (found !== null) return found; } } return null; } function find_first_menu_or_list_value(root_element = document) { const elements = root_element.querySelectorAll("*"); for (const element of elements) { if (element.data && typeof element.data === "object") { const found_value = find_target_path_value(element.data); if (found_value !== null) { return found_value; } } } return null; } function find_elem_in_parent_dom_tree(origin_elem, tag_names) { let node = origin_elem; let depth = 0; while (node && depth <= 3) { const button = node.querySelector(tag_names.join(',')); if (button) { return button; } node = node.parentElement; depth++; } return null; } async function wait_until(condition_func, { interval = 100, timeout = 3000 } = {}) { const start_time = Date.now(); while (Date.now() - start_time < timeout) { const result = condition_func(); if (result) return result; await new Promise((resolve) => setTimeout(resolve, interval)); } throw new Error("⏰ Timeout: Target element is not visible in time"); } function retry_click(element, { max_attempts = 5, interval = 1000 } = {}) { return new Promise((resolve) => { let attempts = 0; function try_click() { if (!element || attempts >= max_attempts) { log.log("⚠️ Retry failed or element missing."); return resolve(); } const rect = element.getBoundingClientRect(); const is_visible = rect.width > 0 && rect.height > 0; if (is_visible) { element.dispatchEvent( new MouseEvent("click", { view: document.defaultView, bubbles: true, cancelable: true, }), ); log.log("πŸ‘‡ Clicked matching menu item"); return resolve(); } else { attempts++; log.log("πŸ‘‡ Clicking attempt: ", attempts); setTimeout(try_click, interval); } } try_click(); }); } function get_visible_elem(target_selector, element = document.body) { const elements = element.querySelectorAll(target_selector); if (!elements || elements.length === 0) { return null; } for (const element of elements) { const rect = element.getBoundingClientRect(); const has_dimensions_and_in_view = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < (window.innerHeight || element.documentElement.clientHeight) && rect.left < (window.innerWidth || element.documentElement.clientWidth); if (!has_dimensions_and_in_view) { continue; } const computed_style = window.getComputedStyle(element); const is_visible = computed_style.opacity !== '0' && computed_style.visibility !== 'hidden' && computed_style.display !== 'none'; if (!is_visible) { continue; } log.log("πŸ‘€ Found visible element:", element); return element; } log.log("⚠️ No visible menu found."); return null; } /* -------------------------------------------------------------------------- */ /* Listeners */ /* -------------------------------------------------------------------------- */ document.addEventListener("mouseover", (event) => { const path = event.composedPath(); for (let element of path) { const tag = element?.tagName?.toLowerCase(); if (tag && !element.querySelector("#quick-actions") && tags.includes(tag)) { log.log("⭐ Video Elem: ", element.tagName, element); const data = find_first_menu_or_list_value(element); if (!data) { log.log("⚠️ No props data found."); //NOTE - Observe continue; } log.log("πŸŽ₯ Video Menu Props: ", data); const thumbnail_elem = element.querySelector(thumbnail_elem_selector); const thumbnail_size = thumbnail_elem?.getClientRects?.().length > 0 ? parseInt(thumbnail_elem.getClientRects()[0].width) : 100; log.log("πŸ–ΌοΈ Thumbnail Size: ", thumbnail_size); const container_position = thumbnail_size < 211 ? "location-02" : "location-01"; const menulist_items = get_menu_list(data); log.log("πŸ“ƒ Menu items: ", menulist_items); append_buttons(element, menulist_items, container_position); } else { continue; } } }, true); document.addEventListener("click", async function (event) { document.querySelectorAll("#quick-action-tooltip").forEach((element) => element.remove()); const button = event.target.closest(".qa-button"); if (!button) return; event.stopPropagation(); event.stopImmediatePropagation(); event.preventDefault(); const action_type = button.dataset.icon; let response; switch (action_type) { case "not_interested": response = "Not interested"; log.log("😴 Marking as not interested"); break; case "dont_recommend_channel": response = "Don't recommend channel"; log.log("🚫 Don't recommend channel"); break; case "hide": response = "Hide"; log.log("πŸ—‘οΈ Hiding video"); break; case "remove": response = "Remove from"; log.log("πŸ—‘οΈ Remove from playlist"); break; case "save": response = "Save to playlist"; log.log("πŸ“‚ Saving to playlist"); break; default: log.log("☠️ Unknown action"); } const menus = find_elem_in_parent_dom_tree( button, action_button_selectors ); if (!menus) { log.log("❌ Menu button not found."); return; } log.log("🎯 Menu button found:", menus); if (menus.id.toLowerCase() === "button") { menus.dispatchEvent(new MouseEvent("click", { bubbles: false })); } else { menus.querySelector("button").dispatchEvent(new MouseEvent("click", { bubbles: false })); } log.log("πŸ‘‡ Button clicked, waiting for menu..."); try { const visible_menu = await wait_until(() => get_visible_elem(dropdown_menu_tag_name), { interval: 100, timeout: 3000, }); if (visible_menu) { try { const target_item = await wait_until( () => { const items = visible_menu.querySelectorAll(popup_menu_items_selector); return items.length > 0 ? items : null; }, { interval: 100, timeout: 5000, }, ); if (target_item) { log.log("πŸŽ‰ Target items found:", target_item); for (const item of target_item) { if ( item.textContent === response || (response === "Remove from" && item.textContent.startsWith("Remove from")) ) { log.log(`βœ… Matched: (${response} = ${item.textContent})`); log.log(`βœ…`, item); const button = item; await retry_click(button, { max_attempts: 3, interval: 1000 }).finally(() => { document.body.click(); }); break; } else { log.log(`❌ Not a match: (${response} = ${item.textContent})`); } } } } catch (error) { log.log("πŸ›‘ !", error.message); } } } catch (error) { log.log("πŸ›‘ !!", error.message); } }); document.addEventListener("yt-action", (event) => { if (yt_update_action_names.includes(event.detail.actionName)) { log.log("πŸ› Page updated."); document.querySelectorAll("#quick-actions").forEach((element) => element.remove()); } }); enable_fallback_tooltip(".qa-button"); /* -------------------------------------------------------------------------- */ /* This script is brought to you in support of FIFTY FIFTY πŸ’– */ /* -------------------------------------------------------------------------- */ if ("πŸ’–") { const selectors_to_watch = ['a', 'yt-formatted-string', '.yt-core-attributed-string']; const observed_elements = new WeakMap(); const target_texts = [ "FIFTY FIFTY Official", "FIFTY FIFTY", "@WE_FIFTYFIFTY" ]; function has_matching_text(element) { const text = element.textContent.trim(); return target_texts.includes(text); } function observe_text_content_changes(element) { if (observed_elements.has(element)) return; const element_observer = new MutationObserver(() => { element.classList.toggle("fancy", has_matching_text(element)); }); observed_elements.set(element, element_observer); element_observer.observe(element, { characterData: true, childList: true, subtree: true }); } function handle_removed_node(node) { if (node.nodeType !== 1) return; if (observed_elements.has(node)) { observed_elements.get(node).disconnect(); observed_elements.delete(node); } node.querySelectorAll(selectors_to_watch.join(',')).forEach(child => { if (observed_elements.has(child)) { observed_elements.get(child).disconnect(); observed_elements.delete(child); } }); } function init() { document.querySelectorAll(selectors_to_watch.join(',')).forEach(element => { if (has_matching_text(element)) element.classList.add("fancy"); observe_text_content_changes(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 selectors_to_watch) { const elements = node.matches(selector) ? [node] : node.querySelectorAll(selector); elements.forEach(element => { if (has_matching_text(element)) element.classList.add("fancy"); observe_text_content_changes(element); }); } } for (const node of mutation.removedNodes) { handle_removed_node(node); } } }); observer.observe(document.body, { childList: true, subtree: true }); } if (document.body) { init(); } else { new MutationObserver((_, obs) => { if (document.body) { obs.disconnect(); init(); } }).observe(document.documentElement, { childList: true }); } } })();