GeoGuessr Rating History Displayer

Display rating history from georank.io on GeoGuessr profile pages

Instali tiun ĉi skripton?
Author's suggested script

You may also like GeoGuessr Focus Mode.

Instali tiun ĉi skripton
// ==UserScript==
// @name         GeoGuessr Rating History Displayer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @match        https://www.geoguessr.com/*
// @description  Display rating history from georank.io on GeoGuessr profile pages
// @author       AaronThug
// @license      MIT
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    let isExpanded = false;
    let chartInstance = null;
    let cachedData = null;
    let currentUserId = null;

    const styles = `
        .rating-history-bar {
            background: #2c2c54;
            border: 1px solid #3c3c7e;
            border-radius: 8px;
            margin-bottom: 12px;
            cursor: pointer;
            transition: all 0.3s ease;
            position: relative;
            overflow: hidden;
        }
        .rating-history-bar:hover {
            background: #3c3c7e;
            border-color: #4c4c8e;
        }
        .rating-history-content {
            padding: 8px 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .rating-history-content h4 {
            color: #ffffff;
            margin: 0;
            font-size: 14px;
            font-weight: 600;
        }
        .rating-history-arrow {
            color: #ffffff;
            font-size: 12px;
            transition: transform 0.3s ease;
        }
        .rating-history-arrow.expanded {
            transform: rotate(180deg);
        }
        .rating-history-chart-container {
            max-height: 0;
            overflow: hidden;
            transition: max-height 0.3s ease;
            background: #1a1a2e;
            border-top: 1px solid #3c3c7e;
        }
        .rating-history-chart-container.expanded {
            max-height: 600px;
        }
        .rating-history-chart {
            padding: 20px;
            height: 400px;
        }
        .rating-history-loading {
            text-align: center;
            padding: 40px 20px;
            color: #ffffff;
        }
        .rating-history-error {
            text-align: center;
            padding: 40px 20px;
            color: #ff6b6b;
        }
    `;

    function addStyles() {
        const styleSheet = document.createElement('style');
        styleSheet.textContent = styles;
        document.head.appendChild(styleSheet);
    }

    function isProfilePage() {
        const path = window.location.pathname;
        if (path === '/me/profile' || path.startsWith('/user/')) return true;
        const pathParts = path.split('/').filter(part => part !== '');
        if (pathParts.length >= 2 && pathParts[0].length === 2) {
            const remainingPath = '/' + pathParts.slice(1).join('/');
            return remainingPath === '/me/profile' || remainingPath.startsWith('/user/');
        }
        return false;
    }

    async function getUserId() {
        const path = window.location.pathname;
        if (path.startsWith('/user/')) return path.split('/user/')[1];
        if (path === '/me/profile') return await fetchCurrentUserId();
        const pathParts = path.split('/').filter(part => part !== '');
        if (pathParts.length >= 2 && pathParts[0].length === 2) {
            const remainingPath = '/' + pathParts.slice(1).join('/');
            if (remainingPath.startsWith('/user/')) return remainingPath.split('/user/')[1];
            if (remainingPath === '/me/profile') return await fetchCurrentUserId();
        }
        return null;
    }

    async function fetchCurrentUserId() {
        try {
            const response = await fetch('https://www.geoguessr.com/api/v3/profiles');
            const data = await response.json();
            return data.id || data.user?.id || data.profile?.id || null;
        } catch (error) {
            return null;
        }
    }

    async function fetchRatingHistory(userId) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://georank.io/api/userhistory/${userId}`,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
                },
                onload: function(response) {
                    try {
                        if (response.status === 200) {
                            const data = JSON.parse(response.responseText);
                            resolve(data.rating?.overall || null);
                        } else {
                            resolve(null);
                        }
                    } catch (error) {
                        resolve(null);
                    }
                },
                onerror: () => resolve(null),
                ontimeout: () => resolve(null),
                timeout: 10000
            });
        });
    }

    function createRatingHistoryBar() {
        const bar = document.createElement('div');
        bar.className = 'rating-history-bar';
        bar.innerHTML = `
            <div class="rating-history-content">
                <h4>Rating History</h4>
                <span class="rating-history-arrow">▼</span>
            </div>
            <div class="rating-history-chart-container">
                <div class="rating-history-chart">
                    <canvas id="rating-history-canvas"></canvas>
                </div>
            </div>
        `;
        
        const arrow = bar.querySelector('.rating-history-arrow');
        const container = bar.querySelector('.rating-history-chart-container');
        
        bar.addEventListener('click', async function() {
            if (!isExpanded) {
                isExpanded = true;
                arrow.classList.add('expanded');
                container.classList.add('expanded');
                if (!cachedData) {
                    showLoading();
                    await loadAndDisplayChart();
                }
            } else {
                isExpanded = false;
                arrow.classList.remove('expanded');
                container.classList.remove('expanded');
            }
        });
        
        return bar;
    }

    function showLoading() {
        const chartDiv = document.querySelector('.rating-history-chart');
        chartDiv.innerHTML = '<div class="rating-history-loading">Loading rating history...</div>';
    }

    function showError(message) {
        const chartDiv = document.querySelector('.rating-history-chart');
        chartDiv.innerHTML = `<div class="rating-history-error">${message}</div>`;
    }

    async function loadAndDisplayChart() {
        try {
            if (!currentUserId) {
                currentUserId = await getUserId();
                if (!currentUserId) {
                    showError('Failed to get rating history. Try again later.');
                    return;
                }
            }

            const ratingData = await fetchRatingHistory(currentUserId);
            if (!ratingData) {
                showError('Failed to get rating history. Try again later.');
                return;
            }
            if (Object.keys(ratingData).length === 0) {
                showError('The user has no available rating history.');
                return;
            }

            cachedData = ratingData;
            displayChart(ratingData);
        } catch (error) {
            showError('Failed to get rating history. Try again later.');
        }
    }

    function displayChart(data) {
        const chartDiv = document.querySelector('.rating-history-chart');
        chartDiv.innerHTML = '<canvas id="rating-history-canvas"></canvas>';
        
        const canvas = document.getElementById('rating-history-canvas');
        const ctx = canvas.getContext('2d');
        const dates = Object.keys(data).sort();
        const ratings = dates.map(date => data[date]);

        if (chartInstance) chartInstance.destroy();

        chartInstance = new Chart(ctx, {
            type: 'line',
            data: {
                labels: dates,
                datasets: [{
                    label: 'Overall Rating',
                    data: ratings,
                    borderColor: '#4CAF50',
                    backgroundColor: 'rgba(76, 175, 80, 0.1)',
                    borderWidth: 2,
                    fill: true,
                    tension: 0.1,
                    pointRadius: 3,
                    pointHoverRadius: 5,
                    pointBackgroundColor: '#4CAF50',
                    pointBorderColor: '#ffffff',
                    pointBorderWidth: 2
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    legend: { labels: { color: '#ffffff' } },
                    tooltip: {
                        backgroundColor: 'rgba(0, 0, 0, 0.8)',
                        titleColor: '#ffffff',
                        bodyColor: '#ffffff',
                        borderColor: '#4CAF50',
                        borderWidth: 1
                    }
                },
                scales: {
                    x: {
                        ticks: { color: '#ffffff', maxTicksLimit: 10 },
                        grid: { color: 'rgba(255, 255, 255, 0.1)' }
                    },
                    y: {
                        ticks: { color: '#ffffff' },
                        grid: { color: 'rgba(255, 255, 255, 0.1)' }
                    }
                },
                interaction: { intersect: false, mode: 'index' }
            }
        });
    }

    function insertRatingHistoryBar() {
        if (document.querySelector('.rating-history-bar')) return true;
        const statisticsBar = document.querySelector('.bars_root__tryg2');
        if (statisticsBar) {
            const ratingHistoryBar = createRatingHistoryBar();
            statisticsBar.parentNode.insertBefore(ratingHistoryBar, statisticsBar);
            return true;
        }
        return false;
    }

    function init() {
        if (!isProfilePage()) return;
        addStyles();
        
        isExpanded = false;
        cachedData = null;
        currentUserId = null;
        if (chartInstance) {
            chartInstance.destroy();
            chartInstance = null;
        }
        
        if (insertRatingHistoryBar()) return;
        
        const observer = new MutationObserver(function(mutations) {
            mutations.forEach(function(mutation) {
                if (mutation.type === 'childList') {
                    if (insertRatingHistoryBar()) {
                        setTimeout(() => {
                            if (!document.querySelector('.rating-history-bar')) {
                                insertRatingHistoryBar();
                            }
                        }, 500);
                    }
                }
            });
        });
        
        observer.observe(document.body, { childList: true, subtree: true });
        
        if (window.location.pathname.includes('/me/profile')) {
            const interval = setInterval(() => {
                if (insertRatingHistoryBar()) clearInterval(interval);
            }, 1000);
            setTimeout(() => clearInterval(interval), 10000);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            const existingBar = document.querySelector('.rating-history-bar');
            if (!isProfilePage()) {
                if (existingBar) existingBar.remove();
                return;
            }
            cachedData = null;
            currentUserId = null;
            isExpanded = false;
            if (chartInstance) {
                chartInstance.destroy();
                chartInstance = null;
            }
            if (existingBar) existingBar.remove();
            setTimeout(init, 1500);
        }
    }).observe(document, {subtree: true, childList: true});

})();
长期地址
遇到问题?请前往 GitHub 提 Issues。