GeoGuessr Friends: Tiny Flag by Name

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.

当前为 2025-08-14 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
  }
})();