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