Greasy Fork is available in English.
Add score indicator to threads using the (likes/replies) metric.
当前为
// ==UserScript==
// @name QQ/SB/SV Score
// @description Add score indicator to threads using the (likes/replies) metric.
// @version 0.4
// @author C89sd
// @namespace http://greasyfork.icu/users/1376767
// @match https://*.alternatehistory.com/*
// @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();
PT = 0;
function ncdf(z) {
let t = 1 / (1 + 0.2315419 * Math.abs(z));
let d = 0.3989423 * Math.exp(-z * z / 2);
let prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
if (z > 0) prob = 1 - prob;
return prob;
}
// #MEASURE_SCALE#
// ----------- INIT
const VERSION = 7;
const KEY = 'measure_scale';
const NMAX = 10000.0; // maximum effective sample size
let data = JSON.parse(localStorage.getItem(KEY) || '{}');
if (!data.version || data.version !== VERSION) {
data = { version: VERSION, mean: 0, M2: 0, count: 0, version: VERSION };
}
function updateStreaming(score) {
// determine effective weight
const weight = data.count < NMAX ? 1 / (data.count + 1) : 1 / NMAX;
// update mean and variance
const delta = score - data.mean;
data.mean += weight * delta;
data.M2 = (1 - weight) * data.M2 + weight * delta * (score - data.mean);
// increment count up to NMAX
if (data.count < NMAX) data.count++;
}
function getMeasurement() {
const variance = data.M2 || 0;
const stddev = Math.sqrt(variance);
return { mean: data.mean, std: stddev, n: data.count };
}
// --- adjusted score 0–100
function adjustedScore(score) {
const MEASUREMENT = getMeasurement();
if (MEASUREMENT.std === 0) return NaN; // avoid division by zero
const z = (score - MEASUREMENT.mean) / MEASUREMENT.std;
const p = ncdf(z);
return Math.min(Math.max(p * 100, 0), 100);
}
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');
const DEBUG = false;
for (const thread of threads) {
const meta = thread.querySelector(':scope > .structItem-cell--meta');
if (!meta) { DEBUG && console.log('Scorer skip: no meta cell', thread); continue; }
// First message score, title="First message reaction score: 30"
//
// !! On some site its the TOTAL LIKES not FIRST LIKES !!
//
const titleAttr = meta.getAttribute('title');
const likesMatch = titleAttr?.match(/([\d,]+)/);
if (!likesMatch) { DEBUG && console.log('Scorer skip: no likes match in title', thread); continue; }
const likes = parseInt(likesMatch[1].replace(/,/g, ''), 10);
// Replies
const repliesText = meta.firstElementChild?.lastElementChild?.textContent;
let replies = parseKmDigit(repliesText);
if (replies === 0) replies = 1;
if (isNaN(likes) || isNaN(replies)) { DEBUG && console.log('Scorer skip: NaN likes/replies', {likes, replies, thread}); continue; }
if (replies > 1000) {
// replies >= 999 becomes 1k, 2k, 3k ... resolution loss
// instead we estimate the replies via page count (25 posts per page)
let pagesEl = thread.querySelector('.structItem-pageJump a:last-of-type');
if (pagesEl && pagesEl.textContent.trim() === 'New') { // AH has the last page be a New link, grab previous one
pagesEl = pagesEl.previousElementSibling;
}
const pages = pagesEl ? parseInt(pagesEl.textContent, 10) : 1;
replies = Math.max(replies, Math.floor((pages - 0.5) * 25));
}
// Views
const viewsText = meta.firstElementChild?.nextElementSibling?.lastElementChild?.textContent;
let views = parseKmDigit(viewsText);
// Words
const wordsText = thread.querySelector('.structItem-parts > li > a[data-xf-click="overlay"]')?.textContent;
const wordsMatch = wordsText?.trim().split(' ').pop();
if (!wordsMatch) { DEBUG && console.log('Scorer skip: no words match in title', thread); continue; }
let words = parseKmDigit(wordsMatch);
DEBUG && console.log(likes, replies, words, views, thread)
// const score = (scale * likes / replies).toFixed(PT);
// const score = (1000* scale * replies / words).toFixed(PT);
// const score = (1000* scale * (Math.max(replies, views/140) / words) ).toFixed(PT);
let score = 10 * Math.log1p(replies) / Math.log1p(words);
// score = Math.log1p(score);
// console.log(score)
let displayScore = adjustedScore(score);
{
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(displayScore.toFixed(PT)));
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 = displayScore.toFixed(PT);
mobileScoreEl.style.width = 'auto'
if (ALIGN_LEFT) thread.insertBefore(mobileScoreEl, thread.querySelector('.structItem-cell--latest') || null);
else thread.appendChild(mobileScoreEl);
}
// #MEASURE_SCALE#
updateStreaming(score);
}
localStorage.setItem(KEY, JSON.stringify(data));
// // ----------- VISUALISE
let MEASUREMENT = getMeasurement();
// console.log(`{ MEASUREMENT = { mean:${MEASUREMENT.mean.toFixed(4)}, std:${MEASUREMENT.std.toFixed(4)}, n:${MEASUREMENT.n} }; }`);