// ==UserScript== // @name AniList Edit Multiple Media Simultaneously // @license MIT // @namespace rtonne // @match https://anilist.co/* // @icon https://www.google.com/s2/favicons?sz=64&domain=anilist.co // @version 1.0 // @author Rtonne // @description Adds the ability to select multiple manga/anime in your lists and act on them simultaneously // @grant GM.getResourceText // @grant GM.addStyle // @require https://update.greasyfork.org/scripts/496874/1387742/components.js // @require https://update.greasyfork.org/scripts/496875/1387743/helpers.js // @resource GLOBAL_CSS https://raw.githubusercontent.com/p-laranjinha/userscripts/master/AniList%20Edit%20Multiple%20Media%20Simultaneously/global.css // @resource PLUS_SVG https://raw.githubusercontent.com/p-laranjinha/userscripts/master/AniList%20Edit%20Multiple%20Media%20Simultaneously/plus.svg // @resource MINUS_SVG https://raw.githubusercontent.com/p-laranjinha/userscripts/master/AniList%20Edit%20Multiple%20Media%20Simultaneously/minus.svg // @downloadURL none // ==/UserScript== // REPLACE THE @require AND @resource WITH THE FOLLOWING DURING DEVELOPMENT // AND REMEMBER TO UPDATE THE REQUIRES // @require components.js // @require helpers.js // @resource GLOBAL_CSS global.css // @resource PLUS_SVG plus.svg // @resource MINUS_SVG minus.svg const GLOBAL_CSS = GM.getResourceText("GLOBAL_CSS"); GM.addStyle(GLOBAL_CSS); const PLUS_SVG = GM.getResourceText("PLUS_SVG"); const MINUS_SVG = GM.getResourceText("MINUS_SVG"); let WAS_LAST_LIST_ANIME = false; let current_url = null; let new_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 { 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; } // If we have actions in the banner, it's not our list and can't edit it if ( (await waitForElements(".banner-content .actions"))[0].children.length > 0 ) { return; } setupButtons(); setupForm(); } catch (err) { console.error(err); } }); observer.observe(document.body, { childList: true, subtree: true, }); async function setupButtons() { const entries = await waitForElements(".entry, .entry-card"); // If the url is different we are in a different list // Or if the list length is different, we loaded more of the same list if ( current_url === new_url && entries.length === document.querySelectorAll(".rtonne-anilist-multiselect-addbutton").length ) { return; } current_url = new_url; let isCard = false; if (entries.length > 0 && entries[0].classList.contains("entry-card")) { isCard = true; } entries.forEach((entry) => { const cover = entry.querySelector(".cover"); // We return if the item already has a select button so // there isn't an infinite loop where adding a button triggers // the observer which adds more buttons if (entry.querySelector(".rtonne-anilist-multiselect-addbutton")) return; const add_button = document.createElement("div"); add_button.className = "rtonne-anilist-multiselect-addbutton edit"; add_button.innerHTML = PLUS_SVG; // I'm appending the buttons to the cards in a different place so I can have them above long titles if (isCard) { entry.append(add_button); } else { cover.querySelector(".edit").after(add_button); } const remove_button = document.createElement("div"); remove_button.className = "rtonne-anilist-multiselect-removebutton edit"; remove_button.innerHTML = MINUS_SVG; add_button.after(remove_button); add_button.onclick = () => { entry.className += " rtonne-anilist-multiselect-selected"; }; remove_button.onclick = () => { entry.classList.remove("rtonne-anilist-multiselect-selected"); }; }); } async function setupForm() { // Check if the form needs to be made/remade const [container] = await waitForElements(".filters-wrap"); const is_list_anime = document .querySelector(".nav.container > a[href$='animelist']") .classList.contains("router-link-active"); const previous_forms = document.querySelectorAll( ".rtonne-anilist-multiselect-form" ); const previous_helps = document.querySelectorAll( ".rtonne-anilist-multiselect-form-help" ); if (previous_forms.length > 0) { // In case we end up with multiple forms because of asynchronicity, remove the extra ones if (previous_forms.length > 1) { for (let i = 0; i < previous_forms.length - 1; i++) { previous_forms[i].remove(); previous_helps[i].remove(); } } // If we change from anime to manga or vice versa, redo the form if (WAS_LAST_LIST_ANIME !== is_list_anime) { for (let i = 0; i < previous_forms.length; i++) { previous_forms[i].remove(); previous_helps[i].remove(); } } else { return; } } WAS_LAST_LIST_ANIME = is_list_anime; // Choose what status and score to use in the form let status_options = [ "Reading", "Plan to read", "Completed", "Rereading", "Paused", "Dropped", ]; if (is_list_anime) { status_options = [ "Watching", "Plan to read", "Completed", "Rewatching", "Paused", "Dropped", ]; } let score_step = 1, score_max; const [element_with_score_type] = await waitForElements( ".content.container > .medialist" ); if (element_with_score_type.classList.contains("POINT_10_DECIMAL")) { score_step = 0.5; score_max = 10; } else if (element_with_score_type.classList.contains("POINT_100")) { score_max = 100; } else if (element_with_score_type.classList.contains("POINT_10")) { score_max = 10; } else if (element_with_score_type.classList.contains("POINT_5")) { score_max = 5; } else { // if (element_with_score_type.classList.contains("POINT_3")) score_max = 3; } // Create the form container let previous_form = document.querySelector( ".rtonne-anilist-multiselect-form" ); if (previous_form) { return; } const form = document.createElement("div"); form.className = "rtonne-anilist-multiselect-form"; form.style.display = "none"; container.append(form); // We get custom_lists and advanced_scores after creating the form so we can do it only once let custom_lists = []; while (true) { const first_media_id = Number( document .querySelector(".entry .title > a, .entry-card .title > a") .href.split("/")[4] ); const custom_lists_response = await getDataFromEntries( [first_media_id], "customLists" ); if (custom_lists_response.errors) { const error_message = `An error occurred while getting the available custom lists. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { document.body.className += " rtonne-anilist-multiselect-form-failed"; return; } } else { custom_lists = custom_lists_response.data[0] ? Object.keys(custom_lists_response.data[0]) : []; break; } } let advanced_scores = []; while (true) { const first_media_id = Number( document .querySelector(".entry .title > a, .entry-card .title > a") .href.split("/")[4] ); const is_advanced_scores_enabled = await isAdvancedScoringEnabled(); if (is_advanced_scores_enabled.errors) { const error_message = `An error occurred while getting if advanced scores are enabled. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { document.body.className += " rtonne-anilist-multiselect-form-failed"; return; } } else if ( (is_list_anime && is_advanced_scores_enabled.data.anime) || (!is_list_anime && is_advanced_scores_enabled.data.manga) ) { const advanced_scores_response = await getDataFromEntries( [first_media_id], "advancedScores" ); if (advanced_scores_response.errors) { const error_message = `An error occurred while getting the available advanced scores. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { document.body.className += " rtonne-anilist-multiselect-form-failed"; return; } } else { advanced_scores = advanced_scores_response.data[0] ? Object.keys(advanced_scores_response.data[0]) : []; break; } } else { break; } } // Create the form contents const help = document.createElement("div"); help.className = "rtonne-anilist-multiselect-form-help"; help.innerHTML = "ⓘ Because values can be empty, there are 2 ways to enable them. The first one is via an Enable checkbox;" + " the second one is using indeterminate checkboxes, where a dark square and strikethrough text means they're not enabled." + "
ⓘ Batch updating is done whenever possible. The following cases require individual updates:" + " choosing some but not all advanced scores; choosing one or more custom lists; adding or removing from favourites; deleting."; help.style.width = "100%"; help.style.paddingTop = "20px"; help.style.fontSize = "smaller"; help.style.display = "none"; form.after(help); const status_container = document.createElement("div"); status_container.id = "rtonne-anilist-multiselect-status-input"; status_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(status_container); const status_label = document.createElement("label"); status_label.innerText = "Status"; status_container.append(status_label); const status_enabled_checkbox = createCheckbox(status_container, "Enabled"); const status_input = createSelectInput(status_container, status_options); const score_container = document.createElement("div"); score_container.id = "rtonne-anilist-multiselect-score-input"; score_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(score_container); const score_label = document.createElement("label"); score_label.innerText = "Score"; score_container.append(score_label); const score_enabled_checkbox = createCheckbox(score_container, "Enabled"); const score_input = createNumberInput(score_container, score_max, score_step); /** @type {HTMLInputElement[]} */ let advanced_scores_enabled_checkboxes = []; /** @type {HTMLInputElement[]} */ let advanced_scores_inputs = []; if (advanced_scores.length > 0) { for (const advanced_score of advanced_scores) { const advanced_score_container = document.createElement("div"); advanced_score_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(advanced_score_container); const advanced_score_label = document.createElement("label"); advanced_score_label.innerHTML = `${advanced_score} (Advanced Score)`; advanced_score_label.style.wordBreak = "break-all"; advanced_score_container.append(advanced_score_label); advanced_scores_enabled_checkboxes.push( createCheckbox(advanced_score_container, "Enabled") ); advanced_scores_inputs.push( createNumberInput(advanced_score_container, 100, 0) ); } } /** * Collection of progress inputs. * Changes depending on if the list is for anime or manga. * @type {{ * episode_enabled_checkbox: HTMLInputElement, * episode_input: HTMLInputElement, * rewatches_enabled_checkbox: HTMLInputElement, * rewatches_input: HTMLInputElement, * } | { * chapter_enabled_checkbox: HTMLInputElement, * chapter_input: HTMLInputElement, * volume_enabled_checkbox: HTMLInputElement, * volume_input: HTMLInputElement, * rereads_enabled_checkbox: HTMLInputElement, * rereads_input: HTMLInputElement, * }} */ const progress_inputs = (() => { const result = {}; if (is_list_anime) { const episode_container = document.createElement("div"); episode_container.id = "rtonne-anilist-multiselect-episode-input"; episode_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(episode_container); const episode_label = document.createElement("label"); episode_label.innerText = "Episode Progress"; episode_container.append(episode_label); result.episode_enabled_checkbox = createCheckbox( episode_container, "Enabled" ); result.episode_input = createNumberInput(episode_container); const rewatches_container = document.createElement("div"); rewatches_container.id = "rtonne-anilist-multiselect-rewatches-input"; rewatches_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(rewatches_container); const rewatches_label = document.createElement("label"); rewatches_label.innerText = "Total Rewatches"; rewatches_container.append(rewatches_label); result.rewatches_enabled_checkbox = createCheckbox( rewatches_container, "Enabled" ); result.rewatches_input = createNumberInput(rewatches_container); } else { const chapter_container = document.createElement("div"); chapter_container.id = "rtonne-anilist-multiselect-episode-input"; chapter_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(chapter_container); const chapter_label = document.createElement("label"); chapter_label.innerText = "Chapter Progress"; chapter_container.append(chapter_label); result.chapter_enabled_checkbox = createCheckbox( chapter_container, "Enabled" ); result.chapter_input = createNumberInput(chapter_container); const volume_container = document.createElement("div"); volume_container.id = "rtonne-anilist-multiselect-episode-input"; volume_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(volume_container); const volume_label = document.createElement("label"); volume_label.innerText = "Volume Progress"; volume_container.append(volume_label); result.volume_enabled_checkbox = createCheckbox( volume_container, "Enabled" ); result.volume_input = createNumberInput(volume_container); const rereads_container = document.createElement("div"); rereads_container.id = "rtonne-anilist-multiselect-rewatches-input"; rereads_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(rereads_container); const rereads_label = document.createElement("label"); rereads_label.innerText = "Total Rereads"; rereads_container.append(rereads_label); result.rereads_enabled_checkbox = createCheckbox( rereads_container, "Enabled" ); result.rereads_input = createNumberInput(rereads_container); } return result; })(); const start_date_container = document.createElement("div"); start_date_container.id = "rtonne-anilist-multiselect-start-date-input"; start_date_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(start_date_container); const start_date_label = document.createElement("label"); start_date_label.innerText = "Start Date"; start_date_container.append(start_date_label); const start_date_enabled_checkbox = createCheckbox( start_date_container, "Enabled" ); const start_date_input = createDateInput(start_date_container); const finish_date_container = document.createElement("div"); finish_date_container.id = "rtonne-anilist-multiselect-finish-date-input"; finish_date_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(finish_date_container); const finish_date_label = document.createElement("label"); finish_date_label.innerText = "Finish Date"; finish_date_container.append(finish_date_label); const finish_date_enabled_checkbox = createCheckbox( finish_date_container, "Enabled" ); const finish_date_input = createDateInput(finish_date_container); const notes_container = document.createElement("div"); notes_container.id = "rtonne-anilist-multiselect-notes-input"; notes_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox"; form.append(notes_container); const notes_label = document.createElement("label"); notes_label.innerText = "Notes"; notes_container.append(notes_label); const notes_enabled_checkbox = createCheckbox(notes_container, "Enabled"); const notes_input = createTextarea(notes_container); /** @type {HTMLInputElement|null} */ let hide_from_status_list_checkbox; /** @type {HTMLInputElement[]} */ let custom_lists_checkboxes = []; if (custom_lists.length > 0) { const custom_lists_container = document.createElement("div"); custom_lists_container.id = "rtonne-anilist-multiselect-custom-lists-input"; form.append(custom_lists_container); const custom_lists_label = document.createElement("label"); custom_lists_label.innerText = "Custom Lists"; custom_lists_container.append(custom_lists_label); for (const custom_list of custom_lists) { custom_lists_checkboxes.push( createIndeterminateCheckbox(custom_lists_container, custom_list) ); } const custom_lists_separator = document.createElement("div"); custom_lists_separator.style.width = "100%"; custom_lists_separator.style.marginBottom = "6px"; custom_lists_separator.style.borderBottom = "solid 1px rgba(var(--color-text-lighter),.3)"; custom_lists_container.append(custom_lists_separator); hide_from_status_list_checkbox = createIndeterminateCheckbox( custom_lists_container, "Hide from status lists" ); } const other_actions_container = document.createElement("div"); other_actions_container.id = "rtonne-anilist-multiselect-other-actions-input"; form.append(other_actions_container); const other_actions_label = document.createElement("label"); other_actions_label.innerText = "Other Actions"; other_actions_container.append(other_actions_label); const private_checkbox = createIndeterminateCheckbox( other_actions_container, "Private" ); const favourite_checkbox = createIndeterminateCheckbox( other_actions_container, "Favourite" ); const delete_checkbox = createCheckbox(other_actions_container, "Delete"); const deselect_all_button = createDangerButton(form, "Deselect All Entries"); const confirm_button = createButton(form, "Confirm"); new MutationObserver(() => { if ( delete_checkbox.checked || status_enabled_checkbox.checked || (advanced_scores.length > 0 && advanced_scores_enabled_checkboxes.some((e) => e.checked)) || score_enabled_checkbox.checked || (is_list_anime && (progress_inputs.episode_enabled_checkbox.checked || progress_inputs.rewatches_enabled_checkbox.checked)) || (!is_list_anime && (progress_inputs.chapter_enabled_checkbox.checked || progress_inputs.volume_enabled_checkbox.checked || progress_inputs.rereads_enabled_checkbox.checked)) || start_date_enabled_checkbox.checked || finish_date_enabled_checkbox.checked || notes_enabled_checkbox.checked || (custom_lists.length > 0 && (custom_lists_checkboxes.some((e) => !e.indeterminate) || !hide_from_status_list_checkbox.indeterminate)) || !private_checkbox.indeterminate || !favourite_checkbox.indeterminate ) { confirm_button.style.display = "unset"; } else { confirm_button.style.display = "none"; } }).observe(form, { childList: true, subtree: true, attributeFilter: ["class"], }); const currently_selected_label = document.createElement("label"); currently_selected_label.style.alignSelf = "center"; currently_selected_label.style.color = "rgb(var(--color-blue))"; form.append(currently_selected_label); deselect_all_button.onclick = () => { const selected_entries = document.querySelectorAll( ".entry.rtonne-anilist-multiselect-selected, .entry-card.rtonne-anilist-multiselect-selected" ); for (const entry of selected_entries) { entry.classList.remove("rtonne-anilist-multiselect-selected"); } }; confirm_button.onclick = () => { let action_list = ""; let values_to_be_changed = {}; if (!delete_checkbox.checked) { if (status_enabled_checkbox.checked) { action_list += `
  • Set Status to ${status_input.value}.
  • `; switch (status_input.value) { case "Reading": case "Watching": values_to_be_changed.status = "CURRENT"; break; case "Plan to read": values_to_be_changed.status = "PLANNING"; break; case "Completed": values_to_be_changed.status = "COMPLETED"; break; case "Rereading": case "Rewatching": values_to_be_changed.status = "REPEATING"; break; case "Paused": values_to_be_changed.status = "PAUSED"; break; case "Dropped": values_to_be_changed.status = "DROPPED"; break; } } if (score_enabled_checkbox.checked) { action_list += `
  • Set Score to ${score_input.value}.
  • `; values_to_be_changed.score = Number(score_input.value); } if (advanced_scores.length > 0) { // Create array with advanced_scores.length count of null values_to_be_changed.advancedScores = Array.from( { length: advanced_scores.length }, () => null ); for (let i = 0; i < advanced_scores.length; i++) { if (advanced_scores_enabled_checkboxes[i].checked) { action_list += `
  • Set the ${advanced_scores[i]} Advanced Score to ${advanced_scores_inputs[i].value}.
  • `; values_to_be_changed.advancedScores[i] = Number( advanced_scores_inputs[i].value ); } } } if (is_list_anime) { if (progress_inputs.episode_enabled_checkbox.checked) { action_list += `
  • Set Episode Progress to ${progress_inputs.episode_input.value}.
  • `; values_to_be_changed.progress = Number( progress_inputs.episode_input.value ); } if (progress_inputs.rewatches_enabled_checkbox.checked) { action_list += `
  • Set Total Rewatches to ${progress_inputs.rewatches_input.value}.
  • `; values_to_be_changed.repeat = Number( progress_inputs.rewatches_input.value ); } } else { if (progress_inputs.chapter_enabled_checkbox.checked) { action_list += `
  • Set Chapter Progress to ${progress_inputs.chapter_input.value}.
  • `; values_to_be_changed.progress = Number( progress_inputs.chapter_input.value ); } if (progress_inputs.volume_enabled_checkbox.checked) { action_list += `
  • Set Volume Progress to ${progress_inputs.volume_input.value}.
  • `; values_to_be_changed.progressVolume = Number( progress_inputs.volume_input.value ); } if (progress_inputs.rereads_enabled_checkbox.checked) { action_list += `
  • Set Total Rereads to ${progress_inputs.rereads_input.value}.
  • `; values_to_be_changed.repeat = Number( progress_inputs.rereads_input.value ); } } if (start_date_enabled_checkbox.checked) { const date = { year: start_date_input.value.split("-")[0], month: start_date_input.value.split("-")[1], day: start_date_input.value.split("-")[2], }; if (!date.year || !date.month || !date.day) { action_list += `
  • Set Start Date to nothing.
  • `; values_to_be_changed.startedAt = {}; } else { action_list += `
  • Set Start Date to ${start_date_input.value}.
  • `; values_to_be_changed.startedAt = date; } } if (finish_date_enabled_checkbox.checked) { const date = { year: finish_date_input.value.split("-")[0], month: finish_date_input.value.split("-")[1], day: finish_date_input.value.split("-")[2], }; if (!date.year || !date.month || !date.day) { action_list += `
  • Set Finish Date to nothing.
  • `; values_to_be_changed.completedAt = {}; } else { action_list += `
  • Set Finish Date to ${finish_date_input.value}.
  • `; values_to_be_changed.completedAt = date; } } if (notes_enabled_checkbox.checked) { action_list += `
  • Set Notes to ${notes_input.value}.
  • `; values_to_be_changed.notes = notes_input.value; } if (custom_lists.length > 0) { for (let i = 0; i < custom_lists.length; i++) { if (!custom_lists_checkboxes[i].indeterminate) { if (!values_to_be_changed.customLists) { values_to_be_changed.customLists = []; } if (custom_lists_checkboxes[i].checked) { action_list += `
  • Add to the ${custom_lists[i]} Custom List.
  • `; values_to_be_changed.customLists.push(custom_lists[i]); } else { action_list += `
  • Remove from the ${custom_lists[i]} Custom List.
  • `; } } } if (!hide_from_status_list_checkbox.indeterminate) { if (hide_from_status_list_checkbox.checked) { action_list += `
  • Hide from status lists.
  • `; values_to_be_changed.hiddenFromStatusLists = true; } else { action_list += `
  • Show on status lists.
  • `; values_to_be_changed.hiddenFromStatusLists = false; } } } if (!private_checkbox.indeterminate) { if (private_checkbox.checked) { action_list += `
  • Set as Private.
  • `; values_to_be_changed.private = true; } else { action_list += `
  • Set as Public.
  • `; values_to_be_changed.private = false; } } if (!favourite_checkbox.indeterminate) { if (favourite_checkbox.checked) { action_list += `
  • Add to Favourites.
  • `; values_to_be_changed.favourite = true; } else { action_list += `
  • Remove from Favourites.
  • `; values_to_be_changed.favourite = false; } } } else { values_to_be_changed.delete = true; action_list += `
  • Delete.
  • `; } const initial_selected_entries = document.querySelectorAll( ".rtonne-anilist-multiselect-selected" ); const confirm_popup_button = createConfirmPopup( "Are you sure?", `You're about to do the following actions to ${ initial_selected_entries.length } entr${initial_selected_entries.length > 1 ? "ies" : "y"}: ${action_list}` ); confirm_popup_button.onclick = async () => { // It is possible to select the same entry more than once if they're on multiple lists // so we need to remove duplicates let { selected_entries } = Array.from(initial_selected_entries).reduce( (accumulator, currentValue) => { const url = currentValue.querySelector(".title > a").href; if (accumulator.urls.indexOf(url) < 0) { accumulator.urls.push(url); accumulator.selected_entries.push(currentValue); } return accumulator; }, { selected_entries: [], urls: [] } ); // Content is in yet another function so I can do stuff after it returns anywhere const success = await (async () => { let is_cancelled = false; const { popup_wrapper, popup_cancel_button, changePopupTitle, changePopupContent, closePopup, } = createUpdatableCancelPopup("Processing the request...", ""); popup_wrapper.onclick = popup_cancel_button.onclick = () => { is_cancelled = true; }; let media_ids = []; for (const entry of selected_entries) { const media_id = Number( entry.querySelector(".title > a").href.split("/")[4] ); media_ids.push(media_id); } let ids_response; while (true) { ids_response = await getDataFromEntries(media_ids, "id"); if (ids_response.errors) { const error_message = `${ids_response.data.length}/${selected_entries.length} IDs were successfully obtained. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } const ids = ids_response.data; if (values_to_be_changed.delete) { for (let i = 0; i < selected_entries.length && !is_cancelled; i++) { const entry_title = selected_entries[i] .querySelector(".title > a") .innerText.trim(); changePopupContent( createEntryPopupContent( `Deleting: ${entry_title}`, selected_entries[i].querySelector(".image").style .backgroundImage, i + 1, selected_entries.length ) ); while (true) { const delete_response = await deleteEntry(ids[i]); if (delete_response.errors) { const error_message = `An error occurred while deleting ${entry_title}. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } } closePopup(); return true; } if (values_to_be_changed.favourite !== undefined) { let is_favourite_response; while (true) { is_favourite_response = await getDataFromEntries( media_ids, "isFavourite" ); if (is_favourite_response.errors) { const error_message = `An error occurred while getting info to edit favourites. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } for (let i = 0; i < selected_entries.length && !is_cancelled; i++) { const entry_title = selected_entries[i] .querySelector(".title > a") .innerText.trim(); if ( values_to_be_changed.favourite !== is_favourite_response.data[i] ) { changePopupContent( createEntryPopupContent( `${ values_to_be_changed.favourite ? "Adding to favourites" : "Removing from favourites" }: ${selected_entries[i] .querySelector(".title > a") .innerText.trim()}`, selected_entries[i].querySelector(".image").style .backgroundImage, i + 1, selected_entries.length ) ); while (true) { let toggle_favourite_response; if (is_list_anime) { toggle_favourite_response = await toggleFavouriteForEntry({ animeId: media_ids[i], }); } else { toggle_favourite_response = await toggleFavouriteForEntry({ mangaId: media_ids[i], }); } if (toggle_favourite_response.errors) { const error_message = `An error occurred while ${entry_title} was being ${ values_to_be_changed.favourite ? "added to" : "removed from" } favourites. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } } } } // Adding/removing from custom lists requires more meddling. // If some but not all custom lists have been chosen further processing is required. // array.every() returns true if array is empty so we need to check that. /** @type {void | string[][]} */ let all_processed_custom_lists; if ( custom_lists_checkboxes.some((checkbox) => !checkbox.indeterminate) && !( custom_lists_checkboxes.length > 0 && custom_lists_checkboxes.every((checkbox) => !checkbox.indeterminate) ) ) { let custom_lists_response; while (true) { custom_lists_response = await getDataFromEntries( media_ids, "customLists" ); if (custom_lists_response.errors) { const error_message = `An error occurred while getting custom lists. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } all_processed_custom_lists = []; for (let i = 0; i < selected_entries.length && !is_cancelled; i++) { changePopupContent( createEntryPopupContent( `Getting the custom lists of: ${selected_entries[i] .querySelector(".title > a") .innerText.trim()}`, selected_entries[i].querySelector(".image").style .backgroundImage, i + 1, selected_entries.length ) ); let processed_custom_lists = []; let entry_custom_lists = custom_lists_response.data[i]; for (let j = 0; j < custom_lists.length; j++) { if (!custom_lists_checkboxes[j].indeterminate) { if (custom_lists_checkboxes[j].checked) { processed_custom_lists.push(custom_lists[j]); } } else { if (entry_custom_lists[custom_lists[j]]) { processed_custom_lists.push(custom_lists[j]); } } } all_processed_custom_lists.push(processed_custom_lists); } } // Using advanced scores requires more meddling. // If some but not all advanced scores have been chosen further processing is required. // array.every() returns true if array is empty so we need to check that. /** @type {void | string[][]} */ let all_processed_advanced_scores; const some_but_not_all_advanced_scores = advanced_scores_enabled_checkboxes.some( (checkbox) => checkbox.checked ) && !( advanced_scores_enabled_checkboxes.length > 0 && advanced_scores_enabled_checkboxes.every( (checkbox) => checkbox.checked ) ); if (some_but_not_all_advanced_scores) { let advanced_scores_response; while (true) { advanced_scores_response = await getDataFromEntries( media_ids, "advancedScores" ); if (advanced_scores_response.errors) { const error_message = `An error occurred while getting advanced scores. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } all_processed_advanced_scores = []; for (let i = 0; i < selected_entries.length && !is_cancelled; i++) { changePopupContent( createEntryPopupContent( `Getting the advanced scores of: ${selected_entries[i] .querySelector(".title > a") .innerText.trim()}`, selected_entries[i].querySelector(".image").style .backgroundImage, i + 1, selected_entries.length ) ); let processed_advanced_scores = []; let entry_advanced_scores = Object.values( advanced_scores_response.data[i] ); for (let j = 0; j < advanced_scores.length; j++) { if (advanced_scores_enabled_checkboxes[j].checked) { processed_advanced_scores.push( values_to_be_changed.advancedScores[j] ); } else { processed_advanced_scores.push(entry_advanced_scores[j]); } } all_processed_advanced_scores.push(processed_advanced_scores); } } // If any custom lists or some but not all advanced scores have been chosen, we require individual updates. if ( custom_lists_checkboxes.some((checkbox) => !checkbox.indeterminate) || some_but_not_all_advanced_scores ) { const values = { ...values_to_be_changed }; for (let i = 0; i < selected_entries.length && !is_cancelled; i++) { changePopupContent( createEntryPopupContent( `Updating: ${selected_entries[i] .querySelector(".title > a") .innerText.trim()}`, selected_entries[i].querySelector(".image").style .backgroundImage, i + 1, selected_entries.length ) ); while (true) { if (all_processed_custom_lists) { values.customLists = all_processed_custom_lists[i]; } if (all_processed_advanced_scores) { values.advancedScores = all_processed_advanced_scores[i]; } const update_response = await updateEntry(ids[i], values); if (update_response.errors) { const entry_title = selected_entries[i] .querySelector(".title > a") .innerText.trim(); const error_message = `An error occurred while updating ${entry_title}. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } } closePopup(); return true; } // Don't batch update if not required if ( status_enabled_checkbox.checked || score_enabled_checkbox.checked || (advanced_scores.length > 0 && advanced_scores_enabled_checkboxes.every((e) => e.checked)) || (is_list_anime && (progress_inputs.episode_enabled_checkbox.checked || progress_inputs.rewatches_enabled_checkbox.checked)) || (!is_list_anime && (progress_inputs.chapter_enabled_checkbox.checked || progress_inputs.volume_enabled_checkbox.checked || progress_inputs.rereads_enabled_checkbox.checked)) || start_date_enabled_checkbox.checked || finish_date_enabled_checkbox.checked || notes_enabled_checkbox.checked || (custom_lists.length > 0 && !hide_from_status_list_checkbox.indeterminate) || !private_checkbox.indeterminate ) { changePopupContent( "Updating all the entries at once. Not possible to cancel." ); while (true) { const batch_update_response = await batchUpdateEntries( ids, values_to_be_changed ); if (batch_update_response.errors) { const error_message = `An error occurred while batch updating. Please look at the console for more information. Do you want to retry or cancel the request?`; if (await createErrorPopup(error_message)) { closePopup(); return false; } } else { break; } } } closePopup(); return true; })(); if (success) { const finished_popup_button = createConfirmPopup( "Done!", "The request has finished. Do you want to refresh?" ); finished_popup_button.onclick = () => window.location.reload(); } }; }; new MutationObserver(() => { const selected_entries = document.querySelectorAll( ".rtonne-anilist-multiselect-selected" ).length; currently_selected_label.innerHTML = `You have ${selected_entries} entr${ selected_entries > 1 ? "ies" : "y" } selected.`; if (selected_entries > 0) { form.style.display = "flex"; help.style.display = "block"; } else { form.style.display = "none"; help.style.display = "none"; } }).observe(document.querySelector(".lists"), { childList: true, subtree: true, attributeFilter: ["class"], }); }