Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Holotower Thread Status Updater
- // @namespace http://holotower.org/
- // @version 1.3
- // @author Anonymous
- // @description Updates Holotower favicon on new replies, accurate post counts, adds deleted post counter, and notifies when post limit is reached
- // @icon https://boards.holotower.org/favicon.gif
- // @match *://boards.holotower.org/*/res/*
- // @match *://holotower.org/*/res/*
- // @run-at document-end
- // ==/UserScript==
- (function() {
- 'use strict';
- // === Settings ===
- const showDeletedCounter = true; // Displays the number of deleted posts since entering or refreshing the thread
- const showDeletedIcon = true; // Displays a trash icon next to deleted posts
- const showArchivedMessage = true; // Displays a message when post limit is reached or the thread 404s
- const notifyNewPost = true; // Displays a white circle in the favicon whenever a new post appears
- const notifyNewYou = true; // Displays a red circle in the favicon whenever a post quotes (You)
- const changeFaviconOnArchive = true; // Changes the favicon to red when the thread is archived
- const showUnreadLine = true; // Displays a red line underneath the last post prior to the auto updater
- const FAVICON_URL = window.location.hostname === 'boards.holotower.org'
- ? 'https://boards.holotower.org/favicon.gif'
- : 'https://holotower.org/favicon.gif';
- let alertState = 'none';
- let lastPostCount = null;
- let lastSeenPostId = 0;
- let lastLine = 0;
- let hasUnreadLine = false;
- let previousStyle = '';
- function setFavicon(url) {
- let link = document.querySelector("link[rel*='icon']") || document.createElement('link');
- link.type = 'image/x-icon';
- link.rel = 'shortcut icon';
- link.href = url;
- if (!link.parentNode) document.head.appendChild(link);
- }
- function updateFavicon(color) {
- if (alertState === 'red' && color === 'white') return;
- if (alertState === color) return;
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- canvas.width = img.width;
- canvas.height = img.height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0);
- ctx.beginPath();
- ctx.arc(canvas.width - 16, 16, 5, 0, 2 * Math.PI);
- ctx.fillStyle = color;
- ctx.fill();
- setFavicon(canvas.toDataURL('image/x-icon'));
- alertState = color;
- };
- img.src = FAVICON_URL;
- }
- function updateFaviconArchived() {
- if (!changeFaviconOnArchive) return;
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- canvas.width = img.width;
- canvas.height = img.height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0);
- ctx.globalCompositeOperation = 'source-atop';
- ctx.fillStyle = 'red';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- setFavicon(canvas.toDataURL('image/x-icon'));
- };
- img.src = FAVICON_URL;
- }
- function revertFavicon() {
- if (alertState !== 'none') {
- setFavicon(FAVICON_URL);
- alertState = 'none';
- }
- }
- function classifyPost(node) {
- if (node.nodeType !== 1) return 'none';
- if (node.classList.contains('post-hover')) return 'none';
- if (node.closest('.inline-quote-container')) return 'none';
- if (node.closest('.post.qp.reply')) return 'none';
- if (node.matches('div.post.reply') && node.textContent.trim() === '') return 'none';
- if (node.matches('div.post.reply.quoting-you') && notifyNewYou) return 'red';
- if (node.matches('div.post.reply') && node.querySelector('.body small')?.textContent.trim() === '(You)' && notifyNewYou) return 'red';
- if (node.matches('div.post.reply') && notifyNewPost) return 'white';
- return 'none';
- }
- new MutationObserver(mutations => {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- const result = classifyPost(node);
- if (result === 'red') {
- updateFavicon('red');
- return;
- } else if (result === 'white') {
- updateFavicon('white');
- }
- }
- }
- }).observe(document.body, { childList: true, subtree: true });
- window.addEventListener('scroll', () => {
- if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2) {
- revertFavicon();
- removeUnreadLine();
- }
- });
- function initializeLastSeenPostId() {
- const posts = document.querySelectorAll('div.post.reply');
- posts.forEach(post => {
- const postId = post.id.split('_')[1];
- if (postId > lastSeenPostId) lastSeenPostId = postId;
- lastLine = postId;
- });
- }
- initializeLastSeenPostId();
- function addUnreadLine() {
- if (!showUnreadLine) return;
- const lastPost = document.querySelector(`#reply_${lastLine}`);
- if (lastPost && !hasUnreadLine) {
- previousStyle = lastPost.getAttribute('style') || '';
- lastPost.style.boxShadow = '0 3px red';
- hasUnreadLine = true;
- }
- }
- function removeUnreadLine() {
- if (!showUnreadLine) return;
- const lastPost = document.querySelector(`#reply_${lastLine}`);
- if (lastPost && hasUnreadLine) {
- if (previousStyle) {
- lastPost.setAttribute('style', previousStyle);
- } else {
- lastPost.removeAttribute('style');
- }
- lastLine = lastSeenPostId;
- hasUnreadLine = false;
- }
- }
- function countPostsFromHTML(htmlText) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlText, "text/html");
- const postCount = doc.querySelectorAll('div.post.reply').length;
- if (postCount > 0) {
- lastPostCount = postCount;
- updateThreadStatsActual();
- }
- }
- function updateThreadStatsActual() {
- if (lastPostCount !== null) {
- const oldElement = document.getElementById('thread_stats_posts');
- if (oldElement) {
- oldElement.style.display = 'none';
- let newElement = document.getElementById('thread_stats_posts_actual');
- if (!newElement) {
- newElement = document.createElement('span');
- newElement.id = 'thread_stats_posts_actual';
- oldElement.parentNode.insertBefore(newElement, oldElement.nextSibling);
- }
- newElement.textContent = lastPostCount;
- const postsDeleted = parseInt(oldElement.textContent) - lastPostCount;
- if (showDeletedCounter) {
- let deletedElement = document.getElementById('thread_stats_posts_deleted');
- if (postsDeleted >= 1) {
- if (!deletedElement) {
- deletedElement = document.createElement('span');
- deletedElement.id = 'thread_stats_posts_deleted';
- const imagesElement = document.getElementById('thread_stats_images');
- if (imagesElement) {
- imagesElement.parentNode.insertBefore(deletedElement, imagesElement);
- }
- }
- deletedElement.textContent = postsDeleted;
- deletedElement.insertAdjacentHTML('beforeend', ' deleted | ');
- } else if (deletedElement) {
- deletedElement.remove();
- }
- }
- if (lastPostCount >= 1500) {
- if (changeFaviconOnArchive) updateFaviconArchived();
- if (showArchivedMessage && !document.getElementById('archived-msg')) {
- addArchivedMessage();
- }
- }
- }
- }
- }
- function addArchivedMessage() {
- const postControlsForm = document.forms["postcontrols"];
- if (!postControlsForm) return;
- const archivedMsg = document.createElement('div');
- archivedMsg.id = 'archived-msg';
- archivedMsg.style.marginTop = '-25px';
- archivedMsg.style.marginBottom = '20px';
- archivedMsg.innerHTML = `
- <strong style="color: red; font-size: 14px;">THREAD ARCHIVED</strong><br>
- <img src="https://i.imgur.com/LQHVLil.png" alt="Archived Image" style="margin-top: 5px; width: 7%;">
- `;
- postControlsForm.parentNode.insertBefore(archivedMsg, postControlsForm.nextSibling);
- }
- (function(open) {
- XMLHttpRequest.prototype.open = function(method, url) {
- const currentThreadId = window.location.pathname.split('/res/')[1].split('.')[0];
- const target = `/res/${currentThreadId}.html`;
- this._monitored = url.includes(target) && url.indexOf(target) + target.length === url.length;
- return open.apply(this, arguments);
- };
- })(XMLHttpRequest.prototype.open);
- (function(send) {
- XMLHttpRequest.prototype.send = function() {
- if (this._monitored) {
- this.addEventListener('load', () => {
- if (this.status === 404) {
- if (changeFaviconOnArchive) updateFaviconArchived();
- if (showArchivedMessage && !document.getElementById('archived-msg')) {
- addArchivedMessage();
- }
- } else {
- countPostsFromHTML(this.responseText);
- syncPostStatus(this.responseText);
- }
- });
- }
- return send.apply(this, arguments);
- };
- })(XMLHttpRequest.prototype.send);
- function syncPostStatus(responseText) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(responseText, "text/html");
- const currentThreadId = window.location.pathname.split('/res/')[1].split('.')[0];
- const currentPosts = document.querySelectorAll('div.post.reply');
- const updatePosts = doc.querySelectorAll('div.post.reply');
- if (updatePosts.length === 0) return;
- const updatePostIds = new Set();
- updatePosts.forEach(post => {
- const postId = post.id.split('_')[1];
- updatePostIds.add(postId);
- if (postId > lastSeenPostId) {
- lastSeenPostId = postId;
- addUnreadLine();
- }
- });
- currentPosts.forEach(post => {
- if (post.closest('.post.qp.reply')) return;
- if (post.classList.contains('post-hover')) return;
- const postId = post.id.split('_')[1];
- const xthreadId = post.closest('.thread').id.split('_')[1];
- if (xthreadId !== currentThreadId) return;
- const inlineQuoteContainer = post.closest('.inline-quote-container');
- if (inlineQuoteContainer) {
- const inlinedId = inlineQuoteContainer.getAttribute('data-inlined-id');
- if (!updatePostIds.has(inlinedId)) {
- addDeletedIcon(post);
- } else if (post.querySelector('.fa-trash')) {
- removeDeletedIcon(post);
- }
- } else {
- if (!updatePostIds.has(postId)) {
- addDeletedIcon(post);
- } else if (post.querySelector('.fa-trash')) {
- removeDeletedIcon(post);
- }
- }
- });
- }
- function addDeletedIcon(post) {
- if (!showDeletedIcon) return;
- if (!post.querySelector('.post-btn')) return;
- const postNoLink = post.querySelector('.post_no');
- if (postNoLink && !post.querySelector('.fa-trash')) {
- const trashIcon = document.createElement('i');
- trashIcon.classList.add('fa', 'fa-trash');
- trashIcon.title = 'Deleted';
- trashIcon.style.opacity = '0.5';
- trashIcon.style.marginRight = '0px';
- postNoLink.parentNode.insertBefore(trashIcon, postNoLink.nextSibling.nextSibling);
- }
- }
- function removeDeletedIcon(post) {
- if (!showDeletedIcon) return;
- const postNoLink = post.querySelector('.post_no');
- const trashIcon = postNoLink.nextSibling.nextSibling;
- if (trashIcon && trashIcon.classList.contains('fa-trash')) {
- trashIcon.remove();
- }
- }
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement