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

安装此脚本
  1. // ==UserScript==
  2. // @name AniList Edit Multiple Media Simultaneously
  3. // @license MIT
  4. // @namespace rtonne
  5. // @match https://anilist.co/*
  6. // @icon https://www.google.com/s2/favicons?sz=64&domain=anilist.co
  7. // @version 1.0
  8. // @author Rtonne
  9. // @description Adds the ability to select multiple manga/anime in your lists and act on them simultaneously
  10. // @grant GM.getResourceText
  11. // @grant GM.addStyle
  12. // @require https://update.greasyfork.org/scripts/496874/1387742/components.js
  13. // @require https://update.greasyfork.org/scripts/496875/1387743/helpers.js
  14. // @resource GLOBAL_CSS https://raw.githubusercontent.com/p-laranjinha/userscripts/master/AniList%20Edit%20Multiple%20Media%20Simultaneously/global.css
  15. // @resource PLUS_SVG https://raw.githubusercontent.com/p-laranjinha/userscripts/master/AniList%20Edit%20Multiple%20Media%20Simultaneously/plus.svg
  16. // @resource MINUS_SVG https://raw.githubusercontent.com/p-laranjinha/userscripts/master/AniList%20Edit%20Multiple%20Media%20Simultaneously/minus.svg
  17. // ==/UserScript==
  18.  
  19. // REPLACE THE @require AND @resource WITH THE FOLLOWING DURING DEVELOPMENT
  20. // AND REMEMBER TO UPDATE THE REQUIRES
  21. // @require components.js
  22. // @require helpers.js
  23. // @resource GLOBAL_CSS global.css
  24. // @resource PLUS_SVG plus.svg
  25. // @resource MINUS_SVG minus.svg
  26.  
  27. const GLOBAL_CSS = GM.getResourceText("GLOBAL_CSS");
  28. GM.addStyle(GLOBAL_CSS);
  29. const PLUS_SVG = GM.getResourceText("PLUS_SVG");
  30. const MINUS_SVG = GM.getResourceText("MINUS_SVG");
  31.  
  32. let WAS_LAST_LIST_ANIME = false;
  33.  
  34. let current_url = null;
  35. let new_url = null;
  36.  
  37. const url_regex =
  38. /^https:\/\/anilist.co\/user\/.+\/((animelist)|(mangalist))(\/.*)?$/;
  39.  
  40. // Using observer to run script whenever the body changes
  41. // because anilist doesn't reload when changing page
  42. const observer = new MutationObserver(async () => {
  43. try {
  44. new_url = window.location.href;
  45.  
  46. // Because anilist doesn't reload on changing url
  47. // we have to allow the whole website and check here if we are in a list
  48. if (!url_regex.test(new_url)) {
  49. return;
  50. }
  51.  
  52. // If we have actions in the banner, it's not our list and can't edit it
  53. if (
  54. (await waitForElements(".banner-content .actions"))[0].children.length > 0
  55. ) {
  56. return;
  57. }
  58.  
  59. setupButtons();
  60. setupForm();
  61. } catch (err) {
  62. console.error(err);
  63. }
  64. });
  65. observer.observe(document.body, {
  66. childList: true,
  67. subtree: true,
  68. });
  69.  
  70. async function setupButtons() {
  71. const entries = await waitForElements(".entry, .entry-card");
  72.  
  73. // If the url is different we are in a different list
  74. // Or if the list length is different, we loaded more of the same list
  75. if (
  76. current_url === new_url &&
  77. entries.length ===
  78. document.querySelectorAll(".rtonne-anilist-multiselect-addbutton").length
  79. ) {
  80. return;
  81. }
  82.  
  83. current_url = new_url;
  84.  
  85. let isCard = false;
  86. if (entries.length > 0 && entries[0].classList.contains("entry-card")) {
  87. isCard = true;
  88. }
  89. entries.forEach((entry) => {
  90. const cover = entry.querySelector(".cover");
  91.  
  92. // We return if the item already has a select button so
  93. // there isn't an infinite loop where adding a button triggers
  94. // the observer which adds more buttons
  95. if (entry.querySelector(".rtonne-anilist-multiselect-addbutton")) return;
  96.  
  97. const add_button = document.createElement("div");
  98. add_button.className = "rtonne-anilist-multiselect-addbutton edit";
  99. add_button.innerHTML = PLUS_SVG;
  100. // I'm appending the buttons to the cards in a different place so I can have them above long titles
  101. if (isCard) {
  102. entry.append(add_button);
  103. } else {
  104. cover.querySelector(".edit").after(add_button);
  105. }
  106. const remove_button = document.createElement("div");
  107. remove_button.className = "rtonne-anilist-multiselect-removebutton edit";
  108. remove_button.innerHTML = MINUS_SVG;
  109. add_button.after(remove_button);
  110.  
  111. add_button.onclick = () => {
  112. entry.className += " rtonne-anilist-multiselect-selected";
  113. };
  114.  
  115. remove_button.onclick = () => {
  116. entry.classList.remove("rtonne-anilist-multiselect-selected");
  117. };
  118. });
  119. }
  120.  
  121. async function setupForm() {
  122. // Check if the form needs to be made/remade
  123. const [container] = await waitForElements(".filters-wrap");
  124. const is_list_anime = document
  125. .querySelector(".nav.container > a[href$='animelist']")
  126. .classList.contains("router-link-active");
  127. const previous_forms = document.querySelectorAll(
  128. ".rtonne-anilist-multiselect-form"
  129. );
  130. const previous_helps = document.querySelectorAll(
  131. ".rtonne-anilist-multiselect-form-help"
  132. );
  133. if (previous_forms.length > 0) {
  134. // In case we end up with multiple forms because of asynchronicity, remove the extra ones
  135. if (previous_forms.length > 1) {
  136. for (let i = 0; i < previous_forms.length - 1; i++) {
  137. previous_forms[i].remove();
  138. previous_helps[i].remove();
  139. }
  140. }
  141. // If we change from anime to manga or vice versa, redo the form
  142. if (WAS_LAST_LIST_ANIME !== is_list_anime) {
  143. for (let i = 0; i < previous_forms.length; i++) {
  144. previous_forms[i].remove();
  145. previous_helps[i].remove();
  146. }
  147. } else {
  148. return;
  149. }
  150. }
  151. WAS_LAST_LIST_ANIME = is_list_anime;
  152.  
  153. // Choose what status and score to use in the form
  154. let status_options = [
  155. "Reading",
  156. "Plan to read",
  157. "Completed",
  158. "Rereading",
  159. "Paused",
  160. "Dropped",
  161. ];
  162. if (is_list_anime) {
  163. status_options = [
  164. "Watching",
  165. "Plan to read",
  166. "Completed",
  167. "Rewatching",
  168. "Paused",
  169. "Dropped",
  170. ];
  171. }
  172.  
  173. let score_step = 1,
  174. score_max;
  175. const [element_with_score_type] = await waitForElements(
  176. ".content.container > .medialist"
  177. );
  178. if (element_with_score_type.classList.contains("POINT_10_DECIMAL")) {
  179. score_step = 0.5;
  180. score_max = 10;
  181. } else if (element_with_score_type.classList.contains("POINT_100")) {
  182. score_max = 100;
  183. } else if (element_with_score_type.classList.contains("POINT_10")) {
  184. score_max = 10;
  185. } else if (element_with_score_type.classList.contains("POINT_5")) {
  186. score_max = 5;
  187. } else {
  188. // if (element_with_score_type.classList.contains("POINT_3"))
  189. score_max = 3;
  190. }
  191.  
  192. // Create the form container
  193. let previous_form = document.querySelector(
  194. ".rtonne-anilist-multiselect-form"
  195. );
  196. if (previous_form) {
  197. return;
  198. }
  199. const form = document.createElement("div");
  200. form.className = "rtonne-anilist-multiselect-form";
  201. form.style.display = "none";
  202. container.append(form);
  203.  
  204. // We get custom_lists and advanced_scores after creating the form so we can do it only once
  205. let custom_lists = [];
  206. while (true) {
  207. const first_media_id = Number(
  208. document
  209. .querySelector(".entry .title > a, .entry-card .title > a")
  210. .href.split("/")[4]
  211. );
  212. const custom_lists_response = await getDataFromEntries(
  213. [first_media_id],
  214. "customLists"
  215. );
  216. if (custom_lists_response.errors) {
  217. 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?`;
  218. if (await createErrorPopup(error_message)) {
  219. document.body.className += " rtonne-anilist-multiselect-form-failed";
  220. return;
  221. }
  222. } else {
  223. custom_lists = custom_lists_response.data[0]
  224. ? Object.keys(custom_lists_response.data[0])
  225. : [];
  226. break;
  227. }
  228. }
  229. let advanced_scores = [];
  230. while (true) {
  231. const first_media_id = Number(
  232. document
  233. .querySelector(".entry .title > a, .entry-card .title > a")
  234. .href.split("/")[4]
  235. );
  236. const is_advanced_scores_enabled = await isAdvancedScoringEnabled();
  237. if (is_advanced_scores_enabled.errors) {
  238. 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?`;
  239. if (await createErrorPopup(error_message)) {
  240. document.body.className += " rtonne-anilist-multiselect-form-failed";
  241. return;
  242. }
  243. } else if (
  244. (is_list_anime && is_advanced_scores_enabled.data.anime) ||
  245. (!is_list_anime && is_advanced_scores_enabled.data.manga)
  246. ) {
  247. const advanced_scores_response = await getDataFromEntries(
  248. [first_media_id],
  249. "advancedScores"
  250. );
  251. if (advanced_scores_response.errors) {
  252. 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?`;
  253. if (await createErrorPopup(error_message)) {
  254. document.body.className += " rtonne-anilist-multiselect-form-failed";
  255. return;
  256. }
  257. } else {
  258. advanced_scores = advanced_scores_response.data[0]
  259. ? Object.keys(advanced_scores_response.data[0])
  260. : [];
  261. break;
  262. }
  263. } else {
  264. break;
  265. }
  266. }
  267.  
  268. // Create the form contents
  269. const help = document.createElement("div");
  270. help.className = "rtonne-anilist-multiselect-form-help";
  271. help.innerHTML =
  272. "ⓘ Because values can be empty, there are 2 ways to enable them. The first one is via an Enable checkbox;" +
  273. " the second one is using indeterminate checkboxes, where a dark square and strikethrough text means they're not enabled." +
  274. "<br>ⓘ Batch updating is done whenever possible. The following cases require individual updates:" +
  275. " choosing some but not all advanced scores; choosing one or more custom lists; adding or removing from favourites; deleting.";
  276. help.style.width = "100%";
  277. help.style.paddingTop = "20px";
  278. help.style.fontSize = "smaller";
  279. help.style.display = "none";
  280. form.after(help);
  281.  
  282. const status_container = document.createElement("div");
  283. status_container.id = "rtonne-anilist-multiselect-status-input";
  284. status_container.className =
  285. "rtonne-anilist-multiselect-has-enabled-checkbox";
  286. form.append(status_container);
  287. const status_label = document.createElement("label");
  288. status_label.innerText = "Status";
  289. status_container.append(status_label);
  290. const status_enabled_checkbox = createCheckbox(status_container, "Enabled");
  291. const status_input = createSelectInput(status_container, status_options);
  292.  
  293. const score_container = document.createElement("div");
  294. score_container.id = "rtonne-anilist-multiselect-score-input";
  295. score_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox";
  296. form.append(score_container);
  297. const score_label = document.createElement("label");
  298. score_label.innerText = "Score";
  299. score_container.append(score_label);
  300. const score_enabled_checkbox = createCheckbox(score_container, "Enabled");
  301. const score_input = createNumberInput(score_container, score_max, score_step);
  302.  
  303. /** @type {HTMLInputElement[]} */
  304. let advanced_scores_enabled_checkboxes = [];
  305. /** @type {HTMLInputElement[]} */
  306. let advanced_scores_inputs = [];
  307. if (advanced_scores.length > 0) {
  308. for (const advanced_score of advanced_scores) {
  309. const advanced_score_container = document.createElement("div");
  310. advanced_score_container.className =
  311. "rtonne-anilist-multiselect-has-enabled-checkbox";
  312. form.append(advanced_score_container);
  313. const advanced_score_label = document.createElement("label");
  314. advanced_score_label.innerHTML = `${advanced_score} <small>(Advanced Score)</small>`;
  315. advanced_score_label.style.wordBreak = "break-all";
  316. advanced_score_container.append(advanced_score_label);
  317. advanced_scores_enabled_checkboxes.push(
  318. createCheckbox(advanced_score_container, "Enabled")
  319. );
  320. advanced_scores_inputs.push(
  321. createNumberInput(advanced_score_container, 100, 0)
  322. );
  323. }
  324. }
  325.  
  326. /**
  327. * Collection of progress inputs.
  328. * Changes depending on if the list is for anime or manga.
  329. * @type {{
  330. * episode_enabled_checkbox: HTMLInputElement,
  331. * episode_input: HTMLInputElement,
  332. * rewatches_enabled_checkbox: HTMLInputElement,
  333. * rewatches_input: HTMLInputElement,
  334. * } | {
  335. * chapter_enabled_checkbox: HTMLInputElement,
  336. * chapter_input: HTMLInputElement,
  337. * volume_enabled_checkbox: HTMLInputElement,
  338. * volume_input: HTMLInputElement,
  339. * rereads_enabled_checkbox: HTMLInputElement,
  340. * rereads_input: HTMLInputElement,
  341. * }}
  342. */
  343. const progress_inputs = (() => {
  344. const result = {};
  345. if (is_list_anime) {
  346. const episode_container = document.createElement("div");
  347. episode_container.id = "rtonne-anilist-multiselect-episode-input";
  348. episode_container.className =
  349. "rtonne-anilist-multiselect-has-enabled-checkbox";
  350. form.append(episode_container);
  351. const episode_label = document.createElement("label");
  352. episode_label.innerText = "Episode Progress";
  353. episode_container.append(episode_label);
  354. result.episode_enabled_checkbox = createCheckbox(
  355. episode_container,
  356. "Enabled"
  357. );
  358. result.episode_input = createNumberInput(episode_container);
  359.  
  360. const rewatches_container = document.createElement("div");
  361. rewatches_container.id = "rtonne-anilist-multiselect-rewatches-input";
  362. rewatches_container.className =
  363. "rtonne-anilist-multiselect-has-enabled-checkbox";
  364. form.append(rewatches_container);
  365. const rewatches_label = document.createElement("label");
  366. rewatches_label.innerText = "Total Rewatches";
  367. rewatches_container.append(rewatches_label);
  368. result.rewatches_enabled_checkbox = createCheckbox(
  369. rewatches_container,
  370. "Enabled"
  371. );
  372. result.rewatches_input = createNumberInput(rewatches_container);
  373. } else {
  374. const chapter_container = document.createElement("div");
  375. chapter_container.id = "rtonne-anilist-multiselect-episode-input";
  376. chapter_container.className =
  377. "rtonne-anilist-multiselect-has-enabled-checkbox";
  378. form.append(chapter_container);
  379. const chapter_label = document.createElement("label");
  380. chapter_label.innerText = "Chapter Progress";
  381. chapter_container.append(chapter_label);
  382. result.chapter_enabled_checkbox = createCheckbox(
  383. chapter_container,
  384. "Enabled"
  385. );
  386. result.chapter_input = createNumberInput(chapter_container);
  387.  
  388. const volume_container = document.createElement("div");
  389. volume_container.id = "rtonne-anilist-multiselect-episode-input";
  390. volume_container.className =
  391. "rtonne-anilist-multiselect-has-enabled-checkbox";
  392. form.append(volume_container);
  393. const volume_label = document.createElement("label");
  394. volume_label.innerText = "Volume Progress";
  395. volume_container.append(volume_label);
  396. result.volume_enabled_checkbox = createCheckbox(
  397. volume_container,
  398. "Enabled"
  399. );
  400. result.volume_input = createNumberInput(volume_container);
  401.  
  402. const rereads_container = document.createElement("div");
  403. rereads_container.id = "rtonne-anilist-multiselect-rewatches-input";
  404. rereads_container.className =
  405. "rtonne-anilist-multiselect-has-enabled-checkbox";
  406. form.append(rereads_container);
  407. const rereads_label = document.createElement("label");
  408. rereads_label.innerText = "Total Rereads";
  409. rereads_container.append(rereads_label);
  410. result.rereads_enabled_checkbox = createCheckbox(
  411. rereads_container,
  412. "Enabled"
  413. );
  414. result.rereads_input = createNumberInput(rereads_container);
  415. }
  416. return result;
  417. })();
  418.  
  419. const start_date_container = document.createElement("div");
  420. start_date_container.id = "rtonne-anilist-multiselect-start-date-input";
  421. start_date_container.className =
  422. "rtonne-anilist-multiselect-has-enabled-checkbox";
  423. form.append(start_date_container);
  424. const start_date_label = document.createElement("label");
  425. start_date_label.innerText = "Start Date";
  426. start_date_container.append(start_date_label);
  427. const start_date_enabled_checkbox = createCheckbox(
  428. start_date_container,
  429. "Enabled"
  430. );
  431. const start_date_input = createDateInput(start_date_container);
  432.  
  433. const finish_date_container = document.createElement("div");
  434. finish_date_container.id = "rtonne-anilist-multiselect-finish-date-input";
  435. finish_date_container.className =
  436. "rtonne-anilist-multiselect-has-enabled-checkbox";
  437. form.append(finish_date_container);
  438. const finish_date_label = document.createElement("label");
  439. finish_date_label.innerText = "Finish Date";
  440. finish_date_container.append(finish_date_label);
  441. const finish_date_enabled_checkbox = createCheckbox(
  442. finish_date_container,
  443. "Enabled"
  444. );
  445. const finish_date_input = createDateInput(finish_date_container);
  446.  
  447. const notes_container = document.createElement("div");
  448. notes_container.id = "rtonne-anilist-multiselect-notes-input";
  449. notes_container.className = "rtonne-anilist-multiselect-has-enabled-checkbox";
  450. form.append(notes_container);
  451. const notes_label = document.createElement("label");
  452. notes_label.innerText = "Notes";
  453. notes_container.append(notes_label);
  454. const notes_enabled_checkbox = createCheckbox(notes_container, "Enabled");
  455. const notes_input = createTextarea(notes_container);
  456.  
  457. /** @type {HTMLInputElement|null} */
  458. let hide_from_status_list_checkbox;
  459. /** @type {HTMLInputElement[]} */
  460. let custom_lists_checkboxes = [];
  461. if (custom_lists.length > 0) {
  462. const custom_lists_container = document.createElement("div");
  463. custom_lists_container.id = "rtonne-anilist-multiselect-custom-lists-input";
  464. form.append(custom_lists_container);
  465. const custom_lists_label = document.createElement("label");
  466. custom_lists_label.innerText = "Custom Lists";
  467. custom_lists_container.append(custom_lists_label);
  468.  
  469. for (const custom_list of custom_lists) {
  470. custom_lists_checkboxes.push(
  471. createIndeterminateCheckbox(custom_lists_container, custom_list)
  472. );
  473. }
  474.  
  475. const custom_lists_separator = document.createElement("div");
  476. custom_lists_separator.style.width = "100%";
  477. custom_lists_separator.style.marginBottom = "6px";
  478. custom_lists_separator.style.borderBottom =
  479. "solid 1px rgba(var(--color-text-lighter),.3)";
  480. custom_lists_container.append(custom_lists_separator);
  481. hide_from_status_list_checkbox = createIndeterminateCheckbox(
  482. custom_lists_container,
  483. "Hide from status lists"
  484. );
  485. }
  486.  
  487. const other_actions_container = document.createElement("div");
  488. other_actions_container.id = "rtonne-anilist-multiselect-other-actions-input";
  489. form.append(other_actions_container);
  490. const other_actions_label = document.createElement("label");
  491. other_actions_label.innerText = "Other Actions";
  492. other_actions_container.append(other_actions_label);
  493. const private_checkbox = createIndeterminateCheckbox(
  494. other_actions_container,
  495. "Private"
  496. );
  497. const favourite_checkbox = createIndeterminateCheckbox(
  498. other_actions_container,
  499. "Favourite"
  500. );
  501. const delete_checkbox = createCheckbox(other_actions_container, "Delete");
  502.  
  503. const deselect_all_button = createDangerButton(form, "Deselect All Entries");
  504.  
  505. const confirm_button = createButton(form, "Confirm");
  506. new MutationObserver(() => {
  507. if (
  508. delete_checkbox.checked ||
  509. status_enabled_checkbox.checked ||
  510. (advanced_scores.length > 0 &&
  511. advanced_scores_enabled_checkboxes.some((e) => e.checked)) ||
  512. score_enabled_checkbox.checked ||
  513. (is_list_anime &&
  514. (progress_inputs.episode_enabled_checkbox.checked ||
  515. progress_inputs.rewatches_enabled_checkbox.checked)) ||
  516. (!is_list_anime &&
  517. (progress_inputs.chapter_enabled_checkbox.checked ||
  518. progress_inputs.volume_enabled_checkbox.checked ||
  519. progress_inputs.rereads_enabled_checkbox.checked)) ||
  520. start_date_enabled_checkbox.checked ||
  521. finish_date_enabled_checkbox.checked ||
  522. notes_enabled_checkbox.checked ||
  523. (custom_lists.length > 0 &&
  524. (custom_lists_checkboxes.some((e) => !e.indeterminate) ||
  525. !hide_from_status_list_checkbox.indeterminate)) ||
  526. !private_checkbox.indeterminate ||
  527. !favourite_checkbox.indeterminate
  528. ) {
  529. confirm_button.style.display = "unset";
  530. } else {
  531. confirm_button.style.display = "none";
  532. }
  533. }).observe(form, {
  534. childList: true,
  535. subtree: true,
  536. attributeFilter: ["class"],
  537. });
  538.  
  539. const currently_selected_label = document.createElement("label");
  540. currently_selected_label.style.alignSelf = "center";
  541. currently_selected_label.style.color = "rgb(var(--color-blue))";
  542. form.append(currently_selected_label);
  543.  
  544. deselect_all_button.onclick = () => {
  545. const selected_entries = document.querySelectorAll(
  546. ".entry.rtonne-anilist-multiselect-selected, .entry-card.rtonne-anilist-multiselect-selected"
  547. );
  548. for (const entry of selected_entries) {
  549. entry.classList.remove("rtonne-anilist-multiselect-selected");
  550. }
  551. };
  552.  
  553. confirm_button.onclick = () => {
  554. let action_list = "";
  555. let values_to_be_changed = {};
  556. if (!delete_checkbox.checked) {
  557. if (status_enabled_checkbox.checked) {
  558. action_list += `<li>Set <u>Status</u> to <b>${status_input.value}</b>.</li>`;
  559. switch (status_input.value) {
  560. case "Reading":
  561. case "Watching":
  562. values_to_be_changed.status = "CURRENT";
  563. break;
  564. case "Plan to read":
  565. values_to_be_changed.status = "PLANNING";
  566. break;
  567. case "Completed":
  568. values_to_be_changed.status = "COMPLETED";
  569. break;
  570. case "Rereading":
  571. case "Rewatching":
  572. values_to_be_changed.status = "REPEATING";
  573. break;
  574. case "Paused":
  575. values_to_be_changed.status = "PAUSED";
  576. break;
  577. case "Dropped":
  578. values_to_be_changed.status = "DROPPED";
  579. break;
  580. }
  581. }
  582. if (score_enabled_checkbox.checked) {
  583. action_list += `<li>Set <u>Score</u> to <b>${score_input.value}</b>.</li>`;
  584. values_to_be_changed.score = Number(score_input.value);
  585. }
  586. if (advanced_scores.length > 0) {
  587. // Create array with advanced_scores.length count of null
  588. values_to_be_changed.advancedScores = Array.from(
  589. { length: advanced_scores.length },
  590. () => null
  591. );
  592. for (let i = 0; i < advanced_scores.length; i++) {
  593. if (advanced_scores_enabled_checkboxes[i].checked) {
  594. action_list += `<li>Set the <u>${advanced_scores[i]}</u> <u>Advanced Score</u> to <b>${advanced_scores_inputs[i].value}</b>.</li>`;
  595. values_to_be_changed.advancedScores[i] = Number(
  596. advanced_scores_inputs[i].value
  597. );
  598. }
  599. }
  600. }
  601. if (is_list_anime) {
  602. if (progress_inputs.episode_enabled_checkbox.checked) {
  603. action_list += `<li>Set <u>Episode Progress</u> to <b>${progress_inputs.episode_input.value}</b>.</li>`;
  604. values_to_be_changed.progress = Number(
  605. progress_inputs.episode_input.value
  606. );
  607. }
  608. if (progress_inputs.rewatches_enabled_checkbox.checked) {
  609. action_list += `<li>Set <u>Total Rewatches</u> to <b>${progress_inputs.rewatches_input.value}</b>.</li>`;
  610. values_to_be_changed.repeat = Number(
  611. progress_inputs.rewatches_input.value
  612. );
  613. }
  614. } else {
  615. if (progress_inputs.chapter_enabled_checkbox.checked) {
  616. action_list += `<li>Set <u>Chapter Progress</u> to <b>${progress_inputs.chapter_input.value}</b>.</li>`;
  617. values_to_be_changed.progress = Number(
  618. progress_inputs.chapter_input.value
  619. );
  620. }
  621. if (progress_inputs.volume_enabled_checkbox.checked) {
  622. action_list += `<li>Set <u>Volume Progress</u> to <b>${progress_inputs.volume_input.value}</b>.</li>`;
  623. values_to_be_changed.progressVolume = Number(
  624. progress_inputs.volume_input.value
  625. );
  626. }
  627. if (progress_inputs.rereads_enabled_checkbox.checked) {
  628. action_list += `<li>Set <u>Total Rereads</u> to <b>${progress_inputs.rereads_input.value}</b>.</li>`;
  629. values_to_be_changed.repeat = Number(
  630. progress_inputs.rereads_input.value
  631. );
  632. }
  633. }
  634. if (start_date_enabled_checkbox.checked) {
  635. const date = {
  636. year: start_date_input.value.split("-")[0],
  637. month: start_date_input.value.split("-")[1],
  638. day: start_date_input.value.split("-")[2],
  639. };
  640.  
  641. if (!date.year || !date.month || !date.day) {
  642. action_list += `<li>Set <u>Start Date</u> to <b>nothing</b>.</li>`;
  643. values_to_be_changed.startedAt = {};
  644. } else {
  645. action_list += `<li>Set <u>Start Date</u> to <b>${start_date_input.value}</b>.</li>`;
  646. values_to_be_changed.startedAt = date;
  647. }
  648. }
  649. if (finish_date_enabled_checkbox.checked) {
  650. const date = {
  651. year: finish_date_input.value.split("-")[0],
  652. month: finish_date_input.value.split("-")[1],
  653. day: finish_date_input.value.split("-")[2],
  654. };
  655.  
  656. if (!date.year || !date.month || !date.day) {
  657. action_list += `<li>Set <u>Finish Date</u> to <b>nothing</b>.</li>`;
  658. values_to_be_changed.completedAt = {};
  659. } else {
  660. action_list += `<li>Set <u>Finish Date</u> to <b>${finish_date_input.value}</b>.</li>`;
  661. values_to_be_changed.completedAt = date;
  662. }
  663. }
  664. if (notes_enabled_checkbox.checked) {
  665. action_list += `<li>Set <u>Notes</u> to <b>${notes_input.value}</b>.</li>`;
  666. values_to_be_changed.notes = notes_input.value;
  667. }
  668. if (custom_lists.length > 0) {
  669. for (let i = 0; i < custom_lists.length; i++) {
  670. if (!custom_lists_checkboxes[i].indeterminate) {
  671. if (!values_to_be_changed.customLists) {
  672. values_to_be_changed.customLists = [];
  673. }
  674. if (custom_lists_checkboxes[i].checked) {
  675. action_list += `<li>Add to the <b>${custom_lists[i]}</b> <u>Custom List</u>.</li>`;
  676. values_to_be_changed.customLists.push(custom_lists[i]);
  677. } else {
  678. action_list += `<li>Remove from the <b>${custom_lists[i]}</b> <u>Custom List</u>.</li>`;
  679. }
  680. }
  681. }
  682. if (!hide_from_status_list_checkbox.indeterminate) {
  683. if (hide_from_status_list_checkbox.checked) {
  684. action_list += `<li><u><b>Hide</b> from status lists.</u></li>`;
  685. values_to_be_changed.hiddenFromStatusLists = true;
  686. } else {
  687. action_list += `<li><u><b>Show</b> on status lists.</u></li>`;
  688. values_to_be_changed.hiddenFromStatusLists = false;
  689. }
  690. }
  691. }
  692. if (!private_checkbox.indeterminate) {
  693. if (private_checkbox.checked) {
  694. action_list += `<li>Set as <u><b>Private</b></u>.</li>`;
  695. values_to_be_changed.private = true;
  696. } else {
  697. action_list += `<li>Set as <u><b>Public</b></u>.</li>`;
  698. values_to_be_changed.private = false;
  699. }
  700. }
  701. if (!favourite_checkbox.indeterminate) {
  702. if (favourite_checkbox.checked) {
  703. action_list += `<li><b>Add</b> to <u>Favourites</u>.</li>`;
  704. values_to_be_changed.favourite = true;
  705. } else {
  706. action_list += `<li><b>Remove</b> from <u>Favourites</u>.</li>`;
  707. values_to_be_changed.favourite = false;
  708. }
  709. }
  710. } else {
  711. values_to_be_changed.delete = true;
  712. action_list += `<li><u><b>Delete</b></u>.</li>`;
  713. }
  714.  
  715. const initial_selected_entries = document.querySelectorAll(
  716. ".rtonne-anilist-multiselect-selected"
  717. );
  718. const confirm_popup_button = createConfirmPopup(
  719. "Are you sure?",
  720. `You're about to do the following actions to <b><u>${
  721. initial_selected_entries.length
  722. } entr${initial_selected_entries.length > 1 ? "ies" : "y"}</u></b>:
  723. ${action_list}`
  724. );
  725.  
  726. confirm_popup_button.onclick = async () => {
  727. // It is possible to select the same entry more than once if they're on multiple lists
  728. // so we need to remove duplicates
  729. let { selected_entries } = Array.from(initial_selected_entries).reduce(
  730. (accumulator, currentValue) => {
  731. const url = currentValue.querySelector(".title > a").href;
  732. if (accumulator.urls.indexOf(url) < 0) {
  733. accumulator.urls.push(url);
  734. accumulator.selected_entries.push(currentValue);
  735. }
  736. return accumulator;
  737. },
  738. { selected_entries: [], urls: [] }
  739. );
  740.  
  741. // Content is in yet another function so I can do stuff after it returns anywhere
  742. const success = await (async () => {
  743. let is_cancelled = false;
  744. const {
  745. popup_wrapper,
  746. popup_cancel_button,
  747. changePopupTitle,
  748. changePopupContent,
  749. closePopup,
  750. } = createUpdatableCancelPopup("Processing the request...", "");
  751. popup_wrapper.onclick = popup_cancel_button.onclick = () => {
  752. is_cancelled = true;
  753. };
  754.  
  755. let media_ids = [];
  756. for (const entry of selected_entries) {
  757. const media_id = Number(
  758. entry.querySelector(".title > a").href.split("/")[4]
  759. );
  760. media_ids.push(media_id);
  761. }
  762. let ids_response;
  763. while (true) {
  764. ids_response = await getDataFromEntries(media_ids, "id");
  765. if (ids_response.errors) {
  766. 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?`;
  767. if (await createErrorPopup(error_message)) {
  768. closePopup();
  769. return false;
  770. }
  771. } else {
  772. break;
  773. }
  774. }
  775. const ids = ids_response.data;
  776.  
  777. if (values_to_be_changed.delete) {
  778. for (let i = 0; i < selected_entries.length && !is_cancelled; i++) {
  779. const entry_title = selected_entries[i]
  780. .querySelector(".title > a")
  781. .innerText.trim();
  782. changePopupContent(
  783. createEntryPopupContent(
  784. `Deleting: <b>${entry_title}</b>`,
  785. selected_entries[i].querySelector(".image").style
  786. .backgroundImage,
  787. i + 1,
  788. selected_entries.length
  789. )
  790. );
  791. while (true) {
  792. const delete_response = await deleteEntry(ids[i]);
  793. if (delete_response.errors) {
  794. 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?`;
  795. if (await createErrorPopup(error_message)) {
  796. closePopup();
  797. return false;
  798. }
  799. } else {
  800. break;
  801. }
  802. }
  803. }
  804. closePopup();
  805. return true;
  806. }
  807. if (values_to_be_changed.favourite !== undefined) {
  808. let is_favourite_response;
  809. while (true) {
  810. is_favourite_response = await getDataFromEntries(
  811. media_ids,
  812. "isFavourite"
  813. );
  814. if (is_favourite_response.errors) {
  815. 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?`;
  816. if (await createErrorPopup(error_message)) {
  817. closePopup();
  818. return false;
  819. }
  820. } else {
  821. break;
  822. }
  823. }
  824. for (let i = 0; i < selected_entries.length && !is_cancelled; i++) {
  825. const entry_title = selected_entries[i]
  826. .querySelector(".title > a")
  827. .innerText.trim();
  828. if (
  829. values_to_be_changed.favourite !== is_favourite_response.data[i]
  830. ) {
  831. changePopupContent(
  832. createEntryPopupContent(
  833. `${
  834. values_to_be_changed.favourite
  835. ? "Adding to favourites"
  836. : "Removing from favourites"
  837. }: <b>${selected_entries[i]
  838. .querySelector(".title > a")
  839. .innerText.trim()}</b>`,
  840. selected_entries[i].querySelector(".image").style
  841. .backgroundImage,
  842. i + 1,
  843. selected_entries.length
  844. )
  845. );
  846. while (true) {
  847. let toggle_favourite_response;
  848. if (is_list_anime) {
  849. toggle_favourite_response = await toggleFavouriteForEntry({
  850. animeId: media_ids[i],
  851. });
  852. } else {
  853. toggle_favourite_response = await toggleFavouriteForEntry({
  854. mangaId: media_ids[i],
  855. });
  856. }
  857. if (toggle_favourite_response.errors) {
  858. const error_message = `An error occurred while <b>${entry_title}</b> was being ${
  859. values_to_be_changed.favourite ? "added to" : "removed from"
  860. } favourites. Please look at the console for more information. Do you want to retry or cancel the request?`;
  861. if (await createErrorPopup(error_message)) {
  862. closePopup();
  863. return false;
  864. }
  865. } else {
  866. break;
  867. }
  868. }
  869. }
  870. }
  871. }
  872.  
  873. // Adding/removing from custom lists requires more meddling.
  874. // If some but not all custom lists have been chosen further processing is required.
  875. // array.every() returns true if array is empty so we need to check that.
  876. /** @type {void | string[][]} */
  877. let all_processed_custom_lists;
  878. if (
  879. custom_lists_checkboxes.some((checkbox) => !checkbox.indeterminate) &&
  880. !(
  881. custom_lists_checkboxes.length > 0 &&
  882. custom_lists_checkboxes.every((checkbox) => !checkbox.indeterminate)
  883. )
  884. ) {
  885. let custom_lists_response;
  886. while (true) {
  887. custom_lists_response = await getDataFromEntries(
  888. media_ids,
  889. "customLists"
  890. );
  891. if (custom_lists_response.errors) {
  892. 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?`;
  893. if (await createErrorPopup(error_message)) {
  894. closePopup();
  895. return false;
  896. }
  897. } else {
  898. break;
  899. }
  900. }
  901. all_processed_custom_lists = [];
  902. for (let i = 0; i < selected_entries.length && !is_cancelled; i++) {
  903. changePopupContent(
  904. createEntryPopupContent(
  905. `Getting the custom lists of: <b>${selected_entries[i]
  906. .querySelector(".title > a")
  907. .innerText.trim()}</b>`,
  908. selected_entries[i].querySelector(".image").style
  909. .backgroundImage,
  910. i + 1,
  911. selected_entries.length
  912. )
  913. );
  914. let processed_custom_lists = [];
  915. let entry_custom_lists = custom_lists_response.data[i];
  916. for (let j = 0; j < custom_lists.length; j++) {
  917. if (!custom_lists_checkboxes[j].indeterminate) {
  918. if (custom_lists_checkboxes[j].checked) {
  919. processed_custom_lists.push(custom_lists[j]);
  920. }
  921. } else {
  922. if (entry_custom_lists[custom_lists[j]]) {
  923. processed_custom_lists.push(custom_lists[j]);
  924. }
  925. }
  926. }
  927. all_processed_custom_lists.push(processed_custom_lists);
  928. }
  929. }
  930.  
  931. // Using advanced scores requires more meddling.
  932. // If some but not all advanced scores have been chosen further processing is required.
  933. // array.every() returns true if array is empty so we need to check that.
  934. /** @type {void | string[][]} */
  935. let all_processed_advanced_scores;
  936. const some_but_not_all_advanced_scores =
  937. advanced_scores_enabled_checkboxes.some(
  938. (checkbox) => checkbox.checked
  939. ) &&
  940. !(
  941. advanced_scores_enabled_checkboxes.length > 0 &&
  942. advanced_scores_enabled_checkboxes.every(
  943. (checkbox) => checkbox.checked
  944. )
  945. );
  946. if (some_but_not_all_advanced_scores) {
  947. let advanced_scores_response;
  948. while (true) {
  949. advanced_scores_response = await getDataFromEntries(
  950. media_ids,
  951. "advancedScores"
  952. );
  953. if (advanced_scores_response.errors) {
  954. 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?`;
  955. if (await createErrorPopup(error_message)) {
  956. closePopup();
  957. return false;
  958. }
  959. } else {
  960. break;
  961. }
  962. }
  963. all_processed_advanced_scores = [];
  964. for (let i = 0; i < selected_entries.length && !is_cancelled; i++) {
  965. changePopupContent(
  966. createEntryPopupContent(
  967. `Getting the advanced scores of: <b>${selected_entries[i]
  968. .querySelector(".title > a")
  969. .innerText.trim()}</b>`,
  970. selected_entries[i].querySelector(".image").style
  971. .backgroundImage,
  972. i + 1,
  973. selected_entries.length
  974. )
  975. );
  976. let processed_advanced_scores = [];
  977. let entry_advanced_scores = Object.values(
  978. advanced_scores_response.data[i]
  979. );
  980. for (let j = 0; j < advanced_scores.length; j++) {
  981. if (advanced_scores_enabled_checkboxes[j].checked) {
  982. processed_advanced_scores.push(
  983. values_to_be_changed.advancedScores[j]
  984. );
  985. } else {
  986. processed_advanced_scores.push(entry_advanced_scores[j]);
  987. }
  988. }
  989. all_processed_advanced_scores.push(processed_advanced_scores);
  990. }
  991. }
  992.  
  993. // If any custom lists or some but not all advanced scores have been chosen, we require individual updates.
  994. if (
  995. custom_lists_checkboxes.some((checkbox) => !checkbox.indeterminate) ||
  996. some_but_not_all_advanced_scores
  997. ) {
  998. const values = { ...values_to_be_changed };
  999. for (let i = 0; i < selected_entries.length && !is_cancelled; i++) {
  1000. changePopupContent(
  1001. createEntryPopupContent(
  1002. `Updating: <b>${selected_entries[i]
  1003. .querySelector(".title > a")
  1004. .innerText.trim()}</b>`,
  1005. selected_entries[i].querySelector(".image").style
  1006. .backgroundImage,
  1007. i + 1,
  1008. selected_entries.length
  1009. )
  1010. );
  1011. while (true) {
  1012. if (all_processed_custom_lists) {
  1013. values.customLists = all_processed_custom_lists[i];
  1014. }
  1015. if (all_processed_advanced_scores) {
  1016. values.advancedScores = all_processed_advanced_scores[i];
  1017. }
  1018. const update_response = await updateEntry(ids[i], values);
  1019. if (update_response.errors) {
  1020. const entry_title = selected_entries[i]
  1021. .querySelector(".title > a")
  1022. .innerText.trim();
  1023. 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?`;
  1024. if (await createErrorPopup(error_message)) {
  1025. closePopup();
  1026. return false;
  1027. }
  1028. } else {
  1029. break;
  1030. }
  1031. }
  1032. }
  1033. closePopup();
  1034. return true;
  1035. }
  1036.  
  1037. // Don't batch update if not required
  1038. if (
  1039. status_enabled_checkbox.checked ||
  1040. score_enabled_checkbox.checked ||
  1041. (advanced_scores.length > 0 &&
  1042. advanced_scores_enabled_checkboxes.every((e) => e.checked)) ||
  1043. (is_list_anime &&
  1044. (progress_inputs.episode_enabled_checkbox.checked ||
  1045. progress_inputs.rewatches_enabled_checkbox.checked)) ||
  1046. (!is_list_anime &&
  1047. (progress_inputs.chapter_enabled_checkbox.checked ||
  1048. progress_inputs.volume_enabled_checkbox.checked ||
  1049. progress_inputs.rereads_enabled_checkbox.checked)) ||
  1050. start_date_enabled_checkbox.checked ||
  1051. finish_date_enabled_checkbox.checked ||
  1052. notes_enabled_checkbox.checked ||
  1053. (custom_lists.length > 0 &&
  1054. !hide_from_status_list_checkbox.indeterminate) ||
  1055. !private_checkbox.indeterminate
  1056. ) {
  1057. changePopupContent(
  1058. "Updating all the entries at once. Not possible to cancel."
  1059. );
  1060. while (true) {
  1061. const batch_update_response = await batchUpdateEntries(
  1062. ids,
  1063. values_to_be_changed
  1064. );
  1065. if (batch_update_response.errors) {
  1066. 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?`;
  1067. if (await createErrorPopup(error_message)) {
  1068. closePopup();
  1069. return false;
  1070. }
  1071. } else {
  1072. break;
  1073. }
  1074. }
  1075. }
  1076.  
  1077. closePopup();
  1078. return true;
  1079. })();
  1080.  
  1081. if (success) {
  1082. const finished_popup_button = createConfirmPopup(
  1083. "Done!",
  1084. "The request has finished. Do you want to refresh?"
  1085. );
  1086. finished_popup_button.onclick = () => window.location.reload();
  1087. }
  1088. };
  1089. };
  1090.  
  1091. new MutationObserver(() => {
  1092. const selected_entries = document.querySelectorAll(
  1093. ".rtonne-anilist-multiselect-selected"
  1094. ).length;
  1095. currently_selected_label.innerHTML = `You have <b><u>${selected_entries}</u></b> entr${
  1096. selected_entries > 1 ? "ies" : "y"
  1097. } selected.`;
  1098. if (selected_entries > 0) {
  1099. form.style.display = "flex";
  1100. help.style.display = "block";
  1101. } else {
  1102. form.style.display = "none";
  1103. help.style.display = "none";
  1104. }
  1105. }).observe(document.querySelector(".lists"), {
  1106. childList: true,
  1107. subtree: true,
  1108. attributeFilter: ["class"],
  1109. });
  1110. }