您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Replace review labels with raw % on store pages, hovers, search tiles, and the slider/detail pane
// ==UserScript== // @name Steam Reviews: Percent Hovercards // @namespace Betterthanever // @version 031 // @description Replace review labels with raw % on store pages, hovers, search tiles, and the slider/detail pane // @match https://store.steampowered.com/* // @run-at document-idle // @grant GM_xmlhttpRequest // @connect store.steampowered.com // @license GNU GPLv3 // ==/UserScript== (function () { 'use strict'; const LABEL_WORD_RE = /\b(?:Overwhelmingly\s+|Very\s+|Mostly\s+)?(?:Positive|Negative)\b|\bMixed\b/gi; const PERCENT_RE = /(\d{1,3})\s*%/; const CANDIDATE_CONTAINERS = [ '.user_reviews_summary_row', '.store_review_summary', '.store_overview .user_reviews', '.search_result_review', '#game_hover', '.hover_details_block', '.details_block', '.salepreviewwidgets_StoreSaleWidgetContainer', '.home_pagecontent', '.contenthub_page', '.store_capsule', '.cluster_capsule', 'body' ]; const pctCache = new Map(); function $(sel, root = document) { return root.querySelector(sel); } function $all(sel, root = document) { return Array.from(root.querySelectorAll(sel)); } const txt = el => (el?.textContent || '').trim(); function getAppIdFromContext(node) { let n = node; for (let i = 0; i < 10 && n; i++, n = n.parentElement) { for (const attr of ['data-ds-appid', 'data-appid', 'data-ds-itemkey', 'data-store-tooltip']) { const v = n.getAttribute?.(attr); if (v) { const m = String(v).match(/\b(\d{3,9})\b/); if (m) return m[1]; } } const a = n.querySelector?.('a[href*="/app/"]'); if (a && a.href) { const m = a.href.match(/\/app\/(\d{3,9})\b/); if (m) return m[1]; } } const hoverLink = $('#game_hover a[href*="/app/"]'); if (hoverLink) { const m = hoverLink.href.match(/\/app\/(\d{3,9})\b/); if (m) return m[1]; } return null; } function fetchPercentForApp(appid) { if (!appid) return Promise.resolve(null); if (pctCache.has(appid)) return Promise.resolve(pctCache.get(appid)); const url = `https://store.steampowered.com/appreviews/${appid}?json=1&purchase_type=all&language=all`; const parse = (j) => { try { const q = j.query_summary || j; const pos = Number(q.total_positive || 0); const neg = Number(q.total_negative || 0); const total = pos + neg; if (total > 0) { const pct = String(Math.round((pos / total) * 100)); pctCache.set(appid, pct); return pct; } } catch {} return null; }; return fetch(url, { credentials: 'include' }) .then(r => r.ok ? r.json() : Promise.reject()) .then(parse) .catch(() => new Promise(resolve => { if (typeof GM_xmlhttpRequest !== 'function') return resolve(null); GM_xmlhttpRequest({ method: 'GET', url, onload: res => { try { resolve(parse(JSON.parse(res.responseText))); } catch { resolve(null); } }, onerror: () => resolve(null), ontimeout: () => resolve(null) }); })); } function markDone(el) { el.dataset.tmReviewPct = '1'; } function isDone(el) { return el?.dataset?.tmReviewPct === '1'; } function* walkTextNodes(root) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); let n; while ((n = walker.nextNode())) yield n; } function replaceLabelWordsInElement(el, pct) { let replaced = false; for (const tn of walkTextNodes(el)) { if (LABEL_WORD_RE.test(tn.nodeValue)) { tn.nodeValue = tn.nodeValue.replace(LABEL_WORD_RE, `${pct}%`); replaced = true; } } if (replaced && !el.getAttribute('title')) el.setAttribute('title', 'Review summary replaced by userscript'); return replaced; } async function processNode(container) { if (!(container instanceof HTMLElement)) return; const candidates = new Set(); CANDIDATE_CONTAINERS.forEach(sel => { $all(`${sel} a, ${sel} span, ${sel} div`, container).forEach(el => { const t = txt(el); if (LABEL_WORD_RE.test(t)) candidates.add(el); }); }); if (candidates.size === 0) { $all('a, span, div', container).forEach(el => { const t = txt(el); if (LABEL_WORD_RE.test(t)) candidates.add(el); }); } $all('[data-tooltip-text],[data-tooltip-html],[title],[aria-label]', container).forEach(el => { const tip = el.getAttribute('data-tooltip-text') || el.getAttribute('data-tooltip-html') || el.getAttribute('title') || el.getAttribute('aria-label') || ''; if (PERCENT_RE.test(tip)) candidates.add(el); }); for (const el of candidates) { if (isDone(el)) continue; let pct = null; const tip = el.getAttribute('data-tooltip-text') || el.getAttribute('data-tooltip-html') || el.getAttribute('title') || el.getAttribute('aria-label') || ''; const tipMatch = tip.match(PERCENT_RE); if (tipMatch) pct = tipMatch[1]; if (!pct) { const inlineMatch = txt(el).match(PERCENT_RE); if (inlineMatch) pct = inlineMatch[1]; } if (!pct) { const appid = getAppIdFromContext(el); pct = await fetchPercentForApp(appid); } if (!pct) continue; const changed = replaceLabelWordsInElement(el, pct); if (!changed) { if (!el.querySelector('.tm-review-pct')) { const span = document.createElement('span'); span.className = 'tm-review-pct'; span.textContent = ` ${pct}%`; span.style.whiteSpace = 'nowrap'; span.style.fontWeight = '600'; span.style.fontSize = getComputedStyle(el).fontSize || '12px'; try { el.appendChild(span); } catch { el.parentElement?.appendChild(span); } } } markDone(el); } } function processAll(root = document) { CANDIDATE_CONTAINERS.forEach(sel => { $all(sel, root).forEach(processNode); }); } processAll(); const mo = new MutationObserver(mutations => { for (const m of mutations) { m.addedNodes.forEach(n => { if (n instanceof HTMLElement) processAll(n); }); if (m.target instanceof HTMLElement && (m.type === 'childList' || m.type === 'subtree')) { processNode(m.target); } } }); mo.observe(document.documentElement, { childList: true, subtree: true }); const _ps = history.pushState; history.pushState = function () { const r = _ps.apply(this, arguments); setTimeout(processAll, 120); return r; }; window.addEventListener('popstate', () => setTimeout(processAll, 120)); setInterval(() => { const vr = document.querySelector('.contenthub_page, .home_pagecontent, .salepreviewwidgets_StoreSaleWidgetContainer'); if (vr) processAll(vr); }, 1000); })();