GeoGuessr High Level Ranks

Replace 1500+ levels with special ranks

目前為 2025-08-28 提交的版本,檢視 最新版本

// ==UserScript==
// @name         GeoGuessr High Level Ranks
// @version      1.5.1
// @description  Replace 1500+ levels with special ranks
// @match        https://www.geoguessr.com/*
// @icon         https://i.imgur.com/wHQjX4m.png
// @license      MIT
// @run-at       document-idle
// @grant        GM_addStyle
// @namespace https://example.com/
// ==/UserScript==

(function () {
  'use strict';

  // ----CHANGE THIS-----
  const RECOLOR_TOGGLE = "on";   // "on" = Enable Rank Background, "off" = Disable Rank Background
  // --------------------

  const STYLE_ID = 'gg-ranks-style'; // style element id for recolor css (so we can remove/replace it)

  // Badge sets
  const BADGES_DIVISION = [
    { min: 1500, max: 1649, url: 'https://i.imgur.com/aR6fova.png' },
    { min: 1650, max: 1799, url: 'https://i.imgur.com/No26QT6.png' },
    { min: 1800, max: 1999, url: 'https://i.imgur.com/DH3XBSr.png' },
    { min: 2000, max: 2199, url: 'https://i.imgur.com/mTCZKHg.png' },
    { min: 2200, max: Infinity, url: 'https://i.imgur.com/wHQjX4m.png' },
  ];
  const BADGES_MULTIPLAYER = [...BADGES_DIVISION];

  // Team duels: rating < 1350 => no change
  const BADGES_TEAMDUEL = [
    { min: 1350, max: 1399, url: 'https://i.imgur.com/GYUETku.png' },
    { min: 1400, max: 1499, url: 'https://i.imgur.com/QPo1lET.png' },
    { min: 1500, max: 1599, url: 'https://i.imgur.com/QLW7KyP.png' },
    { min: 1600, max: 1699, url: 'https://i.imgur.com/1K4mAXB.png' },
    { min: 1700, max: Infinity, url: 'https://i.imgur.com/rZsaPIw.png' },
  ];

  // Titles
  const TITLES = [
    { min: 1500, max: 1649, label: 'Grand Champion 3' },
    { min: 1650, max: 1799, label: 'Grand Champion 2' },
    { min: 1800, max: 1999, label: 'Grand Champion 1' },
    { min: 2000, max: 2199, label: 'Legend' },
    { min: 2200, max: Infinity, label: 'Eternal' },
  ];

  const TITLES_TEAMDUEL = [
    { min: 1350, max: 1399, label: 'Grand Champion 3' },
    { min: 1400, max: 1499, label: 'Grand Champion 2' },
    { min: 1500, max: 1599, label: 'Grand Champion 1' },
    { min: 1600, max: 1699, label: 'Legend' },
    { min: 1700, max: Infinity, label: 'Eternal' },
  ];

  // --------------------
  // Utilities
  // --------------------
  function extractFirstInteger(text) {
    if (!text) return null;
    const cleaned = String(text).replace(/,/g, '').trim();
    const m = cleaned.match(/(\d{2,5})/);
    if (!m) return null;
    const n = parseInt(m[1], 10);
    return Number.isFinite(n) ? n : null;
  }

  function pickForRating(arr, rating) {
    if (rating == null) return null;
    for (const e of arr) {
      if (rating >= e.min && rating <= e.max) return e.url || e.label || null;
    }
    return null;
  }

  function pickTitleForRatingFromArray(arr, rating) {
    if (rating == null) return null;
    for (const t of arr) {
      if (rating >= t.min && rating <= t.max) return t.label;
    }
    return null;
  }

  function pickTitleForRating(rating) {
    return pickTitleForRatingFromArray(TITLES, rating);
  }

  // --------------------
  // Style helper (create/remove style element so we can cleanly reset)
  // --------------------
  let styleEl = null;
  function ensureStyleEl() {
    if (!styleEl) {
      styleEl = document.getElementById(STYLE_ID);
      if (!styleEl) {
        styleEl = document.createElement('style');
        styleEl.id = STYLE_ID;
        document.head && document.head.appendChild(styleEl);
      }
    }
    return styleEl;
  }
  function setHeaderCss(cssText) {
    const s = ensureStyleEl();
    s.textContent = cssText || '';
  }
  function clearHeaderCss() {
    if (styleEl || document.getElementById(STYLE_ID)) {
      const s = styleEl || document.getElementById(STYLE_ID);
      s.textContent = '';
    }
  }

  // --------------------
  // Header recolor (prefix selectors to match variable class suffixes)
  // --------------------
  function recolorHeader(rating, isTeamDuel = false) {
    if (RECOLOR_TOGGLE.toLowerCase() !== "on") {
      clearHeaderCss();
      return;
    }

    let background = null;
    let overlay = null;
    let overlayOpacity = 1.0;

    if (isTeamDuel) {
      if (rating >= 1350 && rating <= 1599) {
        background = "linear-gradient(179deg, #8b0000 -3.95%, #ff0000 95.2%)";
        overlay    = "linear-gradient(41deg, #330613, #bf1755)";
        overlayOpacity = 0.7;
      } else if (rating >= 1600 && rating <= 1699) {
        background = "linear-gradient(179deg, #b8860b -3.95%, #ffd700 95.2%)";
        overlay    = "linear-gradient(41deg, #2b1900, #d68940)";
        overlayOpacity = 0.75;
      } else if (rating >= 1700) {
        background = "linear-gradient(179deg, #ffdee3 -3.95%, #ffdbe2 95.2%)";
        overlay    = "linear-gradient(41deg, #5e4d5b, #c2089a)";
        overlayOpacity = 0.6;
      } else {
        // nothing matched -> clear
        clearHeaderCss();
        return;
      }
    } else {
      if (rating >= 1500 && rating <= 1999) {
        background = "linear-gradient(179deg, #8b0000 -3.95%, #ff0000 95.2%)";
        overlay    = "linear-gradient(41deg, #330613, #bf1755)";
        overlayOpacity = 0.7;
      } else if (rating >= 2000 && rating <= 2199) {
        background = "linear-gradient(179deg, #b8860b -3.95%, #ffd700 95.2%)";
        overlay    = "linear-gradient(41deg, #2b1900, #d68940)";
        overlayOpacity = 0.75;
      } else if (rating >= 2200) {
        background = "linear-gradient(179deg, #ffdee3 -3.95%, #ffdbe2 95.2%)";
        overlay    = "linear-gradient(41deg, #5e4d5b, #c2089a)";
        overlayOpacity = 0.6;
      } else {
        clearHeaderCss();
        return;
      }
    }

    const css = `
      [class^="division-header_background__"] { background: ${background} !important; }
      [class^="division-header_pattern__"]::before { opacity: 0 !important; }
      [class^="division-header_overlay__"] { background: ${overlay} !important; opacity: ${overlayOpacity} !important; }
    `;
    setHeaderCss(css);
  }

  // --------------------
  // Robust helpers for multiplayer detection
  // --------------------
  function labelWithDigits(root) {
    if (!root) return null;
    // prefer the specific shared_yellowVariant label if present
    const specific = root.querySelector('label.shared_yellowVariant__XONv8');
    if (specific) return specific;
    // otherwise find the first label that contains a 2-5 digit number
    const labels = Array.from(root.querySelectorAll('label'));
    return labels.find(l => /\d{2,5}/.test((l.textContent || '').trim())) || null;
  }

  // --------------------
  // Read current division/team rating and detect team-duel reliably
  // returns { rating: number|null, isTeamDuel: boolean }
  // --------------------
  function getDivisionInfo() {
    const ratingEl = document.querySelector('[class^="division-header_rating__"]');
    const titleEl = document.querySelector('[class^="division-header_title__"]');

    if (!ratingEl) return { rating: null, isTeamDuel: false };

    const rating = extractFirstInteger(ratingEl.textContent || ratingEl.innerText || '');
    const ratingClass = String(ratingEl.className || '');

    // Strict matching: only treat as team-duel when we specifically see
    // the 'division-header_rating__SHoXJ' token. This prevents false
    // positives from similarly-prefixed tokens like 'division-header_rating__CQOgo'.
    const isTeamDuel = /\bdivision-header_rating__SHoXJ\b/.test(ratingClass);

    return { rating, isTeamDuel };
  }

  // --------------------
  // Reset logic (minimal) — restoration removed by user request
  // --------------------
  function resetAll() {
    // clear recolor CSS only
    clearHeaderCss();

    // clear any scheduled updates so we start fresh
    if (scheduled) {
      clearTimeout(scheduled);
      scheduled = null;
    }
  }

  // --------------------
  // Division area update (supports variable class suffixes, team-duel title class like g2AVE)
  // Restoration code removed: if no badge/title found we now leave DOM as-is.
  // --------------------
  function updateDivisionArea() {
    const info = getDivisionInfo();
    const rating = info.rating;
    const isTeamDuel = info.isTeamDuel;

    const badgeEl = document.querySelector('[class^="division-header_badge__"], img[class^="division-header_badge__"]');
    const titleEl = document.querySelector('[class^="division-header_title__"]'); // catches g2AVE, 3YYUS, etc.

    if (rating == null && !badgeEl && !titleEl) return false;

    const badgeArray = isTeamDuel ? BADGES_TEAMDUEL : BADGES_DIVISION;
    const titleArray = isTeamDuel ? TITLES_TEAMDUEL : TITLES;

    const badgeUrl = pickForRating(badgeArray, rating);
    const titleStr = pickTitleForRatingFromArray(titleArray, rating);

    if (!isNaN(rating)) recolorHeader(rating, isTeamDuel);

    // Badge handling (works even if the page already has data-orig-src / data-orig-srcset)
    if (badgeEl && badgeEl.tagName === 'IMG' && badgeUrl) {
      badgeEl.dataset.origSrc = badgeEl.dataset.origSrc || badgeEl.getAttribute('data-orig-src') || badgeEl.getAttribute('src') || '';
      badgeEl.dataset.origSrcset = badgeEl.dataset.origSrcset || badgeEl.getAttribute('data-orig-srcset') || badgeEl.getAttribute('srcset') || '';

      const cur = badgeEl.getAttribute('src') || '';
      if (!cur.includes(badgeUrl)) {
        badgeEl.setAttribute('src', badgeUrl);
        badgeEl.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`);
        badgeEl.dataset.replaced = 'true';
        console.log('[GG Division] Replaced division badge ->', badgeUrl, 'rating:', rating, 'teamDuel:', isTeamDuel);
      }
    }

    // Title handling (only change if we have a title for this rating)
    if (titleEl && titleStr) {
      if (!('origTitle' in titleEl.dataset)) {
        titleEl.dataset.origTitle = (titleEl.textContent || '').trim();
        titleEl.dataset.origDataOriginalTitle = titleEl.getAttribute('data-original-title') || '';
      }
      const cur = (titleEl.textContent || '').trim();
      if (cur !== titleStr) {
        titleEl.textContent = titleStr;
        titleEl.dataset.replacedTitle = 'true';
        console.log('[GG Division] Set division title ->', titleStr, 'rating:', rating, 'teamDuel:', isTeamDuel);
      }
    }

    return true;
  }

  // --------------------
  // Team badges update — updates team-matchmaking header badge and rating_wrapper images when on a team duel
  // Restoration removed: if no badgeUrl we simply do nothing.
  // --------------------
  function updateTeamBadges(info) {
    if (!info) info = getDivisionInfo();
    const rating = info.rating;
    const isTeamDuel = info.isTeamDuel;
    if (!isTeamDuel || rating == null) return false;

    const badgeUrl = pickForRating(BADGES_TEAMDUEL, rating);
    if (!badgeUrl) return false;

    // team matchmaking header badge(s) (prefix selector to handle dynamic suffixes)
    const teamHeaderImgs = Array.from(document.querySelectorAll('img[class^="team-matchmaking-layout_badge__"], [class^="team-matchmaking-layout_badge__"]'));
    teamHeaderImgs.forEach(img => {
      if (img && img.tagName === 'IMG') {
        img.dataset.origSrc = img.dataset.origSrc || img.getAttribute('data-orig-src') || img.getAttribute('src') || '';
        img.dataset.origSrcset = img.dataset.origSrcset || img.getAttribute('data-orig-srcset') || img.getAttribute('srcset') || '';
        const cur = img.getAttribute('src') || '';
        if (!cur.includes(badgeUrl)) {
          img.setAttribute('src', badgeUrl);
          img.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`);
          img.dataset.replaced = 'true';
          console.log('[GG Team] Replaced team-matchmaking badge ->', badgeUrl, 'rating:', rating);
        }
      }
    });

    // rating_wrapper images (example: .rating_wrapper__22uFu img)
    const ratingWrapperImgs = Array.from(document.querySelectorAll('.rating_wrapper__22uFu img, [class^="rating_wrapper__22uFu"] img'));
    ratingWrapperImgs.forEach(img => {
      if (img && img.tagName === 'IMG') {
        img.dataset.origSrc = img.dataset.origSrc || img.getAttribute('data-orig-src') || img.getAttribute('src') || '';
        img.dataset.origSrcset = img.dataset.origSrcset || img.getAttribute('data-orig-srcset') || img.getAttribute('srcset') || '';
        const cur = img.getAttribute('src') || '';
        if (!cur.includes(badgeUrl)) {
          img.setAttribute('src', badgeUrl);
          img.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`);
          img.dataset.replaced = 'true';
          console.log('[GG Team] Replaced rating_wrapper badge ->', badgeUrl, 'rating:', rating);
        }
      }
    });

    return true;
  }

  // --------------------
  // Multiplayer boxes (robust)
  // --------------------
  function findMultiplayerBoxes() {
    // primary selector (unchanged)
    let boxes = Array.from(document.querySelectorAll('.multiplayer_ratingBox__05Gko'));
    if (boxes.length) return boxes;

    // fallback: search inside multiplayer root for elements that contain a label with digits
    const root = document.querySelector('.multiplayer_root__jmpXA');
    if (!root) return [];

    const candidates = Array.from(root.querySelectorAll('div,section,article'));
    const filtered = candidates.filter(el => {
      // accept if there's any label with a 2-5 digit number
      const lbl = Array.from(el.querySelectorAll('label')).some(l => /\d{2,5}/.test((l.textContent || '').trim()));
      return lbl;
    });

    // remove nested duplicates (keep top-level ones)
    const topLevel = filtered.filter((el, i, arr) => !arr.some(other => other !== el && other.contains(el)));
    return topLevel;
  }

  // --------------------
  // New helpers: find all digit labels and pair boxes to labels by proximity
  // --------------------
  function findAllDigitLabels() {
    return Array.from(document.querySelectorAll('label'))
      .filter(l => /\d{2,5}/.test((l.textContent || '').trim()));
  }

  // Pair boxes to labels by proximity (prefer vertical proximity)
  function pairBoxesToLabels(boxes) {
    const labels = findAllDigitLabels();
    if (!boxes.length || !labels.length) return new Map();

    const assigned = new Set();
    const mapping = new Map();

    for (const box of boxes) {
      const br = box.getBoundingClientRect();
      let best = null;
      let bestScore = Infinity;

      for (const lbl of labels) {
        if (assigned.has(lbl)) continue;
        const lr = lbl.getBoundingClientRect();
        // distance: weight vertical more than horizontal
        // (use fallback if getBoundingClientRect returns zeros)
        let dy = Math.abs((lr.top || 0) - (br.top || 0));
        let dx = Math.abs((lr.left || 0) - (br.left || 0));
        let score = dy * 2 + dx;
        // If rects are zero (not rendered), use DOM order distance as fallback
        if ((!lr.width && !lr.height) || (!br.width && !br.height)) {
          const li = Array.prototype.indexOf.call(labels, lbl);
          const bi = Array.prototype.indexOf.call(boxes, box);
          score = Math.abs(li - bi) * 1000; // coarse fallback
        }
        if (score < bestScore) {
          bestScore = score;
          best = lbl;
        }
      }

      if (best) {
        mapping.set(box, best);
        assigned.add(best);
      }
    }

    return mapping;
  }

  // --------------------
  // Updated multiplayer box update: accepts optional ratingLabelOverride and isTeamDuel flag
  // Restoration removed: if no badge/title found we leave the DOM unchanged.
  // --------------------
  function updateMultiplayerBox(box, idx, ratingLabelOverride, isTeamDuelFlag) {
    // rating label robustly (use the override if provided)
    const ratingLabel = ratingLabelOverride || labelWithDigits(box);

    // title label: prefer the visible large label (your snippet shows label_label__9xkbh), but also accept generic label
    let titleLabel = box.querySelector('label[data-original-title]') || box.querySelector('label.label_label__9xkbh') || Array.from(box.querySelectorAll('label')).find(l => /[A-Za-z]/.test((l.textContent || '').trim()));

    const imgEl = box.querySelector('img.multiplayer_icon__hRbEa') || box.querySelector('img');
    const rating = ratingLabel ? extractFirstInteger(ratingLabel.textContent || '') : null;

    // Choose arrays depending on whether this box should use team-duel assets
    const badgeArray = isTeamDuelFlag ? BADGES_TEAMDUEL : BADGES_MULTIPLAYER;
    const titleArray = isTeamDuelFlag ? TITLES_TEAMDUEL : TITLES;

    const badgeUrl = pickForRating(badgeArray, rating);
    const titleStr = pickTitleForRatingFromArray(titleArray, rating);

    // Image handling — only change if a badgeUrl exists
    if (imgEl && imgEl.tagName === 'IMG' && badgeUrl) {
      imgEl.dataset.origSrc = imgEl.dataset.origSrc || imgEl.getAttribute('data-orig-src') || imgEl.getAttribute('src') || '';
      imgEl.dataset.origSrcset = imgEl.dataset.origSrcset || imgEl.getAttribute('data-orig-srcset') || imgEl.getAttribute('srcset') || '';

      const cur = (imgEl.getAttribute('src') || '');
      if (!cur.includes(badgeUrl)) {
        imgEl.setAttribute('src', badgeUrl);
        imgEl.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`);
        imgEl.dataset.replaced = 'true';
        console.log('[GG MP] Replaced image for box', idx, '->', badgeUrl, 'rating:', rating, 'teamDuel:', !!isTeamDuelFlag);
      }
    }

    // Title handling — only change if we have a title for this rating
    if (titleLabel && titleStr) {
      if (!('origTitle' in titleLabel.dataset)) {
        titleLabel.dataset.origTitle = (titleLabel.textContent || '').trim();
        titleLabel.dataset.origDataOriginalTitle = titleLabel.getAttribute('data-original-title') || '';
      }
      const cur = (titleLabel.textContent || '').trim();
      if (cur !== titleStr) {
        titleLabel.textContent = titleStr;
        try { titleLabel.setAttribute('data-original-title', titleStr); } catch (e) { /* ignore */ }
        titleLabel.dataset.replacedTitle = 'true';
        console.log('[GG MP] Set title for box', idx, '->', titleStr, 'rating:', rating, 'teamDuel:', !!isTeamDuelFlag);
      }
    }
  }

  // --------------------
  // Updated updateMultiplayerAll uses pairing and passes matched label into updateMultiplayerBox.
  // The second multiplayer box (index 1) is treated as a team-duel box and uses team-duel assets.
  // --------------------
  function updateMultiplayerAll() {
    const boxes = findMultiplayerBoxes();
    if (!boxes || !boxes.length) return false;

    // Pair boxes -> labels (nearest)
    const mapping = pairBoxesToLabels(boxes);

    boxes.forEach((b, i) => {
      try {
        const ratingLabel = mapping.get(b) || null;
        // Treat the second box as team-duel (index === 1)
        const isTeamDuelForThisBox = (i === 1);
        updateMultiplayerBox(b, i, ratingLabel, isTeamDuelForThisBox);
      } catch (e) { console.error('updateMultiplayerBox error', e); }
    });
    return true;
  }

  // --------------------
  // New: update team list entries (leaderboard/team pages)
  // - Finds column content elements (class prefix 'teams-detailed-leaderboard_columnContent__...')
  // - Reads the numeric rating label inside
  // - Locates the nearest team icon image in the same row and replaces it with BADGES_TEAMDUEL for that rating (if >=1350)
  // - Does not restore original images (per your request)
  // --------------------
  function findNearestTeamIconFrom(el) {
    if (!el) return null;

    // Search up a few ancestor levels for a container that contains a team image
    let ancestor = el;
    for (let depth = 0; depth < 5 && ancestor; depth++) {
      // look for known patterns inside ancestor
      const img = ancestor.querySelector('img[class^="team-selector_divisionImage__"], [class^="team-selector_divisionImageWrapper__"] img, img.team-selector_divisionImage__U12_e, img');
      if (img) return img;
      ancestor = ancestor.parentElement;
    }

    // fallback: scan siblings of el up to 3 siblings in either direction for an img
    let sib = el.previousElementSibling;
    for (let i = 0; i < 6 && sib; i++, sib = sib.previousElementSibling) {
      const img = sib.querySelector && sib.querySelector('img');
      if (img) return img;
    }
    sib = el.nextElementSibling;
    for (let i = 0; i < 6 && sib; i++, sib = sib.nextElementSibling) {
      const img = sib.querySelector && sib.querySelector('img');
      if (img) return img;
    }

    return null;
  }

  function updateTeamListEntries() {
    // column elements that contain rating values — prefix selector to be robust to hash suffixes
    const cols = Array.from(document.querySelectorAll('[class^="teams-detailed-leaderboard_columnContent__"]'));
    if (!cols.length) return false;

    let changed = false;
    cols.forEach((col) => {
      try {
        // find any label in the column that has a 2-5 digit rating
        const label = Array.from(col.querySelectorAll('label')).find(l => /\d{2,5}/.test((l.textContent || '').trim()));
        if (!label) return;

        const rating = extractFirstInteger(label.textContent || '');
        if (rating == null) return;

        // get the nearest icon for this row/column
        const img = findNearestTeamIconFrom(col);
        if (!img || img.tagName !== 'IMG') return;

        // pick team-duel badge (pickForRating returns null if <1350)
        const badgeUrl = pickForRating(BADGES_TEAMDUEL, rating);
        if (!badgeUrl) return;

        // set original data attributes if not already set
        img.dataset.origSrc = img.dataset.origSrc || img.getAttribute('data-orig-src') || img.getAttribute('src') || '';
        img.dataset.origSrcset = img.dataset.origSrcset || img.getAttribute('data-orig-srcset') || img.getAttribute('srcset') || '';

        const cur = img.getAttribute('src') || '';
        if (!cur.includes(badgeUrl)) {
          img.setAttribute('src', badgeUrl);
          img.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`);
          img.dataset.replaced = 'true';
          changed = true;
          console.log('[GG TeamsList] Replaced team list icon ->', badgeUrl, 'rating:', rating);
        }
      } catch (e) {
        // ignore individual row errors
        console.error('updateTeamListEntries item error', e);
      }
    });

    return changed;
  }

  // --------------------
  // Combined update + observer + fallback
  // --------------------
  function updateAllOnce() {
    let changed = false;
    changed = updateDivisionArea() || changed;
    // pass divisionInfo to team badge updater
    const divisionInfo = getDivisionInfo();
    changed = updateTeamBadges(divisionInfo) || changed;
    changed = updateMultiplayerAll() || changed;
    // new: update leaderboard / teams list icons
    changed = updateTeamListEntries() || changed;
    return changed;
  }

  // --------------------
  // Debounce / schedule / observer (unchanged)
  // --------------------
  let scheduled = null;
  function scheduleUpdate() {
    if (scheduled) return;
    scheduled = setTimeout(() => {
      scheduled = null;
      updateAllOnce();
    }, 150);
  }

  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) { scheduleUpdate(); break; }
      if (m.type === 'characterData') { scheduleUpdate(); break; }
      if (m.type === 'attributes') { scheduleUpdate(); break; }
    }
  });

  function startObserving() {
    if (!document.body) return;
    observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true });
    scheduleUpdate();
  }

  // --------------------
  // SPA route-change detection (pushState/replaceState/popstate)
  // Note: resetAll no longer restores images/titles per your request.
  // --------------------
  (function installRouteWatcher() {
    const wrap = (method) => {
      const orig = history[method];
      return function () {
        const rv = orig.apply(this, arguments);
        window.dispatchEvent(new Event('gg-route-change'));
        return rv;
      };
    };
    history.pushState = wrap('pushState');
    history.replaceState = wrap('replaceState');
    window.addEventListener('popstate', () => window.dispatchEvent(new Event('gg-route-change')));
    // On route change: reset minimal state and re-run update
    window.addEventListener('gg-route-change', () => {
      try { resetAll(); } catch (e) { /* ignore */ }
      // small delay to let the new page render DOM
      setTimeout(updateAllOnce, 250);
    });
  })();

  // start
  if (document.readyState === 'loading') {
    window.addEventListener('DOMContentLoaded', startObserving, { once: true });
  } else startObserving();

  const fallbackInterval = setInterval(() => updateAllOnce(), 5000);

  // cleanup
  window.addEventListener('beforeunload', () => {
    observer.disconnect();
    clearInterval(fallbackInterval);
    if (scheduled) clearTimeout(scheduled);
  });

  // initial run
  setTimeout(updateAllOnce, 300);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址