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.

当前为 2025-05-23 提交的版本,查看 最新版本

// ==UserScript==
// @name        QQ/SB/SV Mobile readability
// @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. 
// @author      C89sd
// @version     1.9
// @match       https://questionablequesting.com/*
// @match       https://forum.questionablequesting.com/*
// @match       https://forums.spacebattles.com/*
// @match       https://forums.sufficientvelocity.com/*
// @grant       GM_addStyle
// @namespace   https://greasyfork.org/users/1376767
// @noframes
// ==/UserScript==

'use strict';

const IS_THREAD = document.URL.includes('/threads/');
if (!IS_THREAD) return;

const colorCache = new Map();
const adjustTextColor = (textRGBStr) => {
    if (colorCache.has(textRGBStr)) {
        return colorCache.get(textRGBStr);
    }

    function toRGB(str) { // can return up to 3 values [] to [r,g,b]
        const nums = str.match(/\d+/g);
        return nums ? nums.map(Number).slice(0, 3) : [];
    }

    let fg = toRGB(textRGBStr);
    const bg = toRGB("rgb(39, 39, 39)");

    if (fg.length !== 3) { return textRGBStr }

    const clamp = v => Math.max(0, Math.min(255, v));
    const pastelize = ([r, g, b]) => {
        const neutral = 128;
        const whiteBoost = Math.pow((r + g + b) / (3 * 255), 10);
        const factor = 0.8 + 0.1*whiteBoost;
        return [
            neutral + factor * (r - neutral),
            neutral + factor * (g - neutral),
            neutral + factor * (b - neutral)
        ].map(clamp);
    };

    fg = pastelize(fg);

    let positiveDiff = 0;
    positiveDiff += Math.max(0, fg[0] - 39);
    positiveDiff += Math.max(0, fg[1] - 39);
    positiveDiff += Math.max(0, fg[2] - 39);

    const threshold = 180;
    if (positiveDiff < threshold) {
      fg[0] = Math.max(fg[0], 39);
      fg[1] = Math.max(fg[1], 39);
      fg[2] = Math.max(fg[2], 39);

      const avg = (fg[0]+fg[1]+fg[2])/3;
      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

      let correction = Math.round((threshold - positiveDiff) / 3 + boost*40);
      fg = fg.map(c => Math.min(c + correction, 255));
    }

    const result = `rgb(${fg[0]}, ${fg[1]}, ${fg[2]})`;
    colorCache.set(textRGBStr, result);

    return result;
};


const SANS_SERIF = /arial|tahoma|trebuchet ms|verdana/;
const MONOSPACE = /courier new/;
const SERIF = /times new roman|georgia|book antiqua/;

const wrappers = document.querySelectorAll('div.bbWrapper');
for (let wrapper of wrappers) {
  wrapper.style.fontSize = '15px';

  const children = wrapper.querySelectorAll('*');
  for (let child of children) {
    const style = child.style;

    if (style.fontSize) {
      style.fontSize = '';
    }

    if (style.fontFamily) {
      const font = style.fontFamily;
      if (SANS_SERIF.test(font)) {
        style.fontFamily = 'sans-serif';
      } else if (MONOSPACE.test(font)) {
        style.fontFamily = 'monospace';
        style.fontSize = '13.5px';
      } else if (SERIF.test(font)) {
        style.fontFamily = 'serif';
      }
    }

    if (style.textAlign) {
      if (style.textAlign !== 'center' && style.textAlign !== 'right') {
        style.textAlign = '';
      }
    }

    if (style.color && style.color.startsWith('rgb')) {
      style.color = adjustTextColor(style.color);
    }
  }
}

// Smaller tables
GM_addStyle(`
.bbTable {
  font-size: 13.5px;
  overflow: visible;
  table-layout: auto;
}
.bbTable td {
  vertical-align: top;
  white-space: normal;
  word-wrap: normal;
  word-break: break-word;
}
`);

// SB Weekly stats thread
if (document.URL.includes('.1140820/')) {
  // Fixed 2/3 columns
  const w = {2: '50%', 3: '33.33%'};
  for (const t of document.querySelectorAll('.bbTable table')) {
    const c = t.rows[0]?.cells, n = c?.length;
    if (w[n]) {
      t.style.cssText = 'table-layout:fixed;width:100%';
      for (const cell of c) cell.style.width = w[n];
    }
  }
  GM_addStyle(`
    /* Indicator */
    .bbTable td >  b:first-of-type:has(code.bbCodeInline) { display: inline-block; padding: 2px 0; }
    .bbTable td >  b:first-of-type code.bbCodeInline { font-size: 11px; }

    .bbTable span:has(.username) { font-size: 11.5px; } /* Name */
    .bbTable span:has(.bbc-abbr) { font-size: 12.5px; } /* Metric */

    /* Bottom Tags */
    .bbTable td > span:last-child:has(>code.bbCodeInline) { display: inline-flex; flex-wrap: wrap; gap: 1px; padding-top: 6px; }
    .bbTable td > span:last-child > code.bbCodeInline { font-size: 9px; padding: 1px 3px; }

    /* Inline break with sliced decorations
    .bbTable td > span:last-child:has(>code.bbCodeInline) { display: inline-block; padding-top: 2px; }
    .bbTable td > span:last-child > code.bbCodeInline { margin: 1px 1px; box-decoration-break: slice; }
    */
  `);
}