AniList Delete Button on List Items

Adds a delete button to all items on lists

Szkript telepítése?
A szerző által javasolt szkript

Ez is érdekelhet: AniList Edit Multiple Media Simultaneously

Szkript telepítése

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        AniList Delete Button on List Items
// @license     MIT
// @namespace   rtonne
// @match       https://anilist.co/*
// @icon        https://www.google.com/s2/favicons?sz=64&domain=anilist.co
// @version     1.1
// @author      Rtonne
// @description Adds a delete button to all items on lists
// @grant       GM.addStyle
// @grant       GM.registerMenuCommand
// @grant       GM.unregisterMenuCommand
// @grant       GM.getValue
// @grant       GM.setValue
// ==/UserScript==

(async () => {
  const inplace =
    "autoconfirm" ===
    GM.registerMenuCommand(
      (await GM.getValue("autoconfirm", false))
        ? "☑ Auto-confirm Deletion"
        : "☐ Auto-confirm Deletion",
      toggleAutoConfirm,
      { id: "autoconfirm", autoClose: false }
    );
  async function toggleAutoConfirm() {
    const new_value = !(await GM.getValue("autoconfirm", false));
    await GM.setValue("autoconfirm", new_value);
    if (!inplace) {
      GM.unregisterMenuCommand("autoconfirm");
    }
    GM.registerMenuCommand(
      new_value ? "☑ Auto-confirm Deletion" : "☐ Auto-confirm Deletion",
      toggleAutoConfirm,
      { id: "autoconfirm", autoClose: false }
    );
  }
})();

GM.addStyle(`
    .rtonne-anilist-listitem-delete-button {
      background: rgb(var(--color-red)) !important;
    }
    .entry-card .rtonne-anilist-listitem-delete-button {
      top: 42px !important;
    }
    .medialist.table.compact .entry .cover {
      display: flex !important;
      max-width: 82px !important;
      min-width: 82px;
      gap: 2px;
      margin-inline: -41px;
    }
    .medialist.table.compact .entry:not(:hover) .cover .image {
      display: none;
    }
    .medialist.table:not(.compact) .entry:hover .cover {
      display: flex !important;
      max-width: 102px !important;
      min-width: 102px;
      gap: 2px;
    }
    @media (max-width: 760px) {
      .medialist.table:not(.compact) .entry:hover .cover {
        max-width: 82px !important;
        min-width: 82px;
      }
      .medialist.table:not(.compact) .entry:hover {
        padding-left: 97px;
      }
    }
    body.rtonne-anilist-modal-hidden .list-editor-wrap,
    body.rtonne-anilist-messagebox-hidden .el-message-box {
      display: none !important;
    }
  `);

const trash_svg = `
    <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="ellipsis-h" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-ellipsis-h fa-w-16 fa-lg">
      <!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
      <path fill="currentColor" d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/>
    </svg>
  `;

let current_url = null;

const url_regex =
  /^https:\/\/anilist.co\/user\/.+\/((animelist)|(mangalist))(\/.*)?$/;

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

    // Because anilist doesn't reload on changing url
    // we have to allow the whole website and check here if we are in a list
    if (!url_regex.test(new_url)) {
      return;
    }

    const elements = await waitForElements(document, ".entry, .entry-card");

    // 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 (
      current_url === new_url &&
      elements.length ===
        document.querySelectorAll(".rtonne-anilist-listitem-delete-button")
          .length
    ) {
      return;
    }

    current_url = new_url;

    // If we have actions in the banner, it's not our list and can't edit it
    if (
      document.querySelector(".banner-content .actions").children.length > 0
    ) {
      return;
    }

    elements.forEach((parent) => {
      const element = parent.querySelector(".cover");
      const is_card = parent.classList.contains("entry-card");

      // We return if the item already has a delete button so
      // there isn't an infinite loop where adding a button triggers
      // the observer which adds more buttons
      if (element.querySelector(".rtonne-anilist-listitem-delete-button"))
        return;

      const button = document.createElement("div");
      button.className = "rtonne-anilist-listitem-delete-button edit";
      button.innerHTML = trash_svg;
      element.querySelector(".edit").after(button);

      button.onclick = async () => {
        const autoconfirm = await GM.getValue("autoconfirm", false);
        if (autoconfirm) {
          document.body.classList.add("rtonne-anilist-messagebox-hidden");
        }
        document.body.classList.add("rtonne-anilist-modal-hidden");

        const edit_button = element.querySelector(
          ".edit:not(.rtonne-anilist-listitem-delete-button)"
        );
        edit_button.click();

        const [dialog_delete_button] = await waitForElements(
          document,
          ".list-editor-wrap .delete-btn"
        );
        dialog_delete_button.click();

        const [confirm_ok_button] = await waitForElements(
          document,
          ".el-message-box .el-button--small.el-button--primary"
        );
        // I need to wait for the confirm cancel button as well so it all load properly, somehow
        await waitForElements(
          document,
          ".el-message-box .el-button--small:not(.el-button--primary)"
        );

        if (autoconfirm) {
          const fading_in_confirm_message_container = document.querySelector(
            ".el-message-box__wrapper.msgbox-fad-enter-active"
          );
          // Wait until message container finished fading in
          await waitForElementToBeRemovedOrHidden(
            fading_in_confirm_message_container
          );

          confirm_ok_button.click();

          const new_confirm_cancel_button = document.querySelector(
            ".list-editor-wrap .delete-btn"
          );
          await waitForElementToBeRemovedOrHidden(new_confirm_cancel_button);

          document.body.classList.remove("rtonne-anilist-messagebox-hidden");
          document.body.classList.remove("rtonne-anilist-modal-hidden");
        } else {
          const confirm_message_container = document.querySelector(
            ".el-message-box__wrapper"
          );
          await waitForElementToBeRemovedOrHidden(confirm_message_container);

          const dialog_close_button = document.querySelector(
            ".list-editor-wrap button:has(> i.el-icon-close)"
          );
          dialog_close_button.click();

          document.body.classList.remove("rtonne-anilist-modal-hidden");
        }
      };
    });
  } catch (err) {
    console.log(err);
  }
});
observer.observe(document.body, {
  childList: true,
  subtree: true,
});

// https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
// This function is required because 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,
    });
  });
}
function waitForElementToBeRemovedOrHidden(element) {
  return new Promise((resolve) => {
    if (!document.contains(element) || element.style.display === "none") {
      return resolve(null);
    }

    const observer = new MutationObserver(() => {
      if (!document.contains(element) || element.style.display === "none") {
        observer.disconnect();
        resolve(null);
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributeOldValue: true,
      attributeFilter: ["style"],
    });
  });
}