AniList Edit Multiple Media Simultaneously

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

بۇ قوليازمىنى قاچىلاش؟
ئاپتورنىڭ تەۋسىيەلىگەن قوليازمىسى

سىز بەلكىم Anime Songs - AniList Player نى ياقتۇرۇشىڭىز مۇمكىن.

بۇ قوليازمىنى قاچىلاش

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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