您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a small country flag next to every visible player name across the GeoGuessr interface — from friends lists and the side panel to leaderboards, search results, and more — making it easy to see where players are from at a glance.
当前为
// ==UserScript== // @name GeoGuessr Friends: Tiny Flag by Name // @namespace gg-friends-flag-stable // @version 3.0.2 // @description Adds a small country flag next to every visible player name across the GeoGuessr interface — from friends lists and the side panel to leaderboards, search results, and more — making it easy to see where players are from at a glance. // @match https://www.geoguessr.com/* // @run-at document-start // @grant GM_addStyle // @license MIT // ==/UserScript== (function () { "use strict"; // Base URL for GeoGuessr's own flag icons const FLAG_BASE = "https://www.geoguessr.com/static/flags"; // API endpoint to get detailed user info (including countryCode) const USER_API = (id) => `/api/v3/users/${id}`; // CSS selector for the element containing the username const NAME_SEL = '[class*="user-nick_nick__"]'; // CSS selector for the profile link (contains user ID in href) const LINK_SEL = "a[href^='/user/']"; // --- Inject some basic CSS styling for our flag --- GM_addStyle(` .gg-flag { display:inline-block; margin-left:6px; vertical-align:middle; line-height:1; } .gg-flag img { height:12px; width:auto; display:inline-block; vertical-align:middle; } `); // --- Request queue (limits concurrent fetches to avoid lag) --- const MAX_CONCURRENCY = 20; let running = 0; const queue = []; function enqueue(task) { return new Promise((resolve, reject) => { queue.push({ task, resolve, reject }); pump(); }); } function pump() { while (running < MAX_CONCURRENCY && queue.length) { const { task, resolve, reject } = queue.shift(); running++; Promise.resolve() .then(task) .then(resolve, reject) .finally(() => { running--; pump(); }); } } // --- Caches to avoid duplicate work --- const countryById = new Map(); // userId -> Promise<countryCode|null> const processed = new WeakSet(); // name elements already processed const observed = new WeakSet(); // nodes already given to IntersectionObserver // Extract user ID from a link like "/user/<id>" function extractIdFromHref(href) { const m = String(href||"").match(/\/user\/([^/?#]+)/i); return m ? m[1] : null; } // Fetch the user's country code (cached) function getCountry(id) { if (!id) return Promise.resolve(null); if (countryById.has(id)) return countryById.get(id); const p = enqueue(() => fetch(USER_API(id), { credentials: "include" }) .then(r => (r.ok ? r.json() : null)) .then(j => (j && j.countryCode ? String(j.countryCode).toUpperCase() : null)) .catch(() => null) ); countryById.set(id, p); return p; } // Insert a small flag element after the username function placeFlagAfterName(nameEl, code) { if (!nameEl || !code) return; if (processed.has(nameEl)) return; // If a flag is already present, update it and return const next = nameEl.nextElementSibling; if (next && next.classList.contains("gg-flag")) { const img = next.querySelector("img"); if (img) img.src = `${FLAG_BASE}/${code}.svg`; processed.add(nameEl); return; } // Create the flag container const span = document.createElement("span"); span.className = "gg-flag"; const img = document.createElement("img"); img.alt = ""; img.decoding = "async"; img.src = `${FLAG_BASE}/${code}.svg`; img.onerror = () => span.remove(); span.appendChild(img); // Insert flag directly after the username div (before flair badges) nameEl.insertAdjacentElement("afterend", span); processed.add(nameEl); } // Process a profile link to fetch and insert the flag async function handleAnchor(a) { if (!a) return; // Find the username element inside the link const nameEl = a.querySelector(NAME_SEL); if (!nameEl || processed.has(nameEl)) return; const id = extractIdFromHref(a.getAttribute("href")); if (!id) return; const code = await getCountry(id); if (!code) return; placeFlagAfterName(nameEl, code); } // IntersectionObserver: only process visible friend cards const io = new IntersectionObserver((entries) => { for (const e of entries) { if (!e.isIntersecting) continue; io.unobserve(e.target); observed.delete(e.target); handleAnchor(e.target.closest(LINK_SEL) || e.target); } }, { root: null, rootMargin: "0px 0px 200px 0px", threshold: 0 }); // Start watching a profile link function watchAnchor(a) { if (!a || observed.has(a)) return; observed.add(a); const nameEl = a.querySelector(NAME_SEL); io.observe(nameEl || a); } // Scan a DOM subtree for friend profile links function scan(root) { if (!root || root.nodeType !== 1) return; root.querySelectorAll(LINK_SEL).forEach(watchAnchor); } // MutationObserver: react to new friend cards being added to the DOM const mo = new MutationObserver((muts) => { for (const m of muts) { if (m.addedNodes && m.addedNodes.length) { for (const n of m.addedNodes) scan(n); } } }); // Initialize observers function start() { if (!document.body) { requestAnimationFrame(start); return; } mo.observe(document.documentElement, { childList: true, subtree: true }); scan(document); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", start, { once: true }); } else { start(); } })();