Advertisement
Guest User

Untitled

a guest
May 7th, 2025
124
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 29.27 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Holotower Thread Status Updater
  3. // @namespace http://holotower.org/
  4. // @version 1.4.1
  5. // @author Anonymous
  6. // @description Adds accurate post counts, favicon notifications, and other features to Holotower
  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 settings = JSON.parse(localStorage.getItem("Thread Settings")) || {};
  19.  
  20. const showDeletedCounter = settings.showDeletedCounter ?? true;
  21. const showDeletedIcon = settings.showDeletedIcon ?? true;
  22. const showDeletedText = settings.showDeletedText ?? false;
  23. const hideDeleted = settings.hideDeletedPosts ?? false;
  24. const showArchivedMessage = settings.showArchivedMessage ?? true;
  25. const enableFaviconChanges = settings.faviconUpdater ?? true;
  26. const notifyNewPost = settings.notifyNewPost ?? true;
  27. const notifyNewYou = settings.notifyNewYou ?? true;
  28. const changeFaviconOnArchive = settings.changeFaviconOnArchive ?? true;
  29. const showUnreadLine = settings.showUnreadLine ?? true;
  30. const appendQuotes = settings.appendQuotes ?? true;
  31. const appendCrossThread = settings.appendCrossThread ?? false;
  32.  
  33.  
  34. const FAVICON_URL = window.location.hostname === 'boards.holotower.org'
  35. ? 'https://boards.holotower.org/favicon.gif'
  36. : 'https://holotower.org/favicon.gif';
  37. let alertState = 'none';
  38.  
  39. let lastPostCount = null;
  40. let lastSeenPostId = 0;
  41. let lastLine = 0;
  42. let hasUnreadLine = false;
  43. let previousStyle = '';
  44. let currentThreadId = null;
  45. let lowPostWarningCount = 0;
  46. let isLargeDrop = false;
  47.  
  48. function setFavicon(url) {
  49. let link = document.querySelector("link[rel*='icon']") || document.createElement('link');
  50. link.type = 'image/x-icon';
  51. link.rel = 'shortcut icon';
  52. link.href = url;
  53. if (!link.parentNode) document.head.appendChild(link);
  54. }
  55.  
  56. function updateFavicon(color) {
  57. if (alertState === 'red' && color === 'white') return;
  58. if (alertState === color) return;
  59.  
  60. const img = new Image();
  61. img.onload = () => {
  62. const canvas = document.createElement('canvas');
  63. canvas.width = img.width;
  64. canvas.height = img.height;
  65. const ctx = canvas.getContext('2d');
  66. ctx.drawImage(img, 0, 0);
  67.  
  68. ctx.beginPath();
  69. ctx.arc(canvas.width - 16, 16, 5, 0, 2 * Math.PI);
  70. ctx.fillStyle = color;
  71. ctx.fill();
  72.  
  73. setFavicon(canvas.toDataURL('image/x-icon'));
  74. alertState = color;
  75. };
  76. img.src = FAVICON_URL;
  77. }
  78.  
  79. function updateFaviconArchived() {
  80. if (!changeFaviconOnArchive) return;
  81.  
  82. const img = new Image();
  83. img.onload = () => {
  84. const canvas = document.createElement('canvas');
  85. canvas.width = img.width;
  86. canvas.height = img.height;
  87. const ctx = canvas.getContext('2d');
  88. ctx.drawImage(img, 0, 0);
  89.  
  90. ctx.globalCompositeOperation = 'source-atop';
  91. ctx.fillStyle = 'red';
  92. ctx.fillRect(0, 0, canvas.width, canvas.height);
  93.  
  94. setFavicon(canvas.toDataURL('image/x-icon'));
  95. };
  96. img.src = FAVICON_URL;
  97. }
  98.  
  99. function revertFavicon() {
  100. if (alertState !== 'none') {
  101. setFavicon(FAVICON_URL);
  102. alertState = 'none';
  103. }
  104. }
  105.  
  106. window.addEventListener('scroll', () => {
  107. if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2) {
  108. revertFavicon();
  109. removeUnreadLine();
  110. }
  111. });
  112.  
  113. const appendedPostIds = new Set();
  114.  
  115. function appendPosts(post) {
  116. const body = post.querySelector('.body');
  117. if (!body) return;
  118.  
  119. const postId = post.id.split('_')[1];
  120. appendedPostIds.add(postId);
  121.  
  122. const smallTags = body.querySelectorAll('small');
  123. let visibleCount = 0;
  124.  
  125. for (let j = 0; j < smallTags.length; j++) {
  126. if (getComputedStyle(smallTags[j]).display !== 'none') visibleCount++;
  127. }
  128.  
  129. const skipAppendingYou = visibleCount < smallTags.length;
  130.  
  131. for (let j = 0; j < smallTags.length; j++) {
  132. const small = smallTags[j];
  133. const label = small.textContent.trim();
  134. if (label !== '(You)' && label !== '(OP)') continue;
  135.  
  136. const isVisible = getComputedStyle(small).display !== 'none';
  137. let target = small.previousSibling;
  138. while (target && (target.nodeType !== 1 || target.tagName !== 'A')) {
  139. target = target.previousSibling;
  140. }
  141.  
  142. if (target?.tagName !== 'A') continue;
  143.  
  144. if (isVisible) {
  145. small.setAttribute('style', 'display: none !important;');
  146. if (label === '(You)' && skipAppendingYou) continue;
  147. if (!target.textContent.includes(label)) {
  148. target.textContent += ` ${label}`;
  149. }
  150. } else if (label !== '(You)' && !target.textContent.includes(label)) {
  151. target.textContent += ` ${label}`;
  152. }
  153. }
  154.  
  155. const links = body.querySelectorAll('a[href*="/res/"]');
  156. for (let j = 0; j < links.length; j++) {
  157. const link = links[j];
  158. const href = link.getAttribute('href');
  159. const match = href?.match(/\/res\/(\d+)\.html#(\d+)/);
  160. if (!match) continue;
  161.  
  162. const linkThreadId = match[1];
  163. const quotedPostId = match[2];
  164.  
  165. const hasFollowingSmall = link.nextSibling &&
  166. link.nextSibling.nodeType === 1 &&
  167. link.nextSibling.tagName === 'SMALL';
  168.  
  169. if (quotedPostId === currentThreadId && !hasFollowingSmall && !link.textContent.includes('(OP)')) {
  170. link.textContent += ' (OP)';
  171. }
  172.  
  173. if (linkThreadId !== currentThreadId && !hasFollowingSmall && !link.textContent.includes('→') && !link.textContent.includes('(Cross-thread)')) {
  174. link.textContent += appendCrossThread ? ' (Cross-thread)' : ' →';
  175. }
  176. }
  177. }
  178.  
  179. function initializePosts() {
  180. const lastPostCountEl = document.getElementById("thread_stats_posts");
  181. if (lastPostCountEl) {
  182. lastPostCount = parseInt(lastPostCountEl.textContent, 10);
  183. }
  184.  
  185. const posts = document.querySelectorAll('div.post.reply');
  186. if (posts.length) {
  187. const lastPost = posts[posts.length - 1];
  188. const lastPostId = lastPost.id.split('_')[1];
  189. lastSeenPostId = lastPostId;
  190. lastLine = lastPostId;
  191. }
  192.  
  193. if (appendQuotes) {
  194. for (let i = 0; i < posts.length; i++) {
  195. const post = posts[i];
  196. const postId = post.id.split('_')[1];
  197.  
  198. if (!appendedPostIds.has(postId)) {
  199. appendPosts(post);
  200. }
  201. }
  202. }
  203. }
  204.  
  205. function addUnreadLine() {
  206. if (!showUnreadLine) return;
  207.  
  208. const lastPost = document.querySelector(`#reply_${lastLine}`);
  209. if (lastPost && !hasUnreadLine) {
  210. previousStyle = lastPost.getAttribute('style') || '';
  211. lastPost.style.boxShadow = '0 3px red';
  212. hasUnreadLine = true;
  213. }
  214. }
  215.  
  216. function removeUnreadLine() {
  217. if (!showUnreadLine) return;
  218.  
  219. const lastPost = document.querySelector(`#reply_${lastLine}`);
  220. if (lastPost && hasUnreadLine) {
  221. if (previousStyle) {
  222. lastPost.setAttribute('style', previousStyle);
  223. } else {
  224. lastPost.removeAttribute('style');
  225. }
  226. lastLine = lastSeenPostId;
  227. hasUnreadLine = false;
  228. }
  229. }
  230.  
  231. function updateThreadStatsActual() {
  232. if (lastPostCount !== null) {
  233. const oldElement = document.getElementById('thread_stats_posts');
  234. if (oldElement) {
  235. oldElement.style.display = 'none';
  236.  
  237. let newElement = document.getElementById('thread_stats_posts_actual');
  238. if (!newElement) {
  239. newElement = document.createElement('span');
  240. newElement.id = 'thread_stats_posts_actual';
  241. oldElement.parentNode.insertBefore(newElement, oldElement.nextSibling);
  242. }
  243.  
  244. newElement.textContent = lastPostCount;
  245.  
  246. const postsDeleted = parseInt(oldElement.textContent) - lastPostCount;
  247.  
  248. if (showDeletedCounter) {
  249. let deletedElement = document.getElementById('thread_stats_posts_deleted');
  250. if (postsDeleted >= 1) {
  251. if (!deletedElement) {
  252. deletedElement = document.createElement('span');
  253. deletedElement.id = 'thread_stats_posts_deleted';
  254. const imagesElement = document.getElementById('thread_stats_images');
  255. if (imagesElement) {
  256. imagesElement.parentNode.insertBefore(deletedElement, imagesElement);
  257. }
  258. }
  259.  
  260. deletedElement.textContent = postsDeleted;
  261. deletedElement.insertAdjacentHTML('beforeend', ' deleted |&nbsp;');
  262. } else if (deletedElement) {
  263. deletedElement.remove();
  264. }
  265. }
  266.  
  267. if (lastPostCount >= 1500) {
  268. if (enableFaviconChanges && changeFaviconOnArchive) {
  269. updateFaviconArchived();
  270. }
  271. if (showArchivedMessage && !document.getElementById('archived-msg')) {
  272. addArchivedMessage();
  273. }
  274. }
  275. }
  276. }
  277. }
  278.  
  279. function addArchivedMessage() {
  280. const postControlsForm = document.forms["postcontrols"];
  281. if (!postControlsForm) return;
  282.  
  283. const archivedMsg = document.createElement('div');
  284. archivedMsg.id = 'archived-msg';
  285. archivedMsg.style.marginTop = '-25px';
  286. archivedMsg.style.marginBottom = '20px';
  287.  
  288. const messageText = settings.archivedMessageText || "THREAD ARCHIVED";
  289. const imageURL = settings.archivedImageURL || "https://i.imgur.com/LQHVLil.png";
  290. const fontSize = settings.archivedMessageFontSize || "14px";
  291. const imageSize = settings.archivedImageSize || "7%";
  292. const useHeight = settings.archivedImageUseHeight;
  293.  
  294. archivedMsg.innerHTML = `
  295. <strong style="color: red; font-size: ${fontSize};">${messageText}</strong><br>
  296. <img src="${imageURL}" alt="Archived Image" style="margin-top: 5px; ${useHeight ? `height` : `width`}: ${imageSize};">
  297. `;
  298.  
  299. postControlsForm.parentNode.insertBefore(archivedMsg, postControlsForm.nextSibling);
  300. }
  301.  
  302. function initializeCurrentThreadId() {
  303. const match = window.location.pathname.match(/\/res\/(\d+)\.html/);
  304. if (match) {
  305. currentThreadId = match[1];
  306. }
  307. }
  308.  
  309. (function(open) {
  310. XMLHttpRequest.prototype.open = function(method, url) {
  311. if (currentThreadId) {
  312. const target = `/res/${currentThreadId}.html`;
  313. this._monitored = url.includes(target) && url.indexOf(target) + target.length === url.length;
  314. } else {
  315. this._monitored = false;
  316. }
  317. return open.apply(this, arguments);
  318. };
  319. })(XMLHttpRequest.prototype.open);
  320.  
  321. (function(send) {
  322. XMLHttpRequest.prototype.send = function() {
  323. if (this._monitored) {
  324. this.addEventListener('load', () => {
  325. if (this.status === 200) {
  326. syncPostStatus(this.responseText);
  327. } else if (this.status === 404) {
  328. if (enableFaviconChanges && changeFaviconOnArchive) {
  329. updateFaviconArchived();
  330. }
  331. if (showArchivedMessage && !document.getElementById('archived-msg')) {
  332. addArchivedMessage();
  333. }
  334. }
  335. });
  336. }
  337. return send.apply(this, arguments);
  338. };
  339. })(XMLHttpRequest.prototype.send);
  340.  
  341. const justPostedIds = new Set();
  342.  
  343. $(document).on('ajax_after_post', function(e, post_response) {
  344. if (post_response && post_response.id) {
  345. const idStr = String(post_response.id);
  346. justPostedIds.add(idStr);
  347. setTimeout(() => justPostedIds.delete(idStr), 10000);
  348. }
  349. });
  350.  
  351. function syncPostStatus(responseText) {
  352. if (!currentThreadId) return;
  353. const parser = new DOMParser();
  354. const doc = parser.parseFromString(responseText, "text/html");
  355. const currentPosts = document.querySelectorAll('div.post.reply');
  356. const updatePosts = doc.querySelectorAll('div.post.reply');
  357. const postCount = updatePosts.length;
  358.  
  359. if (postCount > 0) {
  360. const postDrop = lastPostCount - postCount;
  361. isLargeDrop = lastPostCount > 200 ? postDrop >= 100 : (postDrop / lastPostCount) >= 0.3;
  362.  
  363. if (isLargeDrop && lastPostCount >= 10) {
  364. lowPostWarningCount++;
  365.  
  366. if (lowPostWarningCount >= 2) {
  367. lastPostCount = postCount;
  368. updateThreadStatsActual();
  369. }
  370. } else {
  371. lowPostWarningCount = 0;
  372. lastPostCount = postCount;
  373. updateThreadStatsActual();
  374. }
  375. }
  376.  
  377. if (postCount === 0) return;
  378.  
  379. const updatePostIds = new Set();
  380. let seenRed = false;
  381. let seenWhite = false;
  382. let ownPostIds = new Set();
  383. if (enableFaviconChanges && notifyNewYou && alertState !== 'red') {
  384. try {
  385. const board = document.querySelector('input[name="board"]')?.value;
  386. const ownPosts = JSON.parse(localStorage.own_posts || '{}');
  387. ownPostIds = board && ownPosts[board] ? new Set(ownPosts[board]) : new Set();
  388. } catch (e) {
  389.  
  390. }
  391. }
  392.  
  393. for (let i = 0; i < updatePosts.length; i++) {
  394. const post = updatePosts[i];
  395. const postId = post.id.split('_')[1];
  396. updatePostIds.add(postId);
  397.  
  398. if (Number(postId) > Number(lastSeenPostId)) {
  399. lastSeenPostId = postId;
  400. if (!hasUnreadLine) addUnreadLine();
  401.  
  402. if (enableFaviconChanges) {
  403. if (notifyNewYou && alertState !== 'red') {
  404. const bodyLinks = post.querySelectorAll('div.body a:not([rel="nofollow"])');
  405. for (let j = 0; j < bodyLinks.length; j++) {
  406. const match = bodyLinks[j].textContent.match(/^>>(\d+)$/);
  407. if (match && ownPostIds.has(match[1])) {
  408. seenRed = true;
  409. break;
  410. }
  411. }
  412. }
  413.  
  414. if (!seenRed && notifyNewPost) {
  415. seenWhite = true;
  416. }
  417. }
  418. }
  419.  
  420. let currentPost = null;
  421.  
  422. const updateBans = post.querySelector('span.public_ban');
  423. if (updateBans) {
  424. currentPost = currentPost || document.getElementById('reply_' + postId);
  425. if (currentPost && !currentPost.querySelector('span.public_ban')) {
  426. const currentBody = currentPost.querySelector('div.body');
  427. if (currentBody) {
  428. currentBody.appendChild(updateBans.cloneNode(true));
  429. }
  430. }
  431. }
  432.  
  433. if (appendQuotes && !appendedPostIds.has(postId)) {
  434. currentPost = currentPost || document.getElementById('reply_' + postId);
  435. if (currentPost) {
  436. appendPosts(currentPost);
  437. }
  438. }
  439. }
  440.  
  441. if (seenRed) {
  442. updateFavicon('red');
  443. } else if (seenWhite) {
  444. updateFavicon('white');
  445. }
  446.  
  447. if ((hideDeleted || showDeletedIcon) && (!isLargeDrop || lowPostWarningCount >= 2)) {
  448. for (let i = 0; i < currentPosts.length; i++) {
  449. const post = currentPosts[i];
  450. if (post.closest('.post.qp.reply') || post.classList.contains('post-hover')) continue;
  451.  
  452. const postId = post.id.split('_')[1];
  453. const threadEl = post.closest('.thread');
  454. if (!threadEl) continue;
  455. const xthreadId = threadEl.id.split('_')[1];
  456. if (xthreadId !== currentThreadId) continue;
  457.  
  458. const inlineQuoteContainer = post.closest('.inline-quote-container');
  459. const refId = inlineQuoteContainer?.getAttribute('data-inlined-id') || postId;
  460.  
  461. const isDeleted = !updatePostIds.has(refId) && !justPostedIds.has(refId);
  462.  
  463. if (isDeleted) {
  464. if (hideDeleted) {
  465. if (!inlineQuoteContainer) {
  466. post.style.setProperty("display", "none", "important");
  467. const br = post.nextElementSibling;
  468. if (br?.tagName === "BR") {
  469. br.style.setProperty("display", "none", "important");
  470. }
  471. }
  472. } else {
  473. addDeletedIcon(post);
  474. }
  475. } else {
  476. post.style.removeProperty("display");
  477. const br = post.nextElementSibling;
  478. if (br?.tagName === "BR") {
  479. br.style.removeProperty("display");
  480. }
  481. if (!hideDeleted && post.querySelector('.deleted-post')) {
  482. removeDeletedIcon(post);
  483. }
  484. }
  485. }
  486. }
  487. }
  488.  
  489. function addDeletedIcon(post) {
  490. if (!showDeletedIcon) return;
  491. if (!post.querySelector('.post-btn')) return;
  492.  
  493. const postNoLink = post.querySelector('.post_no');
  494. if (!postNoLink || post.querySelector('.deleted-post')) return;
  495.  
  496. if (showDeletedText) {
  497. const span = document.createElement('span');
  498. span.textContent = ' [Deleted]';
  499. span.className = 'deleted-post';
  500. span.style.color = 'red';
  501. span.style.fontWeight = 'bolder';
  502. postNoLink.parentNode.insertBefore(span, postNoLink.nextSibling?.nextSibling || null);
  503. } else {
  504. const icon = document.createElement('i');
  505. icon.classList.add('fa', 'fa-trash', 'deleted-post');
  506. icon.title = 'Deleted';
  507. icon.style.opacity = '0.5';
  508. icon.style.marginRight = '0px';
  509. postNoLink.parentNode.insertBefore(icon, postNoLink.nextSibling?.nextSibling || null);
  510. }
  511. }
  512.  
  513. function removeDeletedIcon(post) {
  514. if (!showDeletedIcon) return;
  515.  
  516. const postNoLink = post.querySelector('.post_no');
  517. const deletedPost = postNoLink?.nextSibling?.nextSibling;
  518. if (deletedPost?.classList.contains('deleted-post')) {
  519. deletedPost.remove();
  520. }
  521. }
  522.  
  523. $(function () {
  524. if (typeof Options === "undefined") return;
  525.  
  526. const SETTINGS_KEY = "Thread Settings";
  527.  
  528. const settingsList = [
  529. { key: "showDeletedCounter", label: "Deleted Count", description: "Show deleted post counter" },
  530. { key: "hideDeletedPosts", label: "Hide Deleted Posts", description: "Hide posts upon deletion" },
  531. { key: "showDeletedIcon", label: "Deleted Icon", description: "Add an icon next to deleted posts" },
  532. { key: "faviconUpdater", label: "Favicon Updater", description: "Enable favicon changes" },
  533. { key: "showUnreadLine", label: "Unread Line", description: "Show a line below the last post" },
  534. { key: "appendQuotes", label: "Append Quotes", description: "Append '(OP)' '(You)' '→' to applicable quotes" },
  535. { key: "showArchivedMessage", label: "Archived Message", description: "Show archived message on post limit" }
  536. ];
  537.  
  538. const subSettings = [
  539. { key: "showDeletedText", label: "Deleted Text", description: 'Add <span style="color:red;font-weight:bolder">[Deleted]</span> text instead', parentKey: "showDeletedIcon" },
  540. { key: "notifyNewPost", label: "New Post", description: "White circle in favicon on new posts", parentKey: "faviconUpdater" },
  541. { key: "notifyNewYou", label: "New (You)", description: "Red circle in favicon on new (You) quotes", parentKey: "faviconUpdater" },
  542. { key: "changeFaviconOnArchive", label: "Archived", description: "Favicon turns red on post limit", parentKey: "faviconUpdater" },
  543. { key: "appendCrossThread", label: "Append (Cross-thread)", description: "Use (Cross-thread) instead", parentKey: "appendQuotes" },
  544. { key: "archivedMessageText", label: "Text", parentKey: "showArchivedMessage", type: "text" },
  545. { key: "archivedImageURL", label: "Image URL", parentKey: "showArchivedMessage", type: "text" },
  546. { key: "archivedMessageFontSize", label: "Font Size", description: "Use with 'px' or '%'", parentKey: "showArchivedMessage", type: "text", size: "1", after: "archivedMessageText" },
  547. { key: "archivedImageSize", label: "Image Width", description: "Use with 'px' or '%'", parentKey: "showArchivedMessage", type: "text", size: "1", after: "archivedImageURL" },
  548. { key: "archivedImageUseHeight", label: "Use Height", description: "Instead of width for image", parentKey: "showArchivedMessage", after: "archivedImageSize" }
  549. ];
  550.  
  551. const defaultSettings = {
  552. showDeletedCounter: true,
  553. showDeletedIcon: true,
  554. hideDeletedPosts: false,
  555. showArchivedMessage: true,
  556. faviconUpdater: true,
  557. showUnreadLine: true,
  558. appendQuotes: true,
  559. notifyNewPost: true,
  560. notifyNewYou: true,
  561. changeFaviconOnArchive: true,
  562. showDeletedText: false,
  563. appendCrossThread: false,
  564. archivedMessageText: "THREAD ARCHIVED",
  565. archivedImageURL: "https://i.imgur.com/LQHVLil.png",
  566. archivedMessageFontSize: "14px",
  567. archivedImageSize: "7%",
  568. archivedImageUseHeight: false,
  569. appendQuotesWarning: false
  570. };
  571.  
  572. let threadSettings = {};
  573. try {
  574. threadSettings = JSON.parse(localStorage.getItem(SETTINGS_KEY)) || {};
  575. } catch {
  576. threadSettings = {};
  577. }
  578.  
  579. for (const key in defaultSettings) {
  580. if (!(key in threadSettings)) {
  581. threadSettings[key] = defaultSettings[key];
  582. }
  583. }
  584.  
  585. const saveSettings = () => {
  586. localStorage.setItem(SETTINGS_KEY, JSON.stringify(threadSettings));
  587. };
  588.  
  589. const content = $("<div></div>");
  590.  
  591. settingsList.forEach(({ key, label, description }) => {
  592. const isChecked = threadSettings[key];
  593. const descSpan = description ? `<span class="description">: ${description}</span>` : "";
  594. const checkbox = $(`
  595. <div id="${key}-container">
  596. <label style="text-decoration: underline; cursor: pointer;">
  597. <input type="checkbox" id="${key}" ${isChecked ? "checked" : ""} name="${label}">${label}</label>${descSpan}
  598. </div>
  599. `);
  600. content.append(checkbox);
  601. });
  602.  
  603. subSettings.forEach((s) => {
  604. const { key, label, description, type = "checkbox" } = s;
  605. const value = threadSettings[key];
  606. const descSpan = description ? `<span class="description">: ${description}</span>` : "";
  607.  
  608. let input;
  609. if (type === "text") {
  610. const inputSize = s.size || "20";
  611. input = `<input type="text" id="${key}" value="${value}" size="${inputSize}" style="margin-right: 3px;">`;
  612. } else {
  613. input = `<input type="checkbox" id="${key}" ${value ? "checked" : ""}>`;
  614. }
  615.  
  616. const container = $(`
  617. <div id="${key}-container" style="margin-left: 1.5em;">
  618. <label style="text-decoration: underline; cursor: pointer;">
  619. ${input}${label}</label>${descSpan}
  620. </div>
  621. `);
  622.  
  623. content.append(container);
  624. });
  625.  
  626. const previewWrapper = $(`
  627. <div id="archived-preview" style="
  628. position: absolute;
  629. bottom: 0px;
  630. right: 0px;
  631. width: 220px;
  632. height: 90px;
  633. overflow: hidden;
  634. pointer-events: none;
  635. transform-origin: top left;
  636. ">
  637. </div>
  638. `);
  639. content.css("position", "relative");
  640. content.append(previewWrapper);
  641.  
  642. function renderArchivedPreview() {
  643. const rawFontSize = threadSettings.archivedMessageFontSize || defaultSettings.archivedMessageFontSize || "14px";
  644. const rawImageSize = threadSettings.archivedImageSize || defaultSettings.archivedImageSize || "7%";
  645. const useHeight = threadSettings.archivedImageUseHeight;
  646. const msg = threadSettings.archivedMessageText || defaultSettings.archivedMessageText || "THREAD ARCHIVED";
  647. const img = threadSettings.archivedImageURL || defaultSettings.archivedImageURL || "https://i.imgur.com/LQHVLil.png";
  648.  
  649. const convertSize = (raw, isHeight = false) => {
  650. if (raw.endsWith('%')) {
  651. const percent = parseFloat(raw);
  652. const base = isHeight ? window.innerHeight : window.innerWidth;
  653. return `${(percent / 100) * base}px`;
  654. }
  655. return raw;
  656. };
  657.  
  658. const fontSize = convertSize(rawFontSize);
  659. const imageSize = convertSize(rawImageSize, useHeight);
  660.  
  661. const previewContent = $(`
  662. <div style="display: inline-block; white-space: nowrap;">
  663. <div style="display: inline-block;">
  664. <strong style="color: red; font-size: ${fontSize}; display: inline-block; white-space: nowrap;">${msg}</strong><br>
  665. <img src="${img}" style="margin-top: 5px; display: inline-block; ${useHeight ? 'height' : 'width'}: ${imageSize};">
  666. </div>
  667. </div>
  668. `);
  669.  
  670. previewWrapper.empty().append(previewContent);
  671.  
  672. const imageEl = previewContent.find('img')[0];
  673.  
  674. const applyScale = (scale) => {
  675. previewContent.css({
  676. transform: `scale(${scale})`,
  677. transformOrigin: "top left"
  678. });
  679. };
  680.  
  681. const tryScale = () => {
  682. const bounds = previewContent[0].getBoundingClientRect();
  683. const width = bounds.width;
  684. const height = bounds.height;
  685.  
  686. if (width && height) {
  687. const scaleX = 220 / width;
  688. const scaleY = 90 / height;
  689. const scale = Math.min(scaleX, scaleY, 1);
  690.  
  691. applyScale(scale);
  692. threadSettings.archivedPreviewScale = scale.toFixed(6);
  693. } else {
  694. const fallbackScale = parseFloat(threadSettings.archivedPreviewScale);
  695. if (fallbackScale && isFinite(fallbackScale)) {
  696. applyScale(fallbackScale);
  697. }
  698. }
  699. };
  700.  
  701. if (imageEl.complete) {
  702. tryScale();
  703. } else {
  704. imageEl.onload = tryScale;
  705. }
  706. }
  707.  
  708. Options.add_tab("thread-status", "cog", "Thread Options", content);
  709.  
  710. subSettings.forEach(({ key, parentKey, after }) => {
  711. const parent = $(`#${parentKey}-container`);
  712. const insertAfter = after ? $(`#${after}-container`) : parent;
  713. $(`#${key}-container`).insertAfter(insertAfter);
  714. });
  715.  
  716. settingsList.forEach(({ key }) => {
  717. $(`#${key}`).on("change", function () {
  718. threadSettings[key] = this.checked;
  719. saveSettings();
  720.  
  721. subSettings.forEach(({ key: subKey, parentKey }) => {
  722. if (parentKey === key) {
  723. const disabled = !this.checked;
  724. const label = $(`#${subKey}`).closest("label");
  725. const desc = label.next(".description");
  726. $(`#${subKey}`).prop("disabled", disabled);
  727. label.css("opacity", disabled ? 0.5 : 1);
  728. desc.css("opacity", disabled ? 0.5 : 1);
  729. }
  730. if (key === "showArchivedMessage") {
  731. $("#archived-preview").toggle(this.checked);
  732. }
  733. });
  734. });
  735. });
  736.  
  737. subSettings.forEach(({ key, type = "checkbox" }) => {
  738. const $input = $(`#${key}`);
  739. $input.on("change input", function () {
  740. if (type === "text") {
  741. const trimmed = this.value.trim();
  742. if (key === "archivedImageSize" && !trimmed) {
  743. threadSettings[key] = defaultSettings[key];
  744. threadSettings.archivedImageUseHeight = defaultSettings.archivedImageUseHeight;
  745. $(`#archivedImageUseHeight`).prop("checked", defaultSettings.archivedImageUseHeight);
  746. } else {
  747. threadSettings[key] = trimmed || defaultSettings[key];
  748. }
  749. } else {
  750. threadSettings[key] = this.checked;
  751. }
  752. saveSettings();
  753. renderArchivedPreview();
  754. });
  755. });
  756.  
  757. $("#appendQuotes").on("change", function () {
  758. const checked = $(this).is(":checked");
  759.  
  760. if (checked && !threadSettings.appendQuotesWarning) {
  761. const alert = $(`
  762. <div id="alert_handler">
  763. <div id="alert_background"></div>
  764. <div id="alert_div">
  765. <a id="alert_close" href="javascript:void(0)">
  766. <i class="fa fa-times"></i>
  767. </a>
  768. <div id="alert_message">
  769. <strong>⚠ Compatibility Notice</strong><br><br>
  770. This feature is compatible with the latest version of the <em>Inline Quoting</em> script.<br><br>
  771. Update to <strong>version 2.1</strong> or later if you are experiencing issues with inline quoting appended replies.
  772. </div>
  773. <div style="margin: 13px;">
  774. <label><input type="checkbox" id="appendQuotesWarning">Don't show this message again</label>
  775. </div>
  776. <div>
  777. <button class="button alert_button" id="appendQuotesOk">OK</button>
  778. </div>
  779. </div>
  780. </div>
  781. `);
  782.  
  783. $("body").append(alert);
  784.  
  785. $("#appendQuotesOk").on("click", function () {
  786. if ($("#appendQuotesWarning").is(":checked")) {
  787. threadSettings.appendQuotesWarning = true;
  788. saveSettings();
  789. }
  790. $("#alert_handler").remove();
  791. });
  792.  
  793. $("#alert_close").on("click", function () {
  794. $("#appendQuotes").prop("checked", false);
  795. threadSettings.appendQuotes = false;
  796. saveSettings();
  797. $("#alert_handler").remove();
  798. });
  799. }
  800. });
  801.  
  802. subSettings.forEach(({ key, parentKey }) => {
  803. const isParentChecked = threadSettings[parentKey];
  804. const label = $(`#${key}`).closest("label");
  805. const desc = label.next(".description");
  806. $(`#${key}`).prop("disabled", !isParentChecked);
  807. label.css("opacity", isParentChecked ? 1 : 0.5);
  808. desc.css("opacity", isParentChecked ? 1 : 0.5);
  809. $("#archived-preview").toggle(threadSettings.showArchivedMessage);
  810. });
  811. renderArchivedPreview();
  812. saveSettings();
  813. });
  814.  
  815. if (document.readyState === 'loading') {
  816. window.addEventListener('DOMContentLoaded', () => {
  817. initializeCurrentThreadId();
  818. initializePosts();
  819. });
  820. } else {
  821. initializeCurrentThreadId();
  822. initializePosts();
  823. }
  824. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement