Holotower Custom Fixes

Suppress default previews, persistent “You” highlight dropdown, and Q‑key Quick‑Reply for holotower.org.

  1. // ==UserScript==
  2. // @name Holotower Custom Fixes
  3. // @namespace http://holotower.org/
  4. // @version 1.1
  5. // @author /hlgg/
  6. // @license MIT
  7. // @description Suppress default previews, persistent “You” highlight dropdown, and Q‑key Quick‑Reply for holotower.org.
  8. // @icon https://boards.holotower.org/favicon.gif
  9. // @match *://boards.holotower.org/*
  10. // @match *://holotower.org/*
  11. // @grant GM_addStyle
  12. // @run-at document-end
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // Detect whether “Holotower Thread Status Updater” is active
  19. const isThreadUpdaterRunning = !!document.getElementById('htsu-style');
  20.  
  21. // --------------------------------------------------------------------------
  22. // 1) Hide the site's built-in hover previews
  23. // We only want our custom iq-preview-* popups to show.
  24. // --------------------------------------------------------------------------
  25. GM_addStyle(`
  26. /* Any .qp element that does NOT have an id starting with "iq-preview-" will be hidden */
  27. div.qp:not([id^="iq-preview-"]) {
  28. display: none !important;
  29. }
  30. `);
  31.  
  32. // --------------------------------------------------------------------------
  33. // 2) Prepare a <style> tag to hold our dynamic "You" highlighting rules
  34. // We'll update its contents when the user picks a new border style.
  35. // --------------------------------------------------------------------------
  36. const youStyleEl = document.createElement('style');
  37. youStyleEl.id = 'you-custom-style';
  38. document.head.appendChild(youStyleEl);
  39.  
  40. // Key under which we store the user's chosen border style in localStorage
  41. const STORAGE_KEY = 'youBorderStyle';
  42. const DEFAULT_STYLE = 'dashed';
  43.  
  44. /**
  45. * Writes CSS into our <style> tag to highlight posts:
  46. * - Blue left-border for your own replies (.you)
  47. * - Red left-border for replies quoting you (.quoting-you)
  48. * @param {string} style - the CSS border-style (e.g., solid, dashed)
  49. */
  50. function updateYouBorder(style) {
  51. // always keep the border rules...
  52. let css = `
  53. /* Own posts: add blue border */
  54. div.post.reply.you:has(span.own_post) {
  55. border-left: 5px ${style} #00b8e6 !important;
  56. }
  57.  
  58. /* Replies quoting you: add red border */
  59. div.post.reply.quoting-you {
  60. border-left: 5px ${style} #ff3d3d !important;
  61. }
  62. `;
  63. // only insert the “(You)” + hide-original-small rules if Thread Updater is NOT running
  64. if (!isThreadUpdaterRunning) {
  65. css += `
  66. /* Re-add "(You)" text after the quote link */
  67. div.post.reply.quoting-you a:has(+ small)::after {
  68. content: " (You)" !important;
  69. }
  70.  
  71. /* Hide any original small "(You)" or embed placeholders */
  72. div.post.reply div.body a + small,
  73. div.post.reply div.body span.embed-container {
  74. display: none !important;
  75. }
  76. `;
  77. }
  78.  
  79. youStyleEl.textContent = css;
  80.  
  81. // Save the user's choice so it persists across reload
  82. try {
  83. localStorage.setItem(STORAGE_KEY, style);
  84. } catch (e) {
  85. // localStorage may not be available; ignore errors
  86. }
  87. }
  88.  
  89. // --------------------------------------------------------------------------
  90. // 3) Create and insert a dropdown UI for the user to select their border style
  91. // --------------------------------------------------------------------------
  92. const container = document.createElement('div');
  93. container.style.cssText = 'float:right; margin-bottom:10px';
  94. container.innerHTML = `
  95. Border style:
  96. <select id="youBorderSelector">
  97. <option value="solid">solid</option>
  98. <option value="dashed">dashed</option>
  99. <option value="dotted">dotted</option>
  100. <option value="double">double</option>
  101. <option value="groove">groove</option>
  102. <option value="ridge">ridge</option>
  103. </select>
  104. `;
  105.  
  106. // Insert our dropdown before the existing #style-select element (site's theme selector)
  107. const styleSelect = document.getElementById('style-select');
  108. if (styleSelect && styleSelect.parentNode) {
  109. styleSelect.parentNode.insertBefore(container, styleSelect);
  110. }
  111.  
  112. // Initialize dropdown value from localStorage (or default)
  113. const dropdown = document.getElementById('youBorderSelector');
  114. const saved = localStorage.getItem(STORAGE_KEY);
  115. const initial = saved && dropdown.querySelector(`option[value="${saved}"]`) ? saved : DEFAULT_STYLE;
  116. dropdown.value = initial;
  117. updateYouBorder(initial);
  118.  
  119. // Update the border style whenever the user picks a new option
  120. dropdown.addEventListener('change', e => updateYouBorder(e.target.value));
  121.  
  122. // --------------------------------------------------------------------------
  123. // 4) JavaScript marking of posts with "you" and "quoting-you" classes
  124. // .you => posts authored by you
  125. // .quoting-you => replies that quote "(You)"
  126. // --------------------------------------------------------------------------
  127. /**
  128. * Scans the given root (default: whole document) and adds classes:
  129. * - "you" to posts containing <span class="own_post">
  130. * - "quoting-you" to posts containing a <small> with text "(You)"
  131. */
  132. function markYou(root = document.body) {
  133. // Mark own posts
  134. root.querySelectorAll('div.post.reply span.own_post')
  135. .forEach(el => el.closest('div.post.reply')?.classList.add('you'));
  136.  
  137. // Mark replies quoting you
  138. root.querySelectorAll('div.post.reply .body small')
  139. .forEach(sm => {
  140. if (sm.textContent.trim() === '(You)') {
  141. sm.closest('div.post.reply')?.classList.add('quoting-you');
  142. }
  143. });
  144. }
  145.  
  146. // Initial pass on page load
  147. markYou();
  148. // Observe for new or inlined replies to apply marking
  149. new MutationObserver(muts => {
  150. muts.forEach(m => {
  151. m.addedNodes.forEach(n => {
  152. if (n.nodeType === 1) markYou(n);
  153. });
  154. });
  155. }).observe(document.body, { childList: true, subtree: true });
  156.  
  157. // --------------------------------------------------------------------------
  158. // 5) Q-key Quick‑Reply toggle and auto-focus on citeReply links
  159. // --------------------------------------------------------------------------
  160. /**
  161. * Toggles the Quick‑Reply panel when the user presses 'q' (unless typing in a form).
  162. */
  163. function onKey(e) {
  164. if (e.key.toLowerCase() !== 'q' || e.ctrlKey || e.altKey || e.metaKey) return;
  165.  
  166. // If the user is typing into an input or textarea, do nothing
  167. const active = document.activeElement;
  168. const nm = active?.getAttribute('name');
  169. if (active?.tagName === 'TEXTAREA' || (active?.tagName === 'INPUT' && ['name','email','subject','embed'].includes(nm))) {
  170. return;
  171. }
  172.  
  173. const form = document.getElementById('quick-reply');
  174. if (form && form.style.display !== 'none') {
  175. // Close Quick‑Reply if it's open
  176. form.querySelector('.close-btn')?.click();
  177. } else {
  178. // Otherwise open it and focus the textarea
  179. document.querySelector('.quick-reply-btn')?.click();
  180. setTimeout(() => {
  181. document.querySelector('#quick-reply textarea[name="body"]')?.focus();
  182. }, 50);
  183. }
  184. e.preventDefault();
  185. }
  186. // Capture keydown at the documentElement level to override site handlers
  187. document.documentElement.addEventListener('keydown', onKey, true);
  188.  
  189. // When clicking on a post_no link (citeReply), auto-focus the textarea
  190. document.body.addEventListener('click', e => {
  191. const link = e.target.closest('a.post_no');
  192. if (link && /^citeReply\(\d+\)$/.test(link.getAttribute('onclick') || '')) {
  193. setTimeout(() => {
  194. const ta = document.querySelector('#quick-reply textarea[name="body"]');
  195. if (ta) {
  196. ta.focus();
  197. ta.setSelectionRange(ta.value.length, ta.value.length);
  198. }
  199. }, 50);
  200. }
  201. });
  202.  
  203. })();