8chan Single ID Post Opacity with Thread-Specific Cross-Domain Toggle

Halves opacity of posts with unique labelId (based on background-color) if CHECK_UNIQUE_IDS is true, adds a circle emoji toggle after extraMenuButton to adjust opacity for all posts by ID color in the same thread (including OP), persists toggle state across 8chan.moe and 8chan.se, handles dynamically added posts, forces 100% opacity for posts with expanded images, and sets opacity to 100% on hover

  1. // ==UserScript==
  2. // @name 8chan Single ID Post Opacity with Thread-Specific Cross-Domain Toggle
  3. // @namespace https://8chan.moe
  4. // @description Halves opacity of posts with unique labelId (based on background-color) if CHECK_UNIQUE_IDS is true, adds a circle emoji toggle after extraMenuButton to adjust opacity for all posts by ID color in the same thread (including OP), persists toggle state across 8chan.moe and 8chan.se, handles dynamically added posts, forces 100% opacity for posts with expanded images, and sets opacity to 100% on hover
  5. // @match https://8chan.moe/*/res/*
  6. // @match https://8chan.se/*/res/*
  7. // @version 2.6
  8. // @author Anonymous
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Global constants
  18. const CHECK_UNIQUE_IDS = true; // Enable/disable unique ID opacity check
  19. const TRANSPARENCY_FACTOR = 0.5; // Opacity for unique or toggled posts
  20.  
  21. // Function to extract board and thread from URL and create a domain-agnostic storage key
  22. function getThreadInfo() {
  23. const url = window.location.href;
  24. const regex = /https:\/\/8chan\.(moe|se)\/([^/]+)\/res\/(\d+)\.html/;
  25. const match = url.match(regex);
  26. if (match) {
  27. return {
  28. board: match[2],
  29. thread: match[3],
  30. storageKey: `toggledColors_${match[2]}_${match[3]}`
  31. };
  32. }
  33. return null;
  34. }
  35.  
  36. // Wait for the DOM to be fully loaded
  37. window.addEventListener('load', function() {
  38. const threadInfo = getThreadInfo();
  39. if (!threadInfo) {
  40. console.error('Could not parse board and thread from URL');
  41. return;
  42. }
  43.  
  44. const storageKey = threadInfo.storageKey;
  45. let toggledColors = GM_getValue(storageKey, []);
  46. if (!Array.isArray(toggledColors)) {
  47. toggledColors = [];
  48. GM_setValue(storageKey, toggledColors);
  49. }
  50.  
  51. const colorCount = new Map();
  52.  
  53. function updateColorCounts() {
  54. colorCount.clear();
  55. document.querySelectorAll('.labelId').forEach(label => {
  56. const bgColor = label.style.backgroundColor;
  57. if (bgColor) {
  58. colorCount.set(bgColor, (colorCount.get(bgColor) || 0) + 1);
  59. }
  60. });
  61. }
  62.  
  63. function createToggleIcon(container, bgColor) {
  64. if (container.querySelector('.opacityToggle')) return;
  65.  
  66. const icon = document.createElement('label');
  67. icon.textContent = '⚪';
  68. icon.style.cursor = 'pointer';
  69. icon.style.margin = '0 2px 0 2px'; // Override inherited margin, keep 2px left/right
  70. icon.style.verticalAlign = 'top'; // Align top with buttons
  71. icon.style.display = 'inline-block'; // Match button display
  72. icon.style.color = toggledColors.includes(bgColor) ? '#00ff00' : '#808080';
  73. icon.className = 'opacityToggle glowOnHover coloredIcon';
  74. icon.title = 'Toggle opacity for this ID in this thread';
  75.  
  76. // Insert icon after extraMenuButton
  77. const extraMenuButton = container.querySelector('.extraMenuButton');
  78. if (extraMenuButton) {
  79. extraMenuButton.insertAdjacentElement('afterend', icon);
  80. } else {
  81. // Fallback: append to container
  82. container.appendChild(icon);
  83. }
  84.  
  85. icon.addEventListener('click', () => {
  86. if (toggledColors.includes(bgColor)) {
  87. toggledColors = toggledColors.filter(color => color !== bgColor);
  88. } else {
  89. toggledColors.push(bgColor);
  90. }
  91. GM_setValue(storageKey, toggledColors);
  92.  
  93. icon.style.color = toggledColors.includes(bgColor) ? '#00ff00' : '#808080';
  94.  
  95. document.querySelectorAll('.innerOP, .innerPost').forEach(p => {
  96. const label = p.querySelector('.labelId');
  97. if (label && label.style.backgroundColor === bgColor) {
  98. updatePostOpacity(p);
  99. }
  100. });
  101. });
  102. }
  103.  
  104. function updatePostOpacity(post) {
  105. const labelId = post.querySelector('.labelId');
  106. if (labelId) {
  107. const bgColor = labelId.style.backgroundColor;
  108. if (bgColor) {
  109. const figure = post.querySelector('figure');
  110. if (figure && figure.classList.contains('expandedCell')) {
  111. post.style.opacity = '1';
  112. } else {
  113. let shouldBeOpaque = toggledColors.includes(bgColor) || (CHECK_UNIQUE_IDS && colorCount.get(bgColor) === 1);
  114. post.style.opacity = shouldBeOpaque ? TRANSPARENCY_FACTOR : '1';
  115. }
  116. }
  117. }
  118. }
  119.  
  120. function processPost(post, isOP = false) {
  121. const labelId = post.querySelector('.labelId');
  122. if (labelId) {
  123. const bgColor = labelId.style.backgroundColor;
  124. if (bgColor) {
  125. updatePostOpacity(post);
  126.  
  127. const title = post.querySelector(isOP ? '.opHead.title' : '.postInfo.title');
  128. if (title) {
  129. createToggleIcon(title, bgColor);
  130. }
  131.  
  132. // Observe figure for class changes
  133. const figure = post.querySelector('figure');
  134. if (figure) {
  135. const observer = new MutationObserver(() => {
  136. updatePostOpacity(post);
  137. });
  138. observer.observe(figure, { attributes: true, attributeFilter: ['class'] });
  139. }
  140.  
  141. // Add hover event listeners
  142. post.addEventListener('mouseover', () => {
  143. post.style.opacity = '1';
  144. });
  145. post.addEventListener('mouseout', () => {
  146. updatePostOpacity(post);
  147. });
  148. }
  149. }
  150. }
  151.  
  152. // Initial processing
  153. updateColorCounts();
  154. const opPost = document.querySelector('.innerOP');
  155. if (opPost) processPost(opPost, true);
  156. document.querySelectorAll('.innerPost').forEach(post => processPost(post, false));
  157.  
  158. // MutationObserver for new posts
  159. const postsContainer = document.querySelector('.divPosts');
  160. if (postsContainer) {
  161. const observer = new MutationObserver((mutations) => {
  162. let newPosts = false;
  163. mutations.forEach(mutation => {
  164. if (mutation.addedNodes.length) {
  165. mutation.addedNodes.forEach(node => {
  166. if (node.nodeType === Node.ELEMENT_NODE && node.matches('.postCell')) {
  167. const innerPost = node.querySelector('.innerPost');
  168. if (innerPost) newPosts = true;
  169. }
  170. });
  171. }
  172. });
  173.  
  174. if (newPosts) {
  175. updateColorCounts();
  176. document.querySelectorAll('.innerPost').forEach(post => {
  177. if (!post.style.opacity) processPost(post, false);
  178. });
  179. }
  180. });
  181.  
  182. observer.observe(postsContainer, { childList: true, subtree: true });
  183. }
  184. });
  185. })();