DART - YouTube NOT INTERESTED in 1-Click

Displays two icons on the middle-right side of each YouTube card (Home page): “Not interested” and “Don’t recommend channel.” Executes the action in a single click (opens the card menu, waits, and clicks the target item). Keeps YouTube’s default flow (does not auto-remove or auto-confirm).

// ==UserScript==
// @name            DART - YouTube NOT INTERESTED in 1-Click
// @namespace       dart/youtube/ni-drc
// @version         2025-10-28
// @description     Displays two icons on the middle-right side of each YouTube card (Home page): “Not interested” and “Don’t recommend channel.” Executes the action in a single click (opens the card menu, waits, and clicks the target item). Keeps YouTube’s default flow (does not auto-remove or auto-confirm).
// @author          DartVeiga
// @match           *://www.youtube.com/*
// @match           *://m.youtube.com/*
// @grant           none
// @run-at          document-idle
// @license         MIT
// @icon            https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// ==/UserScript==

(() => {
  'use strict';

  // ===== Visual / structural constants (same as in your first script) =====
  const HOST_CLASS    = 'dart-ni-host';
  const OVERLAY_CLASS = 'dart-ni-overlay';
  const BTN_CLASS     = 'dart-ni-btn';
  const MARK_ATTR     = 'data-dart-wired';
  const CARD_SEL = [
    'ytd-rich-item-renderer',
    'ytd-video-renderer',
    'ytd-compact-video-renderer',
    'ytd-grid-video-renderer'
  ].join(',');

  const LABEL_NI  = 'Not interested';
  const LABEL_DRC = "Don't recommend channel";

  // ===== CSS (layout and style identical to the first script) =====
  const css = `
    .${HOST_CLASS}{ position:relative; z-index:3; overflow:visible; }
    .${HOST_CLASS} .yt-lockup-view-model-wiz__content-image,
    .${HOST_CLASS} .yt-thumbnail-view-model__image { position:relative; z-index:0; }

    .${OVERLAY_CLASS}{
      position:absolute; right:10px; top:50%; transform:translateY(-50%);
      display:flex; flex-direction:column; gap:10px;
      z-index:2147483647; opacity:0; pointer-events:none;
      transition:opacity .12s ease, transform .12s ease;
    }
    .${HOST_CLASS}:hover .${OVERLAY_CLASS}, .${OVERLAY_CLASS}:hover{
      opacity:1; pointer-events:auto;
    }
    .${BTN_CLASS}{
      width:34px; height:34px; border:none; border-radius:999px;
      background:rgba(0,0,0,.82); color:#fff; cursor:pointer;
      display:grid; place-items:center; box-shadow:0 4px 14px rgba(0,0,0,.25);
      transition:transform .08s ease, filter .12s ease, opacity .12s ease;
    }
    .${BTN_CLASS}:hover{ transform:scale(1.07); filter:brightness(1.15); }
    .${BTN_CLASS}[data-busy="1"]{ opacity:.6; cursor:progress; }
    .${BTN_CLASS} svg{ width:18px; height:18px; fill:currentColor; }
  `.replace(/;/g,'!important;');
  const styleTag = document.createElement('style');
  styleTag.textContent = css;
  document.documentElement.appendChild(styleTag);

  // ===== Utility functions =====
  const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));
  const norm  = s => (s||'').trim().replace(/\s+/g,' ');

  function createSVG(d){
    const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
    svg.setAttribute('viewBox','0 0 24 24');
    const p = document.createElementNS('http://www.w3.org/2000/svg','path');
    p.setAttribute('d', d);
    svg.appendChild(p);
    return svg;
  }

  // ===== Menu logic (based on the older script, adapted to YouTube’s new DOM) =====

  // Captures the LAST contextual menu that appears after opening a card’s 3-dot menu.
  // Strategy:
  // 1) Start a short-lived MutationObserver;
  // 2) Trigger the card menu;
  // 3) Retrieve the most recent visible <yt-list-view-model>.
  async function openAndGetFreshMenu(menuBtn, openTimeout = 2500) {
    // Close any previously open menu
    document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true }));

    let lastMenu = null;
    const menus = [];
    let observer;

    observer = new MutationObserver((muts) => {
      for (const m of muts) {
        m.addedNodes.forEach((n) => {
          if (!(n instanceof Element)) return;
          if (n.matches?.('yt-list-view-model[role="listbox"], yt-list-view-model, [role="listbox"], [role="menu"]')) {
            menus.push(n);
            lastMenu = n;
          } else if (n.querySelector) {
            const found = n.querySelector('yt-list-view-model[role="listbox"], yt-list-view-model, [role="listbox"], [role="menu"]');
            if (found) {
              menus.push(found);
              lastMenu = found;
            }
          }
        });
      }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Trigger the card menu
    ['pointerdown', 'mousedown', 'mouseup', 'click'].forEach((type) => {
      menuBtn.dispatchEvent(new MouseEvent(type, { bubbles: true, composed: true, cancelable: true, button: 0 }));
    });

    const t0 = performance.now();
    let menu = null;

    // Wait for the menu (either new or reused)
    while (performance.now() - t0 < openTimeout) {
      // Detect menus added recently
      const visibleMenus = menus.filter((m) => m.offsetParent !== null);

      // Fallback: detect reused menu (already present and visible)
      const fallback = document.querySelector('yt-list-view-model[role="listbox"]:not([hidden]), .ytContextualSheetLayoutContentContainer yt-list-view-model');

      if (visibleMenus.length) {
        menu = visibleMenus[visibleMenus.length - 1];
        break;
      } else if (fallback && fallback.offsetParent !== null) {
        menu = fallback;
        break;
      }
      await new Promise((r) => setTimeout(r, 50));
    }

    observer.disconnect();
    return menu;
  }

  // Waits for the desired menu item and clicks it when found
  async function waitItemAndClick(menu, label, itemTimeout=3000){
    const t0 = performance.now();
    while (performance.now()-t0 < itemTimeout){
      const items = menu.querySelectorAll('yt-list-item-view-model[role="menuitem"], yt-list-item-view-model, [role="menuitem"], button');
      for (const el of items){
        const text = norm(el.textContent);
        if (text === label){
          (el.closest('button') || el).click();
          return true;
        }
      }
      await sleep(80);
    }
    return false;
  }

  // Locates the 3-dot “more actions” button on a card
  function findMenuBtn(card){
    return (
      card.querySelector('.yt-lockup-metadata-view-model-wiz__menu-button button') ||
      card.querySelector('button[aria-label="More actions"]') ||
      card.querySelector('button[aria-haspopup="menu"]') ||
      card.querySelector('button.yt-spec-button-shape-next')
    );
  }

  // ===== 1-click action triggered by the floating button =====
  async function runOneClick(card, kind, btnEl){
    if (!card) return;
    const menuBtn = findMenuBtn(card);
    if (!menuBtn) return console.warn('[DART] menu button not found');

    try{
      btnEl?.setAttribute('data-busy','1');

      const menu = await openAndGetFreshMenu(menuBtn);
      if (!menu) {
        console.warn('[DART] menu not found/visible');
        return;
      }

      const label = (kind === 'NI') ? LABEL_NI : LABEL_DRC;
      const ok = await waitItemAndClick(menu, label);
      if (!ok) console.warn('[DART] menu item not found:', label);

      // Close any leftover open menu
      await sleep(400);
      document.dispatchEvent(new KeyboardEvent('keydown', {key:'Escape', code:'Escape', bubbles:true}));
    } finally {
      btnEl?.removeAttribute('data-busy');
    }
  }

  // ===== UI: floating buttons overlayed on each card (same position/style as v1) =====
  function wireCard(card){
    if (!card || card.hasAttribute(MARK_ATTR)) return;

    card.classList.add(HOST_CLASS);
    card.setAttribute(MARK_ATTR,'1');

    const overlay = document.createElement('div');
    overlay.className = OVERLAY_CLASS;

    // "Not interested" button — native YouTube menu icon
    const niBtn = document.createElement('button');
    niBtn.className = BTN_CLASS;
    niBtn.title = LABEL_NI;
    niBtn.appendChild(createSVG('M12 2c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12 6.48 2 12 2zM3 12c0 2.31.87 4.41 2.29 6L18 5.29C16.41 3.87 14.31 3 12 3c-4.97 0-9 4.03-9 9zm15.71-6L6 18.71C7.59 20.13 9.69 21 12 21c4.97 0 9-4.03 9-9 0-2.31-.87-4.41-2.29-6z'));
    niBtn.addEventListener('click', e => {
      e.preventDefault(); e.stopPropagation();
      runOneClick(card,'NI',niBtn);
    }, {capture:true});

    // "Don't recommend channel" button — native YouTube menu icon
    const drcBtn = document.createElement('button');
    drcBtn.className = BTN_CLASS;
    drcBtn.title = LABEL_DRC;
    drcBtn.appendChild(createSVG('M12 3c-4.96 0-9 4.04-9 9s4.04 9 9 9 9-4.04 9-9-4.04-9-9-9m0-1c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12 6.48 2 12 2zm7 11H5v-2h14v2z'));
    drcBtn.addEventListener('click', e => {
      e.preventDefault(); e.stopPropagation();
      runOneClick(card,'DRC',drcBtn);
    }, {capture:true});

    overlay.appendChild(niBtn);
    overlay.appendChild(drcBtn);
    card.appendChild(overlay);
  }

  function inject(root=document){
    root.querySelectorAll(CARD_SEL).forEach(wireCard);
  }

  // ===== Initialization and dynamic injection =====
  inject();

  const mo = new MutationObserver(muts=>{
    for (const m of muts){
      m.addedNodes.forEach(n=>{
        if (!(n instanceof Element)) return;
        if (n.matches?.(CARD_SEL)) wireCard(n);
        else if (n.querySelector) inject(n);
      });
    }
  });
  mo.observe(document.documentElement, {childList:true, subtree:true});
  window.addEventListener('yt-navigate-finish', ()=>inject(), true);
})();