Greasy Fork 还支持 简体中文。

Steam Review Other lang Calculation

Calculate steam review positive rate for other languages.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         Steam Review Other lang Calculation
// @name:zh-CN   Steam评测其他语言好评率计算
// @namespace    https://controlnet.space/
// @version      2026-03-31
// @description  Calculate steam review positive rate for other languages.
// @description:zh-CN 计算Steam消费者评测中其他语言的好评率。
// @author       ControlNet
// @match        https://store.steampowered.com/app/*
// @grant        none
// @license      AGPL-3.0
// ==/UserScript==

(function () {
    'use strict';

    const RESULT_ID = 'tm_other_lang_review_rate';
    const STYLE_ID = 'tm_other_lang_review_rate_style';

    const LANG_MAP = {
        'zh-cn': 'schinese',
        'zh-tw': 'tchinese',
        'en': 'english',
        'ja': 'japanese',
        'ko': 'koreana',
        'fr': 'french',
        'de': 'german',
        'es': 'spanish',
        'ru': 'russian',
        'pt-br': 'brazilian',
        'pt': 'portuguese',
        'it': 'italian',
        'pl': 'polish',
        'tr': 'turkish',
        'th': 'thai',
        'vi': 'vietnamese',
        'uk': 'ukrainian'
    };

    let cachedResult = null;
    let observer = null;
    let renderScheduled = false;
    let dataLoaded = false;

    function log(...args) {
        console.log('[Steam Other Lang Review]', ...args);
    }

    function getAppId() {
        const match = location.pathname.match(/\/app\/(\d+)/);
        return match ? match[1] : null;
    }

    function getSteamLanguage() {
        const htmlLang = (document.documentElement.lang || '').toLowerCase();
        if (LANG_MAP[htmlLang]) return LANG_MAP[htmlLang];

        const url = new URL(location.href);
        const l = (url.searchParams.get('l') || '').toLowerCase();
        if (l) return l;

        const config = document.querySelector('#application_config');
        const dataConfig = config?.getAttribute('data-config') || '';
        if (dataConfig.includes('"LANGUAGE":"schinese"')) return 'schinese';

        return 'schinese';
    }

    function isSimplifiedChinesePage() {
        const htmlLang = (document.documentElement.lang || '').toLowerCase();
        const url = new URL(location.href);
        const l = (url.searchParams.get('l') || '').toLowerCase();

        return htmlLang === 'zh-cn' || l === 'schinese' || l === '';
    }

    async function fetchReviewSummary(appId, language) {
        const url = new URL(`https://store.steampowered.com/appreviews/${appId}`);
        url.searchParams.set('json', '1');
        url.searchParams.set('language', language);
        url.searchParams.set('filter', 'all');
        url.searchParams.set('day_range', '365');
        url.searchParams.set('review_type', 'all');
        url.searchParams.set('purchase_type', 'all');
        url.searchParams.set('num_per_page', '1');
        url.searchParams.set('cursor', '*');

        const res = await fetch(url.toString(), { credentials: 'same-origin' });
        if (!res.ok) {
            throw new Error(`HTTP ${res.status} when fetching ${language}`);
        }

        const data = await res.json();
        if (!data || data.success !== 1 || !data.query_summary) {
            throw new Error(`Invalid appreviews response for ${language}`);
        }

        const qs = data.query_summary;
        return {
            totalReviews: Number(qs.total_reviews || 0),
            totalPositive: Number(qs.total_positive || 0),
            totalNegative: Number(qs.total_negative || 0),
            scoreDesc: qs.review_score_desc || ''
        };
    }

    function ensureStyle() {
        if (document.getElementById(STYLE_ID)) return;

        const style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = `
            #${RESULT_ID} {
                display: block;
                margin: 10px 0 14px 0;
                padding: 12px 14px;
                border-left: 3px solid #66c0f4;
                background: linear-gradient(90deg, rgba(102,192,244,0.16), rgba(102,192,244,0.08));
                color: #c7d5e0;
                font-size: 14px;
                line-height: 1.55;
                border-radius: 3px;
                box-sizing: border-box;
                width: 100%;
                clear: both;
            }
            #${RESULT_ID} .tm-title {
                display: block;
                font-weight: 700;
                color: #ffffff;
            }
            #${RESULT_ID} .tm-sub {
                display: block;
                margin-top: 4px;
                color: #8f98a0;
                font-size: 12px;
            }
        `;
        document.head.appendChild(style);
    }

    function ensureBox() {
        ensureStyle();

        let box = document.getElementById(RESULT_ID);
        if (!box) {
            box = document.createElement('div');
            box.id = RESULT_ID;
        }
        return box;
    }

    function findStableMountTarget() {
        // 目标位置:插到“筛选条件/您的语言”这一块后面
        const activeFilters = document.querySelector('.reviews_info_ctn #reviews_active_filters');
        if (activeFilters && activeFilters.isConnected) {
            return { type: 'after', node: activeFilters };
        }

        // 备选:插到整个 reviews_info_ctn 末尾最前面
        const reviewsInfo = document.querySelector('.reviews_info_ctn');
        if (reviewsInfo && reviewsInfo.isConnected) {
            return { type: 'prepend', node: reviewsInfo };
        }

        // 再备选:插到评测筛选栏前面
        const filterOptions = document.querySelector('#reviews_filter_options.user_reviews_filter_options');
        if (filterOptions && filterOptions.isConnected) {
            return { type: 'before', node: filterOptions };
        }

        // 最后兜底:整个顾客评测区
        const reviewSection =
            document.querySelector('#reviewSettingsPopupCtn')?.parentElement ||
            document.querySelector('.user_reviews');

        if (reviewSection && reviewSection.isConnected) {
            return { type: 'prepend', node: reviewSection };
        }

        return null;
    }

    function isCorrectlyMounted(box, mount) {
        if (!box.isConnected || !mount?.node?.isConnected) return false;

        if (mount.type === 'after') {
            return box.previousElementSibling === mount.node;
        }
        if (mount.type === 'before') {
            return box.nextElementSibling === mount.node;
        }
        if (mount.type === 'prepend') {
            return box.parentElement === mount.node && mount.node.firstElementChild === box;
        }
        return false;
    }

    function renderCachedResult() {
        if (!cachedResult) return false;

        const mount = findStableMountTarget();
        if (!mount) {
            log('Stable mount target not found yet.');
            return false;
        }

        const box = ensureBox();
        box.innerHTML = `
            <span class="tm-title">${cachedResult.message}</span>
            <span class="tm-sub">${cachedResult.subtext}</span>
        `;

        if (!isCorrectlyMounted(box, mount)) {
            box.remove();

            if (mount.type === 'after') {
                mount.node.insertAdjacentElement('afterend', box);
            } else if (mount.type === 'before') {
                mount.node.insertAdjacentElement('beforebegin', box);
            } else if (mount.type === 'prepend') {
                mount.node.insertAdjacentElement('afterbegin', box);
            }

            log('Mounted box at stable target:', mount);
        }

        return true;
    }

    function scheduleRender() {
        if (renderScheduled) return;
        renderScheduled = true;

        requestAnimationFrame(() => {
            renderScheduled = false;
            try {
                renderCachedResult();
            } catch (err) {
                console.error('[Steam Other Lang Review]', err);
            }
        });
    }

    function startObserver() {
        if (observer || !document.body) return;

        observer = new MutationObserver(() => {
            if (dataLoaded) {
                scheduleRender();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    async function computeResult() {
        const appId = getAppId();
        if (!appId) {
            throw new Error('App ID not found');
        }

        const currentLanguage = getSteamLanguage();

        const [allSummary, currentSummary] = await Promise.all([
            fetchReviewSummary(appId, 'all'),
            fetchReviewSummary(appId, currentLanguage)
        ]);

        const otherReviews = allSummary.totalReviews - currentSummary.totalReviews;
        const otherPositive = allSummary.totalPositive - currentSummary.totalPositive;

        if (otherReviews <= 0) {
            return {
                message: '没有可用于计算的“其他语言”评测。',
                subtext: `全部评测 ${allSummary.totalReviews.toLocaleString('zh-CN')},当前语言评测 ${currentSummary.totalReviews.toLocaleString('zh-CN')}`
            };
        }

        const otherPercent = Math.round((otherPositive / otherReviews) * 100);

        return {
            message: `其他语言的 ${otherReviews.toLocaleString('zh-CN')} 篇用户评测中约有 ${otherPercent}% 为好评。`,
            subtext: `当前语言:${currentSummary.totalReviews.toLocaleString('zh-CN')} 篇;所有语言:${allSummary.totalReviews.toLocaleString('zh-CN')} 篇`
        };
    }

    async function main() {
        if (!isSimplifiedChinesePage()) {
            log('Not a simplified Chinese page, skip.');
            return;
        }

        try {
            startObserver();

            cachedResult = await computeResult();
            dataLoaded = true;
            log('Computed result:', cachedResult);

            scheduleRender();

            let tries = 0;
            const timer = setInterval(() => {
                tries += 1;
                const ok = renderCachedResult();
                if (ok || tries >= 20) {
                    clearInterval(timer);
                    if (!ok) {
                        log('Render retries exhausted.');
                    }
                }
            }, 500);
        } catch (err) {
            console.error('[Steam Other Lang Review]', err);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main, { once: true });
    } else {
        main();
    }

    window.addEventListener('load', () => {
        if (dataLoaded) scheduleRender();
    }, { once: true });
})();