// ==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 // @downloadURL none // ==/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 = ` `; 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"], }); }); }