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 提交的版本,檢視 最新版本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 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();
  }
})();