Survev Quick Stats

Custom stats panel with stats + match history.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Survev Quick Stats
// @namespace    https://survev.io/
// @author       piesimp
// @version      1.0
// @description  Custom stats panel with stats + match history.
// @match        https://survev.io/*
// @run-at       document-end
// @grant        GM_xmlhttpRequest
// @connect      api.survev.io
// ==/UserScript==

(function () {
    'use strict';


    function modifyLeftColumn() {
        const socialBlock = document.getElementById("social-share-block");
        if (socialBlock) socialBlock.remove();

        const kofiLink = document.querySelector('#ad-block-left a[href="https://ko-fi.com/survev"]');
        if (kofiLink) kofiLink.remove();

        const survivShirts = document.querySelector('#ad-block-left .surviv-shirts');
        if (survivShirts) survivShirts.remove();
    }


    function buildEmptyStatsPanel() {

        const adBlock = document.getElementById("ad-block-left");
        const newsBlock = document.getElementById("news-block");
        if (!adBlock) return;

        adBlock.innerHTML = "";

        const container = document.createElement("div");
        container.id = "custom-stats-panel";

        container.innerHTML = `
            <div class="stats-header">
    <div class="stats-title">Your Stats</div>
    <div class="refresh-btn" title="Refresh Stats">⟳</div>
</div>

            <div class="stats-row">
                <div class="stat">
                    <div class="stat-label">Wins</div>
                    <div class="stat-value" id="stat-wins"></div>
                </div>
                <div class="stat">
                    <div class="stat-label">Kills</div>
                    <div class="stat-value" id="stat-kills"></div>
                </div>
                <div class="stat">
                    <div class="stat-label">Games</div>
                    <div class="stat-value" id="stat-games"></div>
                </div>
                <div class="stat">
                    <div class="stat-label">K/G</div>
                    <div class="stat-value" id="stat-kg"></div>
                </div>
            </div>

            <div class="recent-title">Recent Matches</div>

            <div class="recent-list">
                <div class="recent-item">
                    <div class="recent-mode"></div>
                    <div class="recent-time"></div>
                </div>
                <div class="recent-item">
                    <div class="recent-mode"></div>
                    <div class="recent-time"></div>
                </div>
                <div class="recent-item">
                    <div class="recent-mode"></div>
                    <div class="recent-time"></div>
                </div>
            </div>

            <div class="leaderboard-link">Global Leaderboard →</div>
        `;

        adBlock.appendChild(container);
        const refreshBtn = container.querySelector(".refresh-btn");

refreshBtn.onclick = () => {
    refreshBtn.classList.add("spinning");

    const username = window.currentSurvevPlayer;
    if (!username) return;

    fetchAndFillStats(username);
    fetchMatchHistory(username);

    setTimeout(() => {
        refreshBtn.classList.remove("spinning");
    }, 800);
};

        if (newsBlock) {
            const h = newsBlock.offsetHeight;
            adBlock.style.minHeight = (h + 3.8) + "px";
            container.style.minHeight = (h + 140) + "px";
        }

        container.querySelector(".leaderboard-link").onclick = () => {
            window.location.href = "https://survev.io/stats/";
        };
    }


    function fetchAndFillStats(username) {

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api.survev.io/api/user_stats",
            headers: {
                "Content-Type": "application/json; charset=UTF-8",
                "Accept": "*/*",
                "Origin": "https://survev.io",
                "Referer": "https://survev.io/"
            },
            data: JSON.stringify({
                slug: username,
                interval: "alltime",
                mapIdFilter: "-1"
            }),
            onload: function (response) {
                const data = JSON.parse(response.responseText);
                if (!data || data.banned) return;

                document.getElementById("stat-wins").textContent = data.wins || "";
                document.getElementById("stat-kills").textContent = data.kills || "";
                document.getElementById("stat-games").textContent = data.games || "";
                document.getElementById("stat-kg").textContent = data.kpg || "";
            }
        });
    }



    function fetchMatchHistory(username) {

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api.survev.io/api/match_history",
            headers: {
                "Content-Type": "application/json; charset=UTF-8",
                "Accept": "*/*",
                "Origin": "https://survev.io",
                "Referer": "https://survev.io/"
            },
            data: JSON.stringify({
                slug: username,
                offset: 0,
                count: 3,
                teamModeFilter: 7
            }),
            onload: function (response) {
                const matches = JSON.parse(response.responseText);
                if (!Array.isArray(matches)) return;

                const items = document.querySelectorAll(".recent-item");

                matches.slice(0, 3).forEach((match, i) => {

                    const modeMap = {
                        1: "Solo",
                        2: "Duo",
                        3: "Trio",
                        4: "Squad"
                    };

                    const mode = modeMap[match.team_count] || "Mode";
                    const placement = `#${match.rank}`;

                    const endTime = new Date(match.end_time);
                    const now = new Date();
                    const diff = Math.floor((now - endTime) / 1000);

                    let timeAgo;
                    if (diff < 60) timeAgo = `${diff}s ago`;
                    else if (diff < 3600) timeAgo = `${Math.floor(diff / 60)}m ago`;
                    else if (diff < 86400) timeAgo = `${Math.floor(diff / 3600)}h ago`;
                    else timeAgo = `${Math.floor(diff / 86400)}d ago`;

                    const modeDiv = items[i].querySelector(".recent-mode");
                    const timeDiv = items[i].querySelector(".recent-time");

                    modeDiv.textContent = `${mode} ${placement}`;
                    timeDiv.textContent = timeAgo;
                });
            }
        });
    }


    function detectAccountAndLoadStats() {

        const nameElement = document.getElementById("account-player-name");
        const adBlock = document.getElementById("ad-block-left");
        if (!nameElement || !adBlock) return;

        const rawName = nameElement.textContent.trim().toLowerCase();
const playerName = rawName.replace(/\s+/g, "-");

        const notLoggedInTexts = ["log in", "create account", "sign in", "guest"];
        const isLoggedOut = notLoggedInTexts.some(text =>
            playerName.includes(text)
        );

        if (isLoggedOut) {
            adBlock.innerHTML = `
                <div style="color:red; font-weight:bold; padding:15px; text-align:center;">
                    You must be logged in!
                </div>
            `;
            return;
        }
        window.currentSurvevPlayer = playerName;

        buildEmptyStatsPanel();
        fetchAndFillStats(playerName);
        fetchMatchHistory(playerName);
    }

    function injectStyles() {
        const style = document.createElement("style");
        style.textContent = `
        #custom-stats-panel {
            border-radius: 10px;
            padding: 15px;
            color: white;
            font-family: Arial, sans-serif;
            width: 100%;
            box-sizing: border-box;
        }

        .stats-title {
            font-weight: bold;
            color: #f7c948;
            margin-bottom: 10px;
        }

        .stats-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 15px;
        }

        .stat { text-align: center; flex: 1; }

        .stat-label { font-size: 12px; opacity: 0.8; }

        .stat-value {
            font-size: 18px;
            color: #00e5ff;
            margin-top: 4px;
            min-height: 20px;
        }

        .recent-title {
            margin-top: 10px;
            margin-bottom: 8px;
            font-weight: bold;
        }

        .recent-item {
            display: flex;
            justify-content: space-between;
            background: rgba(0,0,0,0.3);
            padding: 6px 8px;
            border-radius: 6px;
            margin-bottom: 6px;
            font-size: 13px;
        }

        .recent-mode { font-weight: bold; }
        .recent-time { opacity: 0.7; }

        .leaderboard-link {
            text-align: center;
            color: #00e5ff;
            cursor: pointer;
            font-size: 13px;
        }
        .stats-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}

.refresh-btn {
    cursor: pointer;
    font-size: 16px;
    opacity: 0.7;
    transition: 0.2s ease;
}

.refresh-btn:hover {
    opacity: 1;
    transform: scale(1.1);
}

.refresh-btn.spinning {
    animation: spin 0.6s linear infinite;
}

@keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}
        `;
        document.head.appendChild(style);
    }



    window.addEventListener("load", function () {
        setTimeout(() => {
            modifyLeftColumn();
            injectStyles();
            detectAccountAndLoadStats();
        }, 1);
    });

})();