您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Add self-calibrating score indicator per-thread using the (replies/words) metric. Add footer toggles [unsorted|autosort] and [show seen|hide seen].
当前为
// ==UserScript== // @name QQ/SB/SV Score // @description Add self-calibrating score indicator per-thread using the (replies/words) metric. Add footer toggles [unsorted|autosort] and [show seen|hide seen]. // @version 0.11 // @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; const CORNER_INDICATOR = true; // false: text; true: colored box let CORNER_TOP = true; // true: trop corner, false: MOBILE-only bottom corner if (window.location.href.includes('/threads/')) return; GM_addStyle(` /* hide on dekstop */ @media (min-width: 650px) { .structItem--thread>.scoreA { display: none !important; } } :root { --boost: 85%; --boostDM: 75%; /* 82%; */ --darken: 55%; --darkenDM: 33.3%; } :root.dark-theme { --darken: var(--darkenDM); --boost: var(--boostDM); } .scoreA { background-image: linear-gradient(hsl(0, 0%, var(--boost)), hsl(0, 0%, var(--boost))) !important; background-blend-mode: color-burn !important; } .scoreA.darkenA { background-image: linear-gradient(hsl(0, 0%, var(--darken)), hsl(0, 0%, var(--darken))) !important; background-blend-mode: multiply !important; } `); const DM = window.getComputedStyle(document.body).color.match(/\d+/g)[0] > 128; if (DM) document.documentElement.classList.add('dark-theme'); // if (dimmed) { indicator.classList.add('darkenA'); } const HSL_STRINGS = [ 'hsl(0.0, 90.7%, 92.3%)', 'hsl(47.8, 67.1%, 81.5%)', 'hsl(118.4, 51.2%, 85%)', 'hsl(122.9, 35.1%, 63.4%)', ]; const COLORS = HSL_STRINGS.map(str => (([h, s, l]) => ({ h, s, l }))(str.match(/[\d.]+/g).map(Number))); function clamp(a, b, x) { return x < a ? a : (x > b ? b : x); } function color(t, range=1.0, use3colors=false) { let a, b; t = t/range; if (t < 0) { t = 0.0; } if (use3colors && t > 1.0) { t = 1.0; } else if (t > 1.5) { t = 1.5; } if (t < 0.5) { a = COLORS[0], b = COLORS[1]; t = t * 2.0; } else if (t <= 1.0) { a = COLORS[1], b = COLORS[2]; t = (t - 0.5) * 2.0; } else { a = COLORS[2], b = COLORS[3]; t = (t - 1.0) * 2.0; } const h = clamp(0, 360, a.h + (b.h - a.h) * t); const s = clamp(0, 100, a.s + (b.s - a.s) * t); const l = clamp(0, 100, a.l + (b.l - a.l) * t); return `hsl(${h.toFixed(1)}, ${s.toFixed(1)}%, ${l.toFixed(1)}%)`; } 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 = 12; const KEY = 'measure_scale'; const NMAX = 1500; const LRU_MAX = 300; let data = JSON.parse(localStorage.getItem(KEY) || '{}'); if (!data.version || data.version !== VERSION) { data = { version: VERSION, mean: 0, M2: 0, count: 0, lruBuffer: [] }; } const lruArray = data.lruBuffer; function addToLRU(key) { const idx = lruArray.indexOf(key); if (idx !== -1) { // remove old copy unless at the front if (idx === 0) return false; lruArray.splice(idx, 1); } else if (lruArray.length === LRU_MAX) lruArray.pop(); // drop the oldest entry lruArray.unshift(key); // put the key at the front return idx === -1; // was it a miss? } 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; } let threadid = parseInt(thread.className.match(/\bjs-threadListItem-(\d+)/)[1], 10); // // 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); // if (isNaN(likes) ) { DEBUG && console.log('Scorer skip: NaN likes', {likes, thread}); continue; } // Replies const repliesText = meta.firstElementChild?.lastElementChild?.textContent; let replies = parseKmDigit(repliesText); if (replies === 0) replies = 1; if (isNaN(replies)) { DEBUG && console.log('Scorer skip: NaN replies', {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) let score = Math.log1p(replies) - Math.log1p(words); let displayScore = adjustedScore(score); // MOBILE if (CORNER_INDICATOR) { // corner const indicator = document.createElement('div'); indicator.className = 'scoreA'; indicator.textContent = displayScore.toFixed(PT); indicator.style.cssText = [ 'display:block', 'width:28px', 'text-align:center', 'line-height:18px', 'padding:0', CORNER_TOP ? 'position:relative' : 'position:absolute', 'color:rgb(42,42,42)', 'background:' + color(displayScore / 100, 1, true), 'float:right', 'margin-left:4px', CORNER_TOP ? 'top:3px' : 'bottom:9px',//'margin-bottom:9px', CORNER_TOP ? 'top:3px' : 'right: 9px' ].join(';'); thread.style.position = 'relative'; if (CORNER_TOP) { let title = thread.querySelector('.structItem-cell--main') title.style.paddingRight = '6px'; title.style.paddingTop = '6px'; title.style.position = 'relative'; title.prepend(indicator); } else { thread.append(indicator); } } else { // DESKTOP -- right column { 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); } // Text Under { 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# if (addToLRU(threadid)) { updateStreaming(score); // console.log('ADDED', threadid) } else { // console.log('skipped', threadid) } } 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} }; }`); // ------------------------------------------------------------------------------------- // Add footer [sort by score] and [hide seen] selectors. if (!window.location.href.includes('/threads/')) { const DEFAULT_CONFIG = { showSeen: true, sortByScore: false }; let config = { ...DEFAULT_CONFIG, ...JSON.parse(localStorage.getItem('_showseen') || '{}') }; // Style injection logic const style = document.createElement('style'); document.head.appendChild(style); function updateVisibilityStyles() { style.textContent = config.showSeen ? '' : `.structItem--thread:has(.hl-name-seen), .structItem--thread:has(.hl-seen), .structItem--thread:has(.hl-watched) { display: none !important; }`; } let firstTime = true, list = []; let sorted = false; function updateSort() { if (sorted === config.sortByScore) return; sorted = config.sortByScore; if (firstTime) { firstTime = false; let threads = document.querySelectorAll('.js-threadList .structItem--thread'); // dont sort Sticky Threads let idx = 0; for (let thread of threads) { let score = -1; let indicator = thread.querySelector('.scoreA'); if (indicator) score = parseInt(indicator.textContent, 10); list.push([thread, idx++, score]); } } if (sorted) { list.sort((a, b) => b[2] - a[2]); } else { list.sort((a, b) => b[1] - a[1]); } list.forEach(([t, i, s]) => { t.parentElement.appendChild(t); }); } // Helper function for creating select elements function createSelect(options, currentValue, handler) { const select = document.createElement('select'); select.style.width = 'max-content'; select.innerHTML = options; select.value = currentValue; select.addEventListener('change', handler); return select; } // Seen posts selector const seenSelector = createSelect( '<option value="true">Show seen</option><option value="false">Hide seen</option>', config.showSeen.toString(), () => { config.showSeen = seenSelector.value === 'true'; localStorage.setItem('_showseen', JSON.stringify(config)); updateVisibilityStyles(); } ); // Sorting selector const sortSelector = createSelect( '<option value="false">Unsorted</option><option value="true">Autosort</option>', config.sortByScore.toString(), () => { config.sortByScore = sortSelector.value === 'true'; localStorage.setItem('_showseen', JSON.stringify(config)); updateSort(); } ); const footer = document.getElementById('footer'); if (footer) { let footerInner = footer.querySelector('.p-footer--container, .p-footer-inner'); console.log() updateVisibilityStyles(); updateSort(); const controlBar = document.createElement('div'); controlBar.style.width = '100%'; controlBar.style.paddingTop = '5px'; controlBar.style.paddingBottom = '5px'; controlBar.style.display = 'flex'; controlBar.style.justifyContent = 'center'; controlBar.style.gap = '10px'; controlBar.className = 'footer'; controlBar.appendChild(sortSelector); controlBar.appendChild(seenSelector); footer.insertBefore(controlBar, footerInner); } }