您需要先安装一个扩展,例如 篡改猴、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.4.0 // @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. // @icon https://www.google.com/s2/favicons?domain=geoguessr.com // @match https://www.geoguessr.com/* // @run-at document-start // @grant GM_addStyle // @license MIT // ==/UserScript== (function () { "use strict"; const FLAG_BASE = "https://www.geoguessr.com/static/flags"; const USER_API = (id) => `/api/v3/users/${id}`; const NAME_SEL = '[class*="user-nick_nick__"]'; const LINK_SEL = "a[href^='/user/']"; GM_addStyle(` .gg-flag { display:inline-block; margin-left:6px; vertical-align:middle; line-height:1; flex:0 0 auto; } .gg-flag img { height:12px; width:auto; display:inline-block; vertical-align:middle; } `); // --- Fetch queue to avoid overloading --- const MAX_CONCURRENCY = 50; 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 --- const countryById = new Map(); // Cache: id -> Promise<string|null> const processed = new WeakSet(); // Marks name elements already processed const observed = new WeakSet(); // Elements already watched by IntersectionObserver function extractIdFromHref(href) { const m = String(href||"").match(/\/user\/([^/?#]+)/i); return m ? m[1] : null; } 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; } // --- KEY: Simple, robust check if GeoGuessr already shows a flag near the name --- function hasNativeFlagNear(nameEl) { // We go up a few levels and search for their flag classes in the respective ancestor's subtree. // Matches both the IMG class itself (country-flag_flag__) and the container class (user-nick_flag__). let cur = nameEl; for (let i = 0; cur && i < 6; i++) { if (cur.querySelector('img[class^="country-flag_flag__"], [class^="user-nick_flag__"] img')) { return true; } cur = cur.parentElement; } return false; } function placeFlagAfterName(nameEl, code) { if (!nameEl || !code) return; // Never add if GeoGuessr already shows a flag near the name if (hasNativeFlagNear(nameEl)) { processed.add(nameEl); return; } // Don’t add more than one of our own if (nameEl.nextElementSibling?.classList?.contains("gg-flag")) { const img = nameEl.nextElementSibling.querySelector("img"); if (img) img.src = `${FLAG_BASE}/${code}.svg`; processed.add(nameEl); return; } 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); nameEl.insertAdjacentElement("afterend", span); processed.add(nameEl); } async function handleAnchor(a) { if (!a) return; const nameEl = a.querySelector(NAME_SEL); if (!nameEl || processed.has(nameEl)) return; // If GeoGuessr already has the flag near the name — do nothing. if (hasNativeFlagNear(nameEl)) { processed.add(nameEl); return; } const id = extractIdFromHref(a.getAttribute("href")); if (!id) return; const code = await getCountry(id); if (!code) { processed.add(nameEl); return; } // After await, the page might have inserted its own flag — check again if (hasNativeFlagNear(nameEl)) { processed.add(nameEl); return; } placeFlagAfterName(nameEl, code); } // Process only when the row is actually visible const io = new IntersectionObserver((entries) => { for (const e of entries) { if (!e.isIntersecting) continue; io.unobserve(e.target); observed.delete(e.target); const anchor = e.target.closest(LINK_SEL) || e.target; handleAnchor(anchor); } }, { root: null, rootMargin: "0px 0px 200px 0px", threshold: 0 }); function watchAnchor(a) { if (!a || observed.has(a)) return; observed.add(a); const nameEl = a.querySelector(NAME_SEL); io.observe(nameEl || a); } function scan(root) { if (!root || root.nodeType !== 1) return; root.querySelectorAll(LINK_SEL).forEach(watchAnchor); } // Continuously find new nodes const mo = new MutationObserver((muts) => { for (const m of muts) { if (m.addedNodes && m.addedNodes.length) { for (const n of m.addedNodes) scan(n); } } }); 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(); } })();