AniList Delete Button on List Items

Adds a delete button to all items on lists

目前为 2024-04-29 提交的版本,查看 最新版本

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