您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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} }; }`);