AH/QQ/SB/SV Mobile readability

Standardize font size to 15px, revert justified text, replace fonts with sans-serif/serif/monospace at calibrated sizes (avoid unsupported/mismatched desktop fonts), dodge colored text from the background and lightly pastelize them to improve contrast, detect centered text and add Uncenter toggle.

  1. // ==UserScript==
  2. // @name AH/QQ/SB/SV Mobile readability
  3. // @description Standardize font size to 15px, revert justified text, replace fonts with sans-serif/serif/monospace at calibrated sizes (avoid unsupported/mismatched desktop fonts), dodge colored text from the background and lightly pastelize them to improve contrast, detect centered text and add Uncenter toggle.
  4. // @author C89sd
  5. // @version 1.10
  6. // @match https://www.alternatehistory.com/*
  7. // @match https://questionablequesting.com/*
  8. // @match https://forum.questionablequesting.com/*
  9. // @match https://forums.spacebattles.com/*
  10. // @match https://forums.sufficientvelocity.com/*
  11. // @grant GM_addStyle
  12. // @namespace https://greasyfork.org/users/1376767
  13. // @noframes
  14. // ==/UserScript==
  15.  
  16. 'use strict';
  17.  
  18. const IS_THREAD = document.URL.includes('/threads/');
  19. if (!IS_THREAD) return;
  20.  
  21. // Centered text toggle
  22. const OP = document.querySelector('.username.u-concealed')?.textContent || '!';
  23. const messages = document.getElementsByClassName('message');
  24. let foundCentered = false;
  25. for (let message of messages) {
  26. if (message.getAttribute('data-author') === OP || message.classList.contains('hasThreadmark')) {
  27. const centeredDiv = message.querySelector('.bbWrapper:first-of-type > div[style*="text-align: center"], .bbWrapper > div.uncentered');
  28. if (centeredDiv) {
  29. foundCentered = true;
  30. break;
  31. }
  32. }
  33. }
  34. if (foundCentered) {
  35. let buttonGrp = document.querySelector('.block-outer-threadmarks .buttonGroup');
  36. let centered = true;
  37. if (buttonGrp) {
  38. buttonGrp.insertAdjacentHTML('afterbegin', '<div class="buttonGroup buttonGroup-buttonWrapper"><span class="button--link button" style="cursor:pointer" title="Toggle center text">Uncenter</span></div>');
  39. buttonGrp.firstElementChild.firstElementChild.addEventListener('click', function() {
  40. document.querySelectorAll(centered ? '.bbWrapper > div[style*="text-align: center"]' : '.bbWrapper > div.uncentered').forEach(el => {
  41. if (centered) {
  42. el.classList.add('uncentered');
  43. el.style.textAlign = '';
  44. } else {
  45. el.style.textAlign = 'center';
  46. }
  47. });
  48. centered = !centered;
  49. });
  50. }
  51. }
  52.  
  53.  
  54. const colorCache = new Map();
  55. const adjustTextColor = (textRGBStr) => {
  56. if (colorCache.has(textRGBStr)) {
  57. return colorCache.get(textRGBStr);
  58. }
  59.  
  60. function toRGB(str) { // can return up to 3 values [] to [r,g,b]
  61. const nums = str.match(/\d+/g);
  62. return nums ? nums.map(Number).slice(0, 3) : [];
  63. }
  64.  
  65. let fg = toRGB(textRGBStr);
  66. const bg = toRGB("rgb(39, 39, 39)");
  67.  
  68. if (fg.length !== 3) { return textRGBStr }
  69.  
  70. const clamp = v => Math.max(0, Math.min(255, v));
  71. const pastelize = ([r, g, b]) => {
  72. const neutral = 128;
  73. const whiteBoost = Math.pow((r + g + b) / (3 * 255), 10);
  74. const factor = 0.8 + 0.1*whiteBoost;
  75. return [
  76. neutral + factor * (r - neutral),
  77. neutral + factor * (g - neutral),
  78. neutral + factor * (b - neutral)
  79. ].map(clamp);
  80. };
  81.  
  82. fg = pastelize(fg);
  83.  
  84. let positiveDiff = 0;
  85. positiveDiff += Math.max(0, fg[0] - 39);
  86. positiveDiff += Math.max(0, fg[1] - 39);
  87. positiveDiff += Math.max(0, fg[2] - 39);
  88.  
  89. const threshold = 180;
  90. if (positiveDiff < threshold) {
  91. fg[0] = Math.max(fg[0], 39);
  92. fg[1] = Math.max(fg[1], 39);
  93. fg[2] = Math.max(fg[2], 39);
  94.  
  95. const avg = (fg[0]+fg[1]+fg[2])/3;
  96. const boost = Math.min(1.0, (Math.abs(fg[0]-avg)+Math.abs(fg[1]-avg)+Math.abs(fg[2]-avg))/255/0.2); // grays = 0, colors >= 1
  97.  
  98. let correction = Math.round((threshold - positiveDiff) / 3 + boost*40);
  99. fg = fg.map(c => Math.min(c + correction, 255));
  100. }
  101.  
  102. const result = `rgb(${fg[0]}, ${fg[1]}, ${fg[2]})`;
  103. colorCache.set(textRGBStr, result);
  104.  
  105. return result;
  106. };
  107.  
  108.  
  109. const SANS_SERIF = /arial|tahoma|trebuchet ms|verdana/;
  110. const MONOSPACE = /courier new/;
  111. const SERIF = /times new roman|georgia|book antiqua/;
  112.  
  113. const wrappers = document.getElementsByClassName('bbWrapper');
  114. for (let wrapper of wrappers) {
  115. wrapper.style.fontSize = '15px';
  116.  
  117. const children = wrapper.querySelectorAll('*');
  118. for (let child of children) {
  119. const style = child.style;
  120.  
  121. if (style.fontSize) {
  122. style.fontSize = '';
  123. }
  124.  
  125. if (style.fontFamily) {
  126. const font = style.fontFamily;
  127. if (SANS_SERIF.test(font)) {
  128. style.fontFamily = 'sans-serif';
  129. } else if (MONOSPACE.test(font)) {
  130. style.fontFamily = 'monospace';
  131. style.fontSize = '13.5px';
  132. } else if (SERIF.test(font)) {
  133. style.fontFamily = 'serif';
  134. }
  135. }
  136.  
  137. if (style.textAlign) {
  138. if (style.textAlign !== 'center' && style.textAlign !== 'right') {
  139. style.textAlign = '';
  140. }
  141. }
  142.  
  143. if (style.color && style.color.startsWith('rgb')) {
  144. style.color = adjustTextColor(style.color);
  145. }
  146. }
  147. }
  148.  
  149. // Smaller tables
  150. GM_addStyle(`
  151. .bbTable {
  152. font-size: 13.5px;
  153. overflow: visible;
  154. table-layout: auto;
  155. }
  156. .bbTable td {
  157. vertical-align: top;
  158. white-space: normal;
  159. word-wrap: normal;
  160. word-break: break-word;
  161. }
  162. `);
  163.  
  164. // SB Weekly stats thread
  165. if (document.URL.includes('.1140820/')) {
  166. // Fixed 2/3 columns
  167. const w = {2: '50%', 3: '33.33%'};
  168. for (const t of document.querySelectorAll('.bbTable table')) {
  169. const c = t.rows[0]?.cells, n = c?.length;
  170. if (w[n]) {
  171. t.style.cssText = 'table-layout:fixed;width:100%';
  172. for (const cell of c) cell.style.width = w[n];
  173. }
  174. }
  175. GM_addStyle(`
  176. /* Indicator */
  177. .bbTable td > b:first-of-type:has(code.bbCodeInline) { display: inline-block; padding: 2px 0; }
  178. .bbTable td > b:first-of-type code.bbCodeInline { font-size: 11px; }
  179.  
  180. .bbTable span:has(.username) { font-size: 11.5px; } /* Name */
  181. .bbTable span:has(.bbc-abbr) { font-size: 12.5px; } /* Metric */
  182.  
  183. /* Bottom Tags */
  184. .bbTable td > span:last-child:has(>code.bbCodeInline) { display: inline-flex; flex-wrap: wrap; gap: 1px; padding-top: 6px; }
  185. .bbTable td > span:last-child > code.bbCodeInline { font-size: 9px; padding: 1px 3px; }
  186.  
  187. /* Inline break with sliced decorations
  188. .bbTable td > span:last-child:has(>code.bbCodeInline) { display: inline-block; padding-top: 2px; }
  189. .bbTable td > span:last-child > code.bbCodeInline { margin: 1px 1px; box-decoration-break: slice; }
  190. */
  191. `);
  192. }