AniList Edit Multiple Media Simultaneously

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

目前為 2024-06-02 提交的版本,檢視 最新版本

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