Unlimited MAL Ignore list

Ignore an unlimited amount of users with other custom settings: deleting the entire post, replace the content of the message with a custom message, replace or delete the avatar, keep or delete the signature.

目前为 2021-10-12 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Unlimited MAL Ignore list
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.60
  5. // @description Ignore an unlimited amount of users with other custom settings: deleting the entire post, replace the content of the message with a custom message, replace or delete the avatar, keep or delete the signature.
  6. // @author Only_Brad
  7. // @match https://myanimelist.net/*
  8. // @run-at document-end
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. const POSTS_URL = "forum/?topicid";
  13. const TOPICS_URL = "forum/?board";
  14. const CLUB_TOPICS_URL_1 = "clubs.php";
  15. const CLUB_TOPICS_URL_2 = "forum/?clubid";
  16. const BLACKLIST_URL = "https://myanimelist.net/blacklist";
  17.  
  18. const BLACKLIST_KEY = "ignore-list";
  19. const SETTINGS_KEY = "ignore-list-settings";
  20.  
  21. const POST_USERS_SELECTOR = ".forum_boardrow2 > div";
  22. const TOPIC_USERS_SELECTOR = ".forum_postusername a";
  23. const MESSAGE_SELECTOR = ".forum_boardrow1 [id^=message]";
  24. const AVATAR_SELECTOR = ".forum-icon";
  25. const USER_PROFILE_SELECTOR = "[href^='/profile']";
  26. const USER_INFO_SELECTOR = "[id^=messageuser]";
  27. const SIGNATURE_SELECTOR = ".sig";
  28. const FORUM_MESSAGE_SELECTOR = "[id^=forumMsg]";
  29. const ACTION_BAR_SELECTOR = "[id^=postEditButtons]";
  30.  
  31. const IGNORE_POSTS = 0;
  32. const REPLACE_POSTS = 1;
  33. const DO_NOTHING_POSTS = 2;
  34.  
  35. let blacklist;
  36. let settings;
  37.  
  38. //routing
  39. if (window.location.href.includes(POSTS_URL)) {
  40. handlePosts();
  41. } else if (
  42. window.location.href.includes(TOPICS_URL) ||
  43. window.location.href.includes(CLUB_TOPICS_URL_1) ||
  44. window.location.href.includes(CLUB_TOPICS_URL_2)
  45. ) {
  46. handleTopics();
  47. } else if (window.location.href === BLACKLIST_URL) {
  48. handleBlacklist();
  49. }
  50.  
  51. //GM_addStyle equivalent that works on firefox
  52. function addStyle(css) {
  53. const style = document.getElementById("addStyleBy8626") || (function() {
  54. const style = document.createElement('style');
  55. style.type = 'text/css';
  56. style.id = "addStyleBy8626";
  57. document.head.appendChild(style);
  58. return style;
  59. })();
  60. style.innerHTML += css;
  61. }
  62.  
  63. //helper functions to load from localStorage
  64. function loadBlackList() {
  65. blacklist = JSON.parse(localStorage.getItem(BLACKLIST_KEY)) || [];
  66. }
  67.  
  68. function saveBlackList() {
  69. localStorage.setItem(BLACKLIST_KEY, JSON.stringify(blacklist));
  70. }
  71.  
  72. function loadSettings() {
  73. settings = JSON.parse(localStorage.getItem(SETTINGS_KEY)) || {
  74. replaceAvatar: false,
  75. removeSignatures: true,
  76. postMode: IGNORE_POSTS,
  77. removeTopics: true,
  78. customPost: "",
  79. customAvatar: ""
  80. };
  81. }
  82.  
  83. function saveSetting(key, value) {
  84. settings[key] = value;
  85. localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
  86. }
  87.  
  88. //functions called by the routers
  89. function handlePosts() {
  90. loadBlackList();
  91. loadSettings();
  92.  
  93. switch (settings.postMode) {
  94. case IGNORE_POSTS:
  95. ignorePosts();
  96. return;
  97. case REPLACE_POSTS:
  98. replacePosts();
  99. break;
  100. default:
  101. break;
  102. }
  103.  
  104. addBlackListButtons();
  105. if (settings.replaceAvatar) replaceAvatar();
  106. if (settings.removeSignatures) removeSignatures();
  107. }
  108.  
  109. function handleTopics() {
  110. loadBlackList();
  111. loadSettings();
  112.  
  113. if (settings.removeTopics) removeTopics();
  114. }
  115.  
  116. function handleBlacklist() {
  117. loadBlackList();
  118. loadSettings();
  119.  
  120. document.title = "Blacklist - MyAnimeList.net";
  121.  
  122. //remove the 404 stuff
  123. document.querySelector("h1").textContent = "Ignore List";
  124. document.querySelector(".error404").remove();
  125.  
  126. //CSS
  127. addStyle(".user{display:flex;margin:10px}.name{margin-right:20px}.name{border-bottom:solid #000 1px}.name[contenteditable]{min-width:100px;border-bottom:solid #000 1px}.name[contenteditable]:focus{border:none;outline:solid red 5px}.page-common #content{display:flex;justify-content:center;}.settings{display:flex;gap:25px;}.settings>*{padding: 25px;}.customPost{width:100% !important;}");
  128.  
  129. //HTML for the blacklist
  130. document.getElementById("content").innerHTML =
  131. `<div data-blacklist class="black-list"></div>
  132. <div data-add-user class="add-user">
  133. <div data-user class="user">
  134. <div data-name class="name" contenteditable="true" onclick="this.focus()"></div>
  135. <button data-add class="add">Add</div>
  136. </div>
  137. </div>`
  138.  
  139. //HTML for the settings
  140. const settings = document.createElement("div");
  141. settings.innerHTML = `
  142. <h2>Settings</h2>
  143. <form>
  144. <div class="settings">
  145. <div class="posts">
  146. <h3>Posts</h3>
  147. <div class="form-check">
  148. <input class="form-check-input" type="radio" name="posts" id="doNothingPosts" data-clickable-setting="doNothingPosts">
  149. <label class="form-check-label" for="doNothingPosts">
  150. Do Nothing
  151. </label>
  152. </div>
  153. <div class="form-check">
  154. <input class="form-check-input" type="radio" name="posts" id="hidePosts" data-clickable-setting="hidePosts">
  155. <label class="form-check-label" for="hidePosts">
  156. Hide Posts
  157. </label>
  158. </div>
  159. <div class="form-check">
  160. <input class="form-check-input" type="radio" name="posts" id="replacePosts" data-clickable-setting="replacePosts">
  161. <label class="form-check-label" for="replacePosts">
  162. Replace posts with a custom message
  163. </label>
  164. </div>
  165. <textarea class="form-control customPost" name="customPost" id="customPost" data-text-setting="customPost"></textarea>
  166. </div>
  167. <div class="avatar">
  168. <h3>Posts extra options</h3>
  169. <div class="form-check">
  170. <input class="form-check-input" type="checkbox" name="replaceAvatar" id="replaceAvatar" data-clickable-setting="replaceAvatar">
  171. <label class="form-check-label" for="replaceAvatar">
  172. Replace avatars with a custom avatar
  173. </label>
  174. <input class="form-control" type="text" name="customAvatar" id="customAvatar" data-text-setting="customAvatar">
  175. <br>
  176. <small>Leave it empty to remove the avatar</small>
  177. </div>
  178. <div class="form-check" style="margin-top: 10px;">
  179. <input class="form-check-input" type="checkbox" name="removeSignatures" id="removeSignatures" data-clickable-setting="removeSignatures">
  180. <label class="form-check-label" for="removeSignatures">
  181. Hide the signature
  182. </label>
  183. </div>
  184. <small style="margin-top: 20px; display: block;"><strong>These settings have no effect if the Posts setting is set to "Hide Posts"</strong></small>
  185. </div>
  186. <div class="topics">
  187. <h3>Topics</h3>
  188. <div class="form-check">
  189. <input class="form-check-input" type="checkbox" name="removeTopics" id="removeTopics" data-clickable-setting="removeTopics">
  190. <label class="form-check-label" for="removeTopics">
  191. Hide Topics
  192. </label>
  193. </div>
  194. </div>
  195. </div>
  196. </form>`;
  197.  
  198. document.getElementById("content").insertAdjacentElement("afterend", settings);
  199. document.querySelector("[data-add]").addEventListener("click", addNode);
  200. document.querySelectorAll("[data-clickable-setting]").forEach(clickable => {
  201. clickable.addEventListener("click", clickedSetting);
  202. });
  203. document.querySelectorAll("[data-text-setting]").forEach(text => {
  204. text.addEventListener("input", textSetting);
  205. });
  206. blacklist.forEach(createNode);
  207. loadSettingsIntoInputs();
  208. }
  209.  
  210. function clickedSetting(e) {
  211. const input = e.target;
  212.  
  213. switch (input.dataset.clickableSetting) {
  214. case "doNothingPosts":
  215. saveSetting("postMode", DO_NOTHING_POSTS);
  216. break;
  217. case "hidePosts":
  218. saveSetting("postMode", IGNORE_POSTS);
  219. break;
  220. case "replacePosts":
  221. saveSetting("postMode", REPLACE_POSTS);
  222. break;
  223. case "replaceAvatar":
  224. saveSetting("replaceAvatar", input.checked);
  225. break;
  226. case "removeTopics":
  227. saveSetting("removeTopics", input.checked);
  228. break;
  229. case "removeSignatures":
  230. saveSetting("removeSignatures", input.checked);
  231. break;
  232. default:
  233. return;
  234. }
  235. }
  236.  
  237. function textSetting(e) {
  238. const input = e.target;
  239. switch (input.dataset.textSetting) {
  240. case "customPost":
  241. saveSetting("customPost", input.value);
  242. break;
  243. case "customAvatar":
  244. saveSetting("customAvatar", input.value);
  245. break;
  246. default:
  247. return;
  248. }
  249. }
  250.  
  251. function loadSettingsIntoInputs() {
  252. switch (settings.postMode) {
  253. case DO_NOTHING_POSTS:
  254. document.getElementById("doNothingPosts").checked = true;
  255. break;
  256. case IGNORE_POSTS:
  257. document.getElementById("hidePosts").checked = true;
  258. break;
  259. case REPLACE_POSTS:
  260. document.getElementById("replacePosts").checked = true;
  261. break;
  262. }
  263.  
  264. if (settings.removeTopics) {
  265. document.getElementById("removeTopics").checked = true;
  266. }
  267. if (settings.removeSignatures) {
  268. document.getElementById("removeSignatures").checked = true;
  269. }
  270. document.getElementById("customPost").value = settings.customPost || "";
  271. document.getElementById("customAvatar").value = settings.customAvatar || "";
  272. }
  273.  
  274. function alterPosts(action) {
  275. document.querySelectorAll(POST_USERS_SELECTOR).forEach(user => {
  276. if (!blacklist.includes(user.querySelector("strong").textContent)) return;
  277. let post = user.parentNode;
  278. for (let i = 0; i < 4; i++) {
  279. post = post.parentNode;
  280. }
  281. action(post, user);
  282. });
  283. }
  284.  
  285. function removeTopics() {
  286. document.querySelectorAll(TOPIC_USERS_SELECTOR).forEach(user => {
  287. if (!blacklist.includes(user.textContent)) return;
  288. user.closest("tr").style.display = "none";
  289. });
  290. }
  291.  
  292. function ignorePosts() {
  293. alterPosts(post => {
  294. post.style.display = "none";
  295. post.previousElementSibling.style.display = "none";
  296. });
  297. }
  298.  
  299. function replacePosts() {
  300. alterPosts(post => {
  301. const message = post.querySelector(MESSAGE_SELECTOR);
  302. message.innerHTML = settings.customPost;
  303. });
  304. }
  305.  
  306. function replaceAvatar() {
  307. alterPosts((post, user) => {
  308. const avatar = user.querySelector(AVATAR_SELECTOR);
  309.  
  310. if (!avatar) {
  311. if (settings.customAvatar === "") return;
  312.  
  313. const avatar = document.createElement("a");
  314. avatar.href = user.querySelector(USER_PROFILE_SELECTOR).href
  315. avatar.className = "forum-icon";
  316. avatar.innerHTML = `
  317. <img class=" lazyloaded" data-src="${settings.customAvatar}" vspace="2" border="0" src="${settings.customAvatar}" width="100" height="125">`;
  318. user.querySelector(USER_INFO_SELECTOR).insertAdjacentElement('afterend', avatar);
  319. } else {
  320. if (settings.customAvatar === "") {
  321. avatar.style.display = "none";
  322. return;
  323. }
  324.  
  325. const img = avatar.querySelector("img");
  326. img.src = settings.customAvatar;
  327. img.setAttribute("data-src", settings.customAvatar);
  328. img.setAttribute("width", 100);
  329. img.setAttribute("height", 125);
  330. }
  331. });
  332. }
  333.  
  334. function removeSignatures() {
  335. alterPosts(post => {
  336. const signature = post.querySelector(SIGNATURE_SELECTOR);
  337. if (!signature) return;
  338. signature.style.display = "none";
  339. });
  340. }
  341.  
  342. function addBlackListButtons() {
  343. document.querySelectorAll(FORUM_MESSAGE_SELECTOR).forEach(forumMessage => {
  344. const actionBar = forumMessage.querySelector(ACTION_BAR_SELECTOR);
  345. const a = document.createElement("a");
  346. a.href = "#!";
  347. a.textContent = "Blacklist User";
  348. a.dataset.username = forumMessage.querySelector("strong").textContent;
  349. a.onclick = blacklistUser;
  350.  
  351. actionBar.prepend(document.createTextNode(" - "));
  352. actionBar.prepend(a);
  353. });
  354. }
  355.  
  356. function blacklistUser(e) {
  357. addUser(e.target.dataset.username);
  358. window.location.reload();
  359. }
  360.  
  361. //Add a user to the blacklist if its not already there
  362. function addUser(username) {
  363. blacklist.push(username);
  364. saveBlackList();
  365. }
  366.  
  367. //Remove a user from the blacklist if it's there
  368. function removeUser(userName) {
  369. blacklist = blacklist.filter(name => userName !== name);
  370. saveBlackList();
  371. }
  372.  
  373. //remove the user node from the html code and then update the localStorage
  374. function removeNode(e) {
  375. const row = e.target.parentNode;
  376. const name = row.querySelector("[data-name]").textContent;
  377. row.remove();
  378. removeUser(name);
  379. }
  380.  
  381. //modify the user node from the html code and then update the localStorage
  382. function saveNode(e) {
  383. const newName = e.target.textContent;
  384. const previousName = e.target.dataset.previousName;
  385.  
  386. previousName && removeUser(previousName);
  387.  
  388. if (newName !== "") {
  389. addUser(newName);
  390. e.target.dataset.previousName = newName;
  391. } else {
  392. e.target.parentNode.remove();
  393. }
  394. }
  395.  
  396. //add a new user node to the html code and then update the localStorage
  397. function addNode(e) {
  398. const node = e.target.parentNode;
  399. const usernameNode = node.querySelector("[data-name]");
  400. const username = usernameNode.textContent;
  401. usernameNode.textContent = "";
  402.  
  403. if (!blacklist.includes(username)) {
  404. createNode(username);
  405. addUser(username);
  406. }
  407. }
  408.  
  409. //create the user node then add it the html code
  410. function createNode(username) {
  411. const newUser = document.createElement("div");
  412. newUser.setAttribute("data-user", "");
  413. newUser.className = "user";
  414. newUser.innerHTML = `<div data-name class="name" contenteditable="true" onclick="this.focus()" data-previous-name="${username}">${username}</div>
  415. <button data-remove class="remove">Remove</button>`;
  416. newUser.querySelector("[data-name]").addEventListener("focusout", saveNode);
  417. newUser.querySelector("[data-remove]").addEventListener("click", removeNode);
  418. document.querySelector("[data-blacklist]").append(newUser);
  419. }
  420. })();