Backloggd - Steam Review Integration - Dark Mode

Displays Steam reviews in unified format

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Backloggd - Steam Review Integration - Dark Mode
// @namespace    https://greasyfork.org/en/users/1410951-nzar-bayezid
// @version      1.0
// @description  Displays Steam reviews in unified format
// @icon         https://www.backloggd.com/favicon.ico
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @match        https://backloggd.com/*
// @match        https://www.backloggd.com/*
// @grant        GM_xmlhttpRequest
// @connect      store.steampowered.com
// @license      MIT
// @downloadURL
// @updateURL
// ==/UserScript==

/*=========================  Version History  ==================================
v1.0 -
- Dark Mode
- Integrated Steam review data into Backloggd game pages
- Unified UI styling for consistent appearance with existing integrations
- Added support for displaying both recent and all-time Steam reviews
- Implemented dynamic color coding based on review sentiment (e.g., Positive, Negative)
- Optimized API request handling for Steam app ID and review data fetching
- Ensured proper error handling and fallbacks for missing or unavailable data
- Enhanced performance by minimizing redundant DOM manipulations
- Added MutationObserver to dynamically handle page updates and new content loading
- Improved localization of review counts using `toLocaleString` for better readability
*/

(function() {
    'use strict';
    const OBSERVER_CONFIG = {
        childList: true,
        subtree: true,
        attributes: false,
        characterData: false
    };

    const REVIEW_COLORS = {
        'Overwhelmingly Positive': '#16181c',
        'Very Positive': '#16181c',
        'Positive': '#16181c',
        'Mostly Positive': '#16181c',
        'Mixed': '#16181c',
        'Mostly Negative': '#16181c',
        'Negative': '#16181c',
        'Very Negative': '#16181c',
        'Overwhelmingly Negative': '#16181c'
    };

    let processing = false;
    let currentPath = '';

    async function mainExecutor() {
        if (processing) return;
        if (location.pathname === currentPath) return;
        if (!document.querySelector('#game-body')) return;

        currentPath = location.pathname;
        processing = true;

        cleanExistingElements();
        await processGameData();
        processing = false;
    }

    function cleanExistingElements() {
        $('.steam-integration').remove();
    }

    async function processGameData() {
        try {
            const gameName = document.querySelector("#title h1").textContent;
            const appId = await fetchSteamAppId(gameName);
            if (!appId) return;

            const reviewData = await fetchSteamReviews(appId);
            renderSteamReview(reviewData, appId);
        } catch (error) {
            console.error('Steam Integration Error:', error);
        }
    }

    function fetchSteamAppId(gameName) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://store.steampowered.com/search/?term=${encodeURIComponent(gameName)}&category1=998`,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
                },
                onload: function(response) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, "text/html");
                    const firstResult = doc.querySelector('.search_result_row');
                    resolve(firstResult?.dataset.dsAppid || null);
                },
                onerror: () => resolve(null),
                timeout: 5000
            });
        });
    }

    function fetchSteamReviews(appId) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://store.steampowered.com/app/${appId}`,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
                    'Accept-Language': 'en-US,en;q=0.9'
                },
                onload: function(response) {
                    try {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, "text/html");
                        const userReviews = doc.querySelector('#userReviews');

                        if (!userReviews) return resolve(null);

                        // Extract Recent Reviews
                        const recent = userReviews.querySelector('.user_reviews_summary_row:nth-child(1)');
                        const recentRating = recent?.querySelector('.game_review_summary')?.textContent;
                        const recentCount = extractNumber(recent?.querySelector('.responsive_hidden')?.textContent);

                        // Extract All Reviews
                        const all = userReviews.querySelector('.user_reviews_summary_row[itemprop="aggregateRating"]');
                        const allRating = all?.querySelector('.game_review_summary')?.textContent;
                        const allCount = extractNumber(all?.querySelector('.responsive_hidden')?.textContent);

                        resolve({
                            recent: { rating: recentRating, count: recentCount },
                            all: { rating: allRating, count: allCount }
                        });
                    } catch {
                        resolve(null);
                    }
                },
                onerror: () => resolve(null),
                timeout: 5000
            });
        });
    }

    function extractNumber(text) {
        const match = text?.match(/\(([\d,]+)\)/);
        return match ? parseInt(match[1].replace(/,/g, '')) : null;
    }

    function formatReviewCount(num) {
        if (!num) return 'N/A';
        return num.toLocaleString();
    }

    function renderSteamReview(reviewData, appId) {
        if (!reviewData || !reviewData.all?.rating) return;

        const target = $("#game-body > div.col > div:nth-child(2) > div.col-12.col-lg-cus-32.mt-1.mt-lg-2");
        if (!target.length) return;

        const color = REVIEW_COLORS[reviewData.all.rating] || '#607D8B';

        const content = [];

        if (reviewData.recent?.rating) {
            content.push(`
                <div style="margin-bottom: 6px; font-size: 14px;">
                    <span style="opacity: 0.8;">Recent Reviews:<br>
                    ${reviewData.recent.rating}
                    (${formatReviewCount(reviewData.recent.count)})
                </div>
            `);
        }

        content.push(`
            <div style="font-size: 14px;">
                <span style="opacity: 0.8;">All Reviews:<br>
                ${reviewData.all.rating}
                (${formatReviewCount(reviewData.all.count)})
            </div>
        `);

        const element = $(`
            <div class="steam-integration" style="margin-top:10px;">
                <a href="https://store.steampowered.com/app/${appId}"
                   target="_blank"
                   style="display: block;
                          background: ${color};
                          color: white;
                          padding: 12px;
                          border-radius: 4px;
                          text-decoration: none;
                          transition: transform 0.2s;
                          border: 1px solid #8f9ca7;
                          line-height: 1.4;">
                    ${content.join('')}
                </a>
            </div>
        `);

        target.append(element);
    }

    // Mutation observer setup
    new MutationObserver(mutations => {
        if (document.querySelector('#game-body') && mutations.some(m => m.addedNodes.length)) {
            mainExecutor();
        }
    }).observe(document.documentElement, OBSERVER_CONFIG);

    // Initial execution
    window.addEventListener('load', mainExecutor);
})();