AniList Edit Multiple Media Simultaneously

Adds the ability to select multiple manga/anime in your lists and act on them simultaneously

// ==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
// ==/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." +
    "<br>ⓘ 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} <small>(Advanced Score)</small>`;
      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 += `<li>Set <u>Status</u> to <b>${status_input.value}</b>.</li>`;
        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 += `<li>Set <u>Score</u> to <b>${score_input.value}</b>.</li>`;
        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 += `<li>Set the <u>${advanced_scores[i]}</u> <u>Advanced Score</u> to <b>${advanced_scores_inputs[i].value}</b>.</li>`;
            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 += `<li>Set <u>Episode Progress</u> to <b>${progress_inputs.episode_input.value}</b>.</li>`;
          values_to_be_changed.progress = Number(
            progress_inputs.episode_input.value
          );
        }
        if (progress_inputs.rewatches_enabled_checkbox.checked) {
          action_list += `<li>Set <u>Total Rewatches</u> to <b>${progress_inputs.rewatches_input.value}</b>.</li>`;
          values_to_be_changed.repeat = Number(
            progress_inputs.rewatches_input.value
          );
        }
      } else {
        if (progress_inputs.chapter_enabled_checkbox.checked) {
          action_list += `<li>Set <u>Chapter Progress</u> to <b>${progress_inputs.chapter_input.value}</b>.</li>`;
          values_to_be_changed.progress = Number(
            progress_inputs.chapter_input.value
          );
        }
        if (progress_inputs.volume_enabled_checkbox.checked) {
          action_list += `<li>Set <u>Volume Progress</u> to <b>${progress_inputs.volume_input.value}</b>.</li>`;
          values_to_be_changed.progressVolume = Number(
            progress_inputs.volume_input.value
          );
        }
        if (progress_inputs.rereads_enabled_checkbox.checked) {
          action_list += `<li>Set <u>Total Rereads</u> to <b>${progress_inputs.rereads_input.value}</b>.</li>`;
          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 += `<li>Set <u>Start Date</u> to <b>nothing</b>.</li>`;
          values_to_be_changed.startedAt = {};
        } else {
          action_list += `<li>Set <u>Start Date</u> to <b>${start_date_input.value}</b>.</li>`;
          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 += `<li>Set <u>Finish Date</u> to <b>nothing</b>.</li>`;
          values_to_be_changed.completedAt = {};
        } else {
          action_list += `<li>Set <u>Finish Date</u> to <b>${finish_date_input.value}</b>.</li>`;
          values_to_be_changed.completedAt = date;
        }
      }
      if (notes_enabled_checkbox.checked) {
        action_list += `<li>Set <u>Notes</u> to <b>${notes_input.value}</b>.</li>`;
        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 += `<li>Add to the <b>${custom_lists[i]}</b> <u>Custom List</u>.</li>`;
              values_to_be_changed.customLists.push(custom_lists[i]);
            } else {
              action_list += `<li>Remove from the <b>${custom_lists[i]}</b> <u>Custom List</u>.</li>`;
            }
          }
        }
        if (!hide_from_status_list_checkbox.indeterminate) {
          if (hide_from_status_list_checkbox.checked) {
            action_list += `<li><u><b>Hide</b> from status lists.</u></li>`;
            values_to_be_changed.hiddenFromStatusLists = true;
          } else {
            action_list += `<li><u><b>Show</b> on status lists.</u></li>`;
            values_to_be_changed.hiddenFromStatusLists = false;
          }
        }
      }
      if (!private_checkbox.indeterminate) {
        if (private_checkbox.checked) {
          action_list += `<li>Set as <u><b>Private</b></u>.</li>`;
          values_to_be_changed.private = true;
        } else {
          action_list += `<li>Set as <u><b>Public</b></u>.</li>`;
          values_to_be_changed.private = false;
        }
      }
      if (!favourite_checkbox.indeterminate) {
        if (favourite_checkbox.checked) {
          action_list += `<li><b>Add</b> to <u>Favourites</u>.</li>`;
          values_to_be_changed.favourite = true;
        } else {
          action_list += `<li><b>Remove</b> from <u>Favourites</u>.</li>`;
          values_to_be_changed.favourite = false;
        }
      }
    } else {
      values_to_be_changed.delete = true;
      action_list += `<li><u><b>Delete</b></u>.</li>`;
    }

    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 <b><u>${
        initial_selected_entries.length
      } entr${initial_selected_entries.length > 1 ? "ies" : "y"}</u></b>:
      ${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: <b>${entry_title}</b>`,
                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 <b>${entry_title}</b>. 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"
                  }: <b>${selected_entries[i]
                    .querySelector(".title > a")
                    .innerText.trim()}</b>`,
                  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 <b>${entry_title}</b> 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: <b>${selected_entries[i]
                  .querySelector(".title > a")
                  .innerText.trim()}</b>`,
                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: <b>${selected_entries[i]
                  .querySelector(".title > a")
                  .innerText.trim()}</b>`,
                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: <b>${selected_entries[i]
                  .querySelector(".title > a")
                  .innerText.trim()}</b>`,
                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 <b>${entry_title}</b>. 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 <b><u>${selected_entries}</u></b> 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"],
  });
}