Youtube Mobile-like Playlist Remove Video Button

Adds a button to remove videos from playlists just like on mobile

当前为 2023-10-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Youtube Mobile-like Playlist Remove Video Button
// @license     MIT
// @namespace   rtonne
// @match       https://www.youtube.com/*
// @grant       none
// @version     1.2
// @author      Rtonne
// @description Adds a button to remove videos from playlists just like on mobile
// @run-at      document-end
// ==/UserScript==

let currentUrl = null;

const urlRegex = /^https:\/\/www.youtube.com\/playlist\?list=.*$/;

// Using observer to run script whenever the body changes
// because youtube doesn't reload when changing page
const observer = new MutationObserver(async () => {
  try {
    let newUrl = window.location.href;

    // Because youtube doesn't reload on changing url
    // we have to allow the whole website and check here if we are in a playlist
    if (!urlRegex.test(newUrl)) {
      return;
    }
    const elements = await waitForElements(
      document,
      "ytd-playlist-video-renderer"
    );

    // If the url is different we are in a different playlist
    // Or if the playlist length is different, we loaded more of the same playlist
    if (
      currentUrl === newUrl &&
      elements.length ===
        document.querySelectorAll(".rtonne-youtube-playlist-delete-button")
          .length
    ) {
      return;
    }

    currentUrl = newUrl;

    // If the list cannot be sorted, we assume we can't remove from it either
    if (
      !document.querySelector(
        "#header-container > #filter-menu > yt-sort-filter-sub-menu-renderer"
      )
    ) {
      return;
    }

    elements.forEach((element) => {
      // Youtube reuses elements, so we check if element already has a button
      if (element.querySelector(".rtonne-youtube-playlist-delete-button"))
        return;

      // ===========
      // Now we create the button and add it to each video
      // ===========

      const elementStyle = document.defaultView.getComputedStyle(element);
      const button = document.createElement("button");
      button.className = "rtonne-youtube-playlist-delete-button";
      button.innerHTML = getYoutubeTrashSvg();
      button.style.height = elementStyle.height;
      button.style.padding = "0";
      button.style.borderRadius = `0 ${elementStyle.borderTopRightRadius} ${elementStyle.borderBottomRightRadius} 0`;
      button.style.borderWidth = "0";
      button.style.fill = "var(--yt-spec-text-primary)";
      button.onmouseover = () => {
        button.style.backgroundColor = "var(--yt-spec-static-brand-red)";
      };
      button.onmouseleave = () => {
        button.style.backgroundColor = "var(--yt-spec-additive-background)";
      };
      button.onmouseleave();

      element.onmouseover = () => {
        button.style.width = "var(--yt-icon-width)";
      };
      element.onmouseleave = () => {
        button.style.width = "0";
      };
      element.onmouseleave();
      element.appendChild(button);

      button.onclick = async () => {
        // Click the 3 dot menu button on the video
        element.querySelector('button[aria-label="Action menu"]').click();

        const popup = (
          await waitForElements(
            document,
            "tp-yt-iron-dropdown.ytd-popup-container:has(> div > ytd-menu-popup-renderer)"
          )
        )[0];

        // Set the popup opacity to 0 to hide it
        popup.style.opacity = "0";

        const removeMenuItem = (
          await waitForElements(
            popup,
            `ytd-menu-service-item-renderer:has(path[d="${getSvgPathD()}"])`
          )
        )[0];

        // Click the remove video from playlist button in the popup
        removeMenuItem.click();

        // Set the opacity back to default
        popup.style.opacity = null;
      };
    });
  } catch (err) {
    console.log(err);
  }
});
observer.observe(document.body, {
  childList: true,
  subtree: true,
});

// I couldn't check if we changed from an editable list to a non-editable list
// in the other observer, so I have this one to just do that and remove the buttons
const sortObserver = new MutationObserver(() => {
  if (!urlRegex.test(window.location.href)) {
    return;
  }
  if (
    !document.querySelector(
      "#header-container > #filter-menu > yt-sort-filter-sub-menu-renderer"
    )
  ) {
    document
      .querySelectorAll(".rtonne-youtube-playlist-delete-button")
      .forEach((element) => element.remove());
  }
});
sortObserver.observe(document.body, {
  childList: true,
  subtree: true,
});

function getYoutubeTrashSvg() {
  return `<div style="height: 24px;">
<svg enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24" focusable="false" style="pointer-events: none; display: block; width: 100%; height: 100%;">
<path d="${getSvgPathD()}"></path>
</svg></div>`;
}

// This function is separate to find the menu's remove button in the observer
function getSvgPathD() {
  return "M11 17H9V8h2v9zm4-9h-2v9h2V8zm4-4v1h-1v16H6V5H5V4h4V3h6v1h4zm-2 1H7v15h10V5z";
}

// https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
// This function is required because youtube uses too much JS
// and elements take a while to appear sometimes
function waitForElements(node, selector) {
  return new Promise((resolve) => {
    if (node.querySelector(selector)) {
      return resolve(node.querySelectorAll(selector));
    }

    const observer = new MutationObserver(() => {
      if (node.querySelector(selector)) {
        observer.disconnect();
        resolve(node.querySelectorAll(selector));
      }
    });

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