AniList Delete Button on List Items

Adds a delete button to all items on lists

  1. // ==UserScript==
  2. // @name AniList Delete Button on List Items
  3. // @license MIT
  4. // @namespace rtonne
  5. // @match https://anilist.co/*
  6. // @icon https://www.google.com/s2/favicons?sz=64&domain=anilist.co
  7. // @version 1.1
  8. // @author Rtonne
  9. // @description Adds a delete button to all items on lists
  10. // @grant GM.addStyle
  11. // @grant GM.registerMenuCommand
  12. // @grant GM.unregisterMenuCommand
  13. // @grant GM.getValue
  14. // @grant GM.setValue
  15. // ==/UserScript==
  16.  
  17. (async () => {
  18. const inplace =
  19. "autoconfirm" ===
  20. GM.registerMenuCommand(
  21. (await GM.getValue("autoconfirm", false))
  22. ? "☑ Auto-confirm Deletion"
  23. : "☐ Auto-confirm Deletion",
  24. toggleAutoConfirm,
  25. { id: "autoconfirm", autoClose: false }
  26. );
  27. async function toggleAutoConfirm() {
  28. const new_value = !(await GM.getValue("autoconfirm", false));
  29. await GM.setValue("autoconfirm", new_value);
  30. if (!inplace) {
  31. GM.unregisterMenuCommand("autoconfirm");
  32. }
  33. GM.registerMenuCommand(
  34. new_value ? "☑ Auto-confirm Deletion" : "☐ Auto-confirm Deletion",
  35. toggleAutoConfirm,
  36. { id: "autoconfirm", autoClose: false }
  37. );
  38. }
  39. })();
  40.  
  41. GM.addStyle(`
  42. .rtonne-anilist-listitem-delete-button {
  43. background: rgb(var(--color-red)) !important;
  44. }
  45. .entry-card .rtonne-anilist-listitem-delete-button {
  46. top: 42px !important;
  47. }
  48. .medialist.table.compact .entry .cover {
  49. display: flex !important;
  50. max-width: 82px !important;
  51. min-width: 82px;
  52. gap: 2px;
  53. margin-inline: -41px;
  54. }
  55. .medialist.table.compact .entry:not(:hover) .cover .image {
  56. display: none;
  57. }
  58. .medialist.table:not(.compact) .entry:hover .cover {
  59. display: flex !important;
  60. max-width: 102px !important;
  61. min-width: 102px;
  62. gap: 2px;
  63. }
  64. @media (max-width: 760px) {
  65. .medialist.table:not(.compact) .entry:hover .cover {
  66. max-width: 82px !important;
  67. min-width: 82px;
  68. }
  69. .medialist.table:not(.compact) .entry:hover {
  70. padding-left: 97px;
  71. }
  72. }
  73. body.rtonne-anilist-modal-hidden .list-editor-wrap,
  74. body.rtonne-anilist-messagebox-hidden .el-message-box {
  75. display: none !important;
  76. }
  77. `);
  78.  
  79. const trash_svg = `
  80. <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">
  81. <!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
  82. <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"/>
  83. </svg>
  84. `;
  85.  
  86. let current_url = null;
  87.  
  88. const url_regex =
  89. /^https:\/\/anilist.co\/user\/.+\/((animelist)|(mangalist))(\/.*)?$/;
  90.  
  91. // Using observer to run script whenever the body changes
  92. // because anilist doesn't reload when changing page
  93. const observer = new MutationObserver(async () => {
  94. try {
  95. let new_url = window.location.href;
  96.  
  97. // Because anilist doesn't reload on changing url
  98. // we have to allow the whole website and check here if we are in a list
  99. if (!url_regex.test(new_url)) {
  100. return;
  101. }
  102.  
  103. const elements = await waitForElements(document, ".entry, .entry-card");
  104.  
  105. // If the url is different we are in a different playlist
  106. // Or if the playlist length is different, we loaded more of the same playlist
  107. if (
  108. current_url === new_url &&
  109. elements.length ===
  110. document.querySelectorAll(".rtonne-anilist-listitem-delete-button")
  111. .length
  112. ) {
  113. return;
  114. }
  115.  
  116. current_url = new_url;
  117.  
  118. // If we have actions in the banner, it's not our list and can't edit it
  119. if (
  120. document.querySelector(".banner-content .actions").children.length > 0
  121. ) {
  122. return;
  123. }
  124.  
  125. elements.forEach((parent) => {
  126. const element = parent.querySelector(".cover");
  127. const is_card = parent.classList.contains("entry-card");
  128.  
  129. // We return if the item already has a delete button so
  130. // there isn't an infinite loop where adding a button triggers
  131. // the observer which adds more buttons
  132. if (element.querySelector(".rtonne-anilist-listitem-delete-button"))
  133. return;
  134.  
  135. const button = document.createElement("div");
  136. button.className = "rtonne-anilist-listitem-delete-button edit";
  137. button.innerHTML = trash_svg;
  138. element.querySelector(".edit").after(button);
  139.  
  140. button.onclick = async () => {
  141. const autoconfirm = await GM.getValue("autoconfirm", false);
  142. if (autoconfirm) {
  143. document.body.classList.add("rtonne-anilist-messagebox-hidden");
  144. }
  145. document.body.classList.add("rtonne-anilist-modal-hidden");
  146.  
  147. const edit_button = element.querySelector(
  148. ".edit:not(.rtonne-anilist-listitem-delete-button)"
  149. );
  150. edit_button.click();
  151.  
  152. const [dialog_delete_button] = await waitForElements(
  153. document,
  154. ".list-editor-wrap .delete-btn"
  155. );
  156. dialog_delete_button.click();
  157.  
  158. const [confirm_ok_button] = await waitForElements(
  159. document,
  160. ".el-message-box .el-button--small.el-button--primary"
  161. );
  162. // I need to wait for the confirm cancel button as well so it all load properly, somehow
  163. await waitForElements(
  164. document,
  165. ".el-message-box .el-button--small:not(.el-button--primary)"
  166. );
  167.  
  168. if (autoconfirm) {
  169. const fading_in_confirm_message_container = document.querySelector(
  170. ".el-message-box__wrapper.msgbox-fad-enter-active"
  171. );
  172. // Wait until message container finished fading in
  173. await waitForElementToBeRemovedOrHidden(
  174. fading_in_confirm_message_container
  175. );
  176.  
  177. confirm_ok_button.click();
  178.  
  179. const new_confirm_cancel_button = document.querySelector(
  180. ".list-editor-wrap .delete-btn"
  181. );
  182. await waitForElementToBeRemovedOrHidden(new_confirm_cancel_button);
  183.  
  184. document.body.classList.remove("rtonne-anilist-messagebox-hidden");
  185. document.body.classList.remove("rtonne-anilist-modal-hidden");
  186. } else {
  187. const confirm_message_container = document.querySelector(
  188. ".el-message-box__wrapper"
  189. );
  190. await waitForElementToBeRemovedOrHidden(confirm_message_container);
  191.  
  192. const dialog_close_button = document.querySelector(
  193. ".list-editor-wrap button:has(> i.el-icon-close)"
  194. );
  195. dialog_close_button.click();
  196.  
  197. document.body.classList.remove("rtonne-anilist-modal-hidden");
  198. }
  199. };
  200. });
  201. } catch (err) {
  202. console.log(err);
  203. }
  204. });
  205. observer.observe(document.body, {
  206. childList: true,
  207. subtree: true,
  208. });
  209.  
  210. // https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
  211. // This function is required because elements take a while to appear sometimes
  212. function waitForElements(node, selector) {
  213. return new Promise((resolve) => {
  214. if (node.querySelector(selector)) {
  215. return resolve(node.querySelectorAll(selector));
  216. }
  217.  
  218. const observer = new MutationObserver(() => {
  219. if (node.querySelector(selector)) {
  220. observer.disconnect();
  221. resolve(node.querySelectorAll(selector));
  222. }
  223. });
  224.  
  225. observer.observe(document.body, {
  226. childList: true,
  227. subtree: true,
  228. });
  229. });
  230. }
  231. function waitForElementToBeRemovedOrHidden(element) {
  232. return new Promise((resolve) => {
  233. if (!document.contains(element) || element.style.display === "none") {
  234. return resolve(null);
  235. }
  236.  
  237. const observer = new MutationObserver(() => {
  238. if (!document.contains(element) || element.style.display === "none") {
  239. observer.disconnect();
  240. resolve(null);
  241. }
  242. });
  243.  
  244. observer.observe(document.body, {
  245. childList: true,
  246. subtree: true,
  247. attributeOldValue: true,
  248. attributeFilter: ["style"],
  249. });
  250. });
  251. }