QQ/SB/SV Mobile readability

Standardize font size to 15px and revert justified text; avoid text that is too small or too big. Replace fonts with sans-serif/serif/monospace at calibrated sizes (avoid unsupported/mismatched desktop fonts). Dodge colored text from the background and lightly pastelize to improve contrast.

  1. // ==UserScript==
  2. // @name QQ/SB/SV Mobile readability
  3. // @description Standardize font size to 15px and revert justified text; avoid text that is too small or too big. Replace fonts with sans-serif/serif/monospace at calibrated sizes (avoid unsupported/mismatched desktop fonts). Dodge colored text from the background and lightly pastelize to improve contrast.
  4. // @author C89sd
  5. // @version 1.9
  6. // @match https://questionablequesting.com/*
  7. // @match https://forum.questionablequesting.com/*
  8. // @match https://forums.spacebattles.com/*
  9. // @match https://forums.sufficientvelocity.com/*
  10. // @grant GM_addStyle
  11. // @namespace https://greasyfork.org/users/1376767
  12. // @noframes
  13. // ==/UserScript==
  14.  
  15. 'use strict';
  16.  
  17. const IS_THREAD = document.URL.includes('/threads/');
  18. if (!IS_THREAD) return;
  19.  
  20. const colorCache = new Map();
  21. const adjustTextColor = (textRGBStr) => {
  22. if (colorCache.has(textRGBStr)) {
  23. return colorCache.get(textRGBStr);
  24. }
  25.  
  26. function toRGB(str) { // can return up to 3 values [] to [r,g,b]
  27. const nums = str.match(/\d+/g);
  28. return nums ? nums.map(Number).slice(0, 3) : [];
  29. }
  30.  
  31. let fg = toRGB(textRGBStr);
  32. const bg = toRGB("rgb(39, 39, 39)");
  33.  
  34. if (fg.length !== 3) { return textRGBStr }
  35.  
  36. const clamp = v => Math.max(0, Math.min(255, v));
  37. const pastelize = ([r, g, b]) => {
  38. const neutral = 128;
  39. const whiteBoost = Math.pow((r + g + b) / (3 * 255), 10);
  40. const factor = 0.8 + 0.1*whiteBoost;
  41. return [
  42. neutral + factor * (r - neutral),
  43. neutral + factor * (g - neutral),
  44. neutral + factor * (b - neutral)
  45. ].map(clamp);
  46. };
  47.  
  48. fg = pastelize(fg);
  49.  
  50. let positiveDiff = 0;
  51. positiveDiff += Math.max(0, fg[0] - 39);
  52. positiveDiff += Math.max(0, fg[1] - 39);
  53. positiveDiff += Math.max(0, fg[2] - 39);
  54.  
  55. const threshold = 180;
  56. if (positiveDiff < threshold) {
  57. fg[0] = Math.max(fg[0], 39);
  58. fg[1] = Math.max(fg[1], 39);
  59. fg[2] = Math.max(fg[2], 39);
  60.  
  61. const avg = (fg[0]+fg[1]+fg[2])/3;
  62. 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
  63.  
  64. let correction = Math.round((threshold - positiveDiff) / 3 + boost*40);
  65. fg = fg.map(c => Math.min(c + correction, 255));
  66. }
  67.  
  68. const result = `rgb(${fg[0]}, ${fg[1]}, ${fg[2]})`;
  69. colorCache.set(textRGBStr, result);
  70.  
  71. return result;
  72. };
  73.  
  74.  
  75. const SANS_SERIF = /arial|tahoma|trebuchet ms|verdana/;
  76. const MONOSPACE = /courier new/;
  77. const SERIF = /times new roman|georgia|book antiqua/;
  78.  
  79. const wrappers = document.querySelectorAll('div.bbWrapper');
  80. for (let wrapper of wrappers) {
  81. wrapper.style.fontSize = '15px';
  82.  
  83. const children = wrapper.querySelectorAll('*');
  84. for (let child of children) {
  85. const style = child.style;
  86.  
  87. if (style.fontSize) {
  88. style.fontSize = '';
  89. }
  90.  
  91. if (style.fontFamily) {
  92. const font = style.fontFamily;
  93. if (SANS_SERIF.test(font)) {
  94. style.fontFamily = 'sans-serif';
  95. } else if (MONOSPACE.test(font)) {
  96. style.fontFamily = 'monospace';
  97. style.fontSize = '13.5px';
  98. } else if (SERIF.test(font)) {
  99. style.fontFamily = 'serif';
  100. }
  101. }
  102.  
  103. if (style.textAlign) {
  104. if (style.textAlign !== 'center' && style.textAlign !== 'right') {
  105. style.textAlign = '';
  106. }
  107. }
  108.  
  109. if (style.color && style.color.startsWith('rgb')) {
  110. style.color = adjustTextColor(style.color);
  111. }
  112. }
  113. }
  114.  
  115. // Smaller tables
  116. GM_addStyle(`
  117. .bbTable {
  118. font-size: 13.5px;
  119. overflow: visible;
  120. table-layout: auto;
  121. }
  122. .bbTable td {
  123. vertical-align: top;
  124. white-space: normal;
  125. word-wrap: normal;
  126. word-break: break-word;
  127. }
  128. `);
  129.  
  130. // SB Weekly stats thread
  131. if (document.URL.includes('.1140820/')) {
  132. // Fixed 2/3 columns
  133. const w = {2: '50%', 3: '33.33%'};
  134. for (const t of document.querySelectorAll('.bbTable table')) {
  135. const c = t.rows[0]?.cells, n = c?.length;
  136. if (w[n]) {
  137. t.style.cssText = 'table-layout:fixed;width:100%';
  138. for (const cell of c) cell.style.width = w[n];
  139. }
  140. }
  141. GM_addStyle(`
  142. /* Indicator */
  143. .bbTable td > b:first-of-type:has(code.bbCodeInline) { display: inline-block; padding: 2px 0; }
  144. .bbTable td > b:first-of-type code.bbCodeInline { font-size: 11px; }
  145.  
  146. .bbTable span:has(.username) { font-size: 11.5px; } /* Name */
  147. .bbTable span:has(.bbc-abbr) { font-size: 12.5px; } /* Metric */
  148.  
  149. /* Bottom Tags */
  150. .bbTable td > span:last-child:has(>code.bbCodeInline) { display: inline-flex; flex-wrap: wrap; gap: 1px; padding-top: 6px; }
  151. .bbTable td > span:last-child > code.bbCodeInline { font-size: 9px; padding: 1px 3px; }
  152.  
  153. /* Inline break with sliced decorations
  154. .bbTable td > span:last-child:has(>code.bbCodeInline) { display: inline-block; padding-top: 2px; }
  155. .bbTable td > span:last-child > code.bbCodeInline { margin: 1px 1px; box-decoration-break: slice; }
  156. */
  157. `);
  158. }