QQ/SB/SV Score

Add score indicator to threads using the (likes/replies) metric.

// ==UserScript==
// @name         QQ/SB/SV Score
// @author       C89sd
// @namespace    https://greasyfork.org/users/1376767
// @version      0.3
// @description  Add score indicator to threads using the (likes/replies) metric.
// @match        https://*.questionablequesting.com/*
// @match        https://*.spacebattles.com/*
// @match        https://*.sufficientvelocity.com/*
// @grant        GM_addStyle
// @noframes
// ==/UserScript==
'use strict';

const ALIGN_LEFT = true;
const COMPACT    = false;

if (window.location.href.includes('/threads/')) return;

let scale, PT;
const domain = window.location.hostname.split('.').slice(-2, -1)[0].toLowerCase();
if (domain === "spacebattles")         scale = 10.24/10.24;
if (domain === "questionablequesting") scale = 10.24/ 2.4;
if (domain === "sufficientvelocity")   scale = 10.24/ 9.8;

                                       scale = scale * (100 / (10.24 * 2.0)); // didn't measure std so assume x2 interval
                                       PT = 0

                                       // scale = scale
                                       // PT = 1

// #MEASURE_SCALE#
// const KEY = 'average';
// const data = JSON.parse(localStorage.getItem(KEY) || '{"sum":0,"count":0}');

GM_addStyle(`
  /* Hide on large screens */
  .mscore {
    display: none;
  }
  /* Show on mobile */
  @media (max-width: 650px) {
    .mscore {
      display: block;
    }
    .mscore.mright {
      float: right !important;
    }
    .mscore.mleft.mcompact {
      padding-left: 4px !important;
    }
    .mscore::before { content: none !important; }
    .mscore.mleft:not(.mcompact)::before{ content: "\\00A0\\00B7\\20\\00A0\\00A0" !important; }
  }
`);

function parseKmDigit(text) {
    if (!text) return NaN;
    const cleanedText = text.trim().toLowerCase();
    const multiplier = cleanedText.endsWith('k') ? 1000 : cleanedText.endsWith('m') ? 1000000 : 1;
    return parseFloat(cleanedText.replace(/,/g, '')) * multiplier;
}

const threads = document.getElementsByClassName('structItem--thread');
// const threads = document.querySelectorAll('.js-threadList>.structItem--thread, .structItemContainer>.structItem--thread');

for (const thread of threads) {
    const meta = thread.querySelector(':scope > .structItem-cell--meta');
    if (!meta) { console.log('Scorer skip: no meta cell', thread); continue; }

    const titleAttr = meta.getAttribute('title');
    const likesMatch = titleAttr?.match(/([\d,]+)/);
    if (!likesMatch) { console.log('Scorer skip: no likes match in title', thread); continue; }

    const likes = parseInt(likesMatch[1].replace(/,/g, ''), 10);
    const repliesText = meta.firstElementChild?.lastElementChild?.textContent;
    let replies = parseKmDigit(repliesText);

    if (replies === 0) replies = 1;

    if (isNaN(likes) || isNaN(replies)) { console.log('Scorer skip: NaN likes/replies', {likes, replies, thread}); continue; }

    if (replies >= 1000) {
        const pagesEl = thread.querySelector('.structItem-pageJump a:last-of-type');
        const pages = pagesEl ? parseInt(pagesEl.textContent, 10) : 1;
        replies = Math.max(replies, Math.floor((pages - 0.5) * 25));
    }

    const score = (scale * likes / replies).toFixed(PT);

  {
    const desktopScoreEl = document.createElement('dl');
    desktopScoreEl.className = 'pairs pairs--justified structItem-minor';

    const dt = document.createElement('dt');
    dt.textContent = 'Score';

    const dd = document.createElement('dd');
    dd.appendChild(document.createTextNode(score));

    desktopScoreEl.appendChild(dt);
    desktopScoreEl.appendChild(dd);

    meta.appendChild(desktopScoreEl);
  }
  {
    const mobileScoreEl = document.createElement('div');
    mobileScoreEl.className = 'structItem-cell structItem-cell--latest mscore ' + (ALIGN_LEFT ? "mleft" : "mright") + (COMPACT ? " mcompact":"");
    mobileScoreEl.textContent = score;

    if (ALIGN_LEFT) thread.insertBefore(mobileScoreEl, thread.querySelector('.structItem-cell--latest') || null);
    else            thread.appendChild(mobileScoreEl);
  }

  // #MEASURE_SCALE#
  // data.sum += score; data.count++;
}
// #MEASURE_SCALE#
// localStorage.setItem(KEY, JSON.stringify(data));
// console.log(`${KEY}: ${(data.sum/data.count).toFixed(2)} (n=${data.count})`);