Advertisement
Guest User

Untitled

a guest
Apr 29th, 2025
130
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.56 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Holotower Thread Status Updater
  3. // @namespace http://holotower.org/
  4. // @version 1.3
  5. // @author Anonymous
  6. // @description Updates Holotower favicon on new replies, accurate post counts, adds deleted post counter, and notifies when post limit is reached
  7. // @icon https://boards.holotower.org/favicon.gif
  8. // @match *://boards.holotower.org/*/res/*
  9. // @match *://holotower.org/*/res/*
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // === Settings ===
  18. const showDeletedCounter = true; // Displays the number of deleted posts since entering or refreshing the thread
  19. const showDeletedIcon = true; // Displays a trash icon next to deleted posts
  20. const showArchivedMessage = true; // Displays a message when post limit is reached or the thread 404s
  21. const notifyNewPost = true; // Displays a white circle in the favicon whenever a new post appears
  22. const notifyNewYou = true; // Displays a red circle in the favicon whenever a post quotes (You)
  23. const changeFaviconOnArchive = true; // Changes the favicon to red when the thread is archived
  24. const showUnreadLine = true; // Displays a red line underneath the last post prior to the auto updater
  25.  
  26. const FAVICON_URL = window.location.hostname === 'boards.holotower.org'
  27. ? 'https://boards.holotower.org/favicon.gif'
  28. : 'https://holotower.org/favicon.gif';
  29. let alertState = 'none';
  30.  
  31. let lastPostCount = null;
  32. let lastSeenPostId = 0;
  33. let lastLine = 0;
  34. let hasUnreadLine = false;
  35. let previousStyle = '';
  36.  
  37. function setFavicon(url) {
  38. let link = document.querySelector("link[rel*='icon']") || document.createElement('link');
  39. link.type = 'image/x-icon';
  40. link.rel = 'shortcut icon';
  41. link.href = url;
  42. if (!link.parentNode) document.head.appendChild(link);
  43. }
  44.  
  45. function updateFavicon(color) {
  46. if (alertState === 'red' && color === 'white') return;
  47. if (alertState === color) return;
  48.  
  49. const img = new Image();
  50. img.onload = () => {
  51. const canvas = document.createElement('canvas');
  52. canvas.width = img.width;
  53. canvas.height = img.height;
  54. const ctx = canvas.getContext('2d');
  55. ctx.drawImage(img, 0, 0);
  56.  
  57. ctx.beginPath();
  58. ctx.arc(canvas.width - 16, 16, 5, 0, 2 * Math.PI);
  59. ctx.fillStyle = color;
  60. ctx.fill();
  61.  
  62. setFavicon(canvas.toDataURL('image/x-icon'));
  63. alertState = color;
  64. };
  65. img.src = FAVICON_URL;
  66. }
  67.  
  68. function updateFaviconArchived() {
  69. if (!changeFaviconOnArchive) return;
  70.  
  71. const img = new Image();
  72. img.onload = () => {
  73. const canvas = document.createElement('canvas');
  74. canvas.width = img.width;
  75. canvas.height = img.height;
  76. const ctx = canvas.getContext('2d');
  77. ctx.drawImage(img, 0, 0);
  78.  
  79. ctx.globalCompositeOperation = 'source-atop';
  80. ctx.fillStyle = 'red';
  81. ctx.fillRect(0, 0, canvas.width, canvas.height);
  82.  
  83. setFavicon(canvas.toDataURL('image/x-icon'));
  84. };
  85. img.src = FAVICON_URL;
  86. }
  87.  
  88. function revertFavicon() {
  89. if (alertState !== 'none') {
  90. setFavicon(FAVICON_URL);
  91. alertState = 'none';
  92. }
  93. }
  94.  
  95. function classifyPost(node) {
  96. if (node.nodeType !== 1) return 'none';
  97. if (node.classList.contains('post-hover')) return 'none';
  98. if (node.closest('.inline-quote-container')) return 'none';
  99. if (node.closest('.post.qp.reply')) return 'none';
  100. if (node.matches('div.post.reply') && node.textContent.trim() === '') return 'none';
  101.  
  102. if (node.matches('div.post.reply.quoting-you') && notifyNewYou) return 'red';
  103. if (node.matches('div.post.reply') && node.querySelector('.body small')?.textContent.trim() === '(You)' && notifyNewYou) return 'red';
  104. if (node.matches('div.post.reply') && notifyNewPost) return 'white';
  105.  
  106. return 'none';
  107. }
  108.  
  109. new MutationObserver(mutations => {
  110. for (const mutation of mutations) {
  111. for (const node of mutation.addedNodes) {
  112. const result = classifyPost(node);
  113. if (result === 'red') {
  114. updateFavicon('red');
  115. return;
  116. } else if (result === 'white') {
  117. updateFavicon('white');
  118. }
  119. }
  120. }
  121. }).observe(document.body, { childList: true, subtree: true });
  122.  
  123. window.addEventListener('scroll', () => {
  124. if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2) {
  125. revertFavicon();
  126. removeUnreadLine();
  127. }
  128. });
  129.  
  130. function initializeLastSeenPostId() {
  131. const posts = document.querySelectorAll('div.post.reply');
  132. posts.forEach(post => {
  133. const postId = post.id.split('_')[1];
  134. if (postId > lastSeenPostId) lastSeenPostId = postId;
  135. lastLine = postId;
  136. });
  137. }
  138.  
  139. initializeLastSeenPostId();
  140.  
  141. function addUnreadLine() {
  142. if (!showUnreadLine) return;
  143.  
  144. const lastPost = document.querySelector(`#reply_${lastLine}`);
  145. if (lastPost && !hasUnreadLine) {
  146. previousStyle = lastPost.getAttribute('style') || '';
  147. lastPost.style.boxShadow = '0 3px red';
  148. hasUnreadLine = true;
  149. }
  150. }
  151.  
  152. function removeUnreadLine() {
  153. if (!showUnreadLine) return;
  154.  
  155. const lastPost = document.querySelector(`#reply_${lastLine}`);
  156. if (lastPost && hasUnreadLine) {
  157. if (previousStyle) {
  158. lastPost.setAttribute('style', previousStyle);
  159. } else {
  160. lastPost.removeAttribute('style');
  161. }
  162. lastLine = lastSeenPostId;
  163. hasUnreadLine = false;
  164. }
  165. }
  166.  
  167. function countPostsFromHTML(htmlText) {
  168. const parser = new DOMParser();
  169. const doc = parser.parseFromString(htmlText, "text/html");
  170. const postCount = doc.querySelectorAll('div.post.reply').length;
  171.  
  172. if (postCount > 0) {
  173. lastPostCount = postCount;
  174. updateThreadStatsActual();
  175. }
  176. }
  177.  
  178. function updateThreadStatsActual() {
  179. if (lastPostCount !== null) {
  180. const oldElement = document.getElementById('thread_stats_posts');
  181. if (oldElement) {
  182. oldElement.style.display = 'none';
  183.  
  184. let newElement = document.getElementById('thread_stats_posts_actual');
  185. if (!newElement) {
  186. newElement = document.createElement('span');
  187. newElement.id = 'thread_stats_posts_actual';
  188. oldElement.parentNode.insertBefore(newElement, oldElement.nextSibling);
  189. }
  190.  
  191. newElement.textContent = lastPostCount;
  192.  
  193. const postsDeleted = parseInt(oldElement.textContent) - lastPostCount;
  194.  
  195. if (showDeletedCounter) {
  196. let deletedElement = document.getElementById('thread_stats_posts_deleted');
  197. if (postsDeleted >= 1) {
  198. if (!deletedElement) {
  199. deletedElement = document.createElement('span');
  200. deletedElement.id = 'thread_stats_posts_deleted';
  201. const imagesElement = document.getElementById('thread_stats_images');
  202. if (imagesElement) {
  203. imagesElement.parentNode.insertBefore(deletedElement, imagesElement);
  204. }
  205. }
  206.  
  207. deletedElement.textContent = postsDeleted;
  208. deletedElement.insertAdjacentHTML('beforeend', ' deleted | ');
  209. } else if (deletedElement) {
  210. deletedElement.remove();
  211. }
  212. }
  213.  
  214. if (lastPostCount >= 1500) {
  215. if (changeFaviconOnArchive) updateFaviconArchived();
  216. if (showArchivedMessage && !document.getElementById('archived-msg')) {
  217. addArchivedMessage();
  218. }
  219. }
  220. }
  221. }
  222. }
  223.  
  224. function addArchivedMessage() {
  225. const postControlsForm = document.forms["postcontrols"];
  226. if (!postControlsForm) return;
  227.  
  228. const archivedMsg = document.createElement('div');
  229. archivedMsg.id = 'archived-msg';
  230. archivedMsg.style.marginTop = '-25px';
  231. archivedMsg.style.marginBottom = '20px';
  232.  
  233. archivedMsg.innerHTML = `
  234. <strong style="color: red; font-size: 14px;">THREAD ARCHIVED</strong><br>
  235. <img src="https://i.imgur.com/LQHVLil.png" alt="Archived Image" style="margin-top: 5px; width: 7%;">
  236. `;
  237.  
  238. postControlsForm.parentNode.insertBefore(archivedMsg, postControlsForm.nextSibling);
  239. }
  240.  
  241. (function(open) {
  242. XMLHttpRequest.prototype.open = function(method, url) {
  243. const currentThreadId = window.location.pathname.split('/res/')[1].split('.')[0];
  244. const target = `/res/${currentThreadId}.html`;
  245. this._monitored = url.includes(target) && url.indexOf(target) + target.length === url.length;
  246. return open.apply(this, arguments);
  247. };
  248. })(XMLHttpRequest.prototype.open);
  249.  
  250. (function(send) {
  251. XMLHttpRequest.prototype.send = function() {
  252. if (this._monitored) {
  253. this.addEventListener('load', () => {
  254. if (this.status === 404) {
  255. if (changeFaviconOnArchive) updateFaviconArchived();
  256. if (showArchivedMessage && !document.getElementById('archived-msg')) {
  257. addArchivedMessage();
  258. }
  259. } else {
  260. countPostsFromHTML(this.responseText);
  261. syncPostStatus(this.responseText);
  262. }
  263. });
  264. }
  265. return send.apply(this, arguments);
  266. };
  267. })(XMLHttpRequest.prototype.send);
  268.  
  269. function syncPostStatus(responseText) {
  270. const parser = new DOMParser();
  271. const doc = parser.parseFromString(responseText, "text/html");
  272. const currentThreadId = window.location.pathname.split('/res/')[1].split('.')[0];
  273. const currentPosts = document.querySelectorAll('div.post.reply');
  274. const updatePosts = doc.querySelectorAll('div.post.reply');
  275.  
  276. if (updatePosts.length === 0) return;
  277.  
  278. const updatePostIds = new Set();
  279. updatePosts.forEach(post => {
  280. const postId = post.id.split('_')[1];
  281. updatePostIds.add(postId);
  282.  
  283. if (postId > lastSeenPostId) {
  284. lastSeenPostId = postId;
  285. addUnreadLine();
  286. }
  287. });
  288.  
  289. currentPosts.forEach(post => {
  290. if (post.closest('.post.qp.reply')) return;
  291. if (post.classList.contains('post-hover')) return;
  292.  
  293. const postId = post.id.split('_')[1];
  294. const xthreadId = post.closest('.thread').id.split('_')[1];
  295.  
  296. if (xthreadId !== currentThreadId) return;
  297.  
  298. const inlineQuoteContainer = post.closest('.inline-quote-container');
  299. if (inlineQuoteContainer) {
  300. const inlinedId = inlineQuoteContainer.getAttribute('data-inlined-id');
  301. if (!updatePostIds.has(inlinedId)) {
  302. addDeletedIcon(post);
  303. } else if (post.querySelector('.fa-trash')) {
  304. removeDeletedIcon(post);
  305. }
  306. } else {
  307. if (!updatePostIds.has(postId)) {
  308. addDeletedIcon(post);
  309. } else if (post.querySelector('.fa-trash')) {
  310. removeDeletedIcon(post);
  311. }
  312. }
  313. });
  314. }
  315.  
  316. function addDeletedIcon(post) {
  317. if (!showDeletedIcon) return;
  318. if (!post.querySelector('.post-btn')) return;
  319.  
  320. const postNoLink = post.querySelector('.post_no');
  321. if (postNoLink && !post.querySelector('.fa-trash')) {
  322. const trashIcon = document.createElement('i');
  323. trashIcon.classList.add('fa', 'fa-trash');
  324. trashIcon.title = 'Deleted';
  325. trashIcon.style.opacity = '0.5';
  326. trashIcon.style.marginRight = '0px';
  327. postNoLink.parentNode.insertBefore(trashIcon, postNoLink.nextSibling.nextSibling);
  328. }
  329. }
  330.  
  331. function removeDeletedIcon(post) {
  332. if (!showDeletedIcon) return;
  333.  
  334. const postNoLink = post.querySelector('.post_no');
  335. const trashIcon = postNoLink.nextSibling.nextSibling;
  336. if (trashIcon && trashIcon.classList.contains('fa-trash')) {
  337. trashIcon.remove();
  338. }
  339. }
  340. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement