Custom Status Bar (Fixed)

Fork of Custom Status Bar by zorby. Fixes broken selectors caused by GeoGuessr class name hash updates. Original: https://greasyfork.org/en/scripts/465248-custom-status-bar

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Custom Status Bar (Fixed)
// @description  Fork of Custom Status Bar by zorby. Fixes broken selectors caused by GeoGuessr class name hash updates. Original: https://greasyfork.org/en/scripts/465248-custom-status-bar
// @version      1.0.0
// @license      MIT
// @author       zorby#1431 (original), fork maintained separately
// @namespace    https://greasyfork.org/
// @match        https://www.geoguessr.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
// ==/UserScript==

function pathMatches(path) {
    return location.pathname.match(new RegExp(`^/(?:[^/]+/)?${path}$`))
}

function getIndex(element) {
    if (!element) return -1
    let i = 0
    while (element = element.previousElementSibling) i++
    return i
}

const OBSERVER_CONFIG = {
    characterDataOldValue: false,
    subtree: true,
    childList: true,
    characterData: false
}

const SCRIPT_PREFIX = "csb__"
const CONFIG_KEY = SCRIPT_PREFIX + "config"
const STYLE_ID = SCRIPT_PREFIX + "style"
const PERCENTAGE_INPUT_CLASS = SCRIPT_PREFIX + "percentage-input"
const COLOR_INPUT_CLASS = SCRIPT_PREFIX + "color-input"
const TEXT_INPUT_CLASS = SCRIPT_PREFIX + "text-input"
const DELETE_BUTTON_CLASS = SCRIPT_PREFIX + "delete-button"
const STANDARD_BUTTON_CLASS = SCRIPT_PREFIX + "standard-button"
const DOWN_BUTTON_CLASS = SCRIPT_PREFIX + "down-button"
const UP_BUTTON_CLASS = SCRIPT_PREFIX + "up-button"
const CUSTOMIZE_STATUS_BAR_BUTTON_ID = SCRIPT_PREFIX + "customize-status-bar-button"
const ADD_GRADIENT_NODE_BUTTON_ID = SCRIPT_PREFIX + "add-gradient-node-button"
const CUSTOMIZE_STATUS_BAR_SCREEN_ID = SCRIPT_PREFIX + "customize-status-bar-screen"
const GRADIENT_NODE_LIST_ID = SCRIPT_PREFIX + "gradient-node-list"
const TEXT_COLOR_NODE_LIST_ID = SCRIPT_PREFIX + "text-color-node-list"
const RESUME_BUTTON_ID = SCRIPT_PREFIX + "resume-button"
const BACKGROUND_COLOR_ID = SCRIPT_PREFIX + "background-color"
const TEMP_COLOR_VAR = "--" + SCRIPT_PREFIX + "temp-color"
const RESET_DEFAULTS_BUTTON_ID = SCRIPT_PREFIX + "reset-defaults-button"

const defaultNode = () => ({ color: "#000000", percentage: 100 })

const DEFAULT_BACKGROUND_COLOR = "var(--ds-color-purple-80)"
const DEFAULT_GRADIENT_NODES = [
    { color: "#a19bd999", percentage: 0 },
    { color: "#00000000", percentage: 50 },
    { color: "#00000000", percentage: 50 }
]
const DEFAULT_TEXT_COLORS = [
    "var(--ds-color-purple-20)",
    "var(--ds-color-white)"
]

const configString = localStorage.getItem(CONFIG_KEY)
let gradientNodes = DEFAULT_GRADIENT_NODES
let textColors = DEFAULT_TEXT_COLORS
let backgroundColor = DEFAULT_BACKGROUND_COLOR

if (configString) {
    const config = JSON.parse(configString)
    gradientNodes = config.gradient
    textColors = config.textColors
    backgroundColor = config.backgroundColor
}

// ─── Robust selectors (class*= to survive GeoGuessr hash updates) ─────────────

function q(selector) {
    return document.querySelector(selector)
}

function getGameMenuOverlay() {
    return q('[class*="game-menu_inGameMenuOverlay"]')
}

function getInGameLayout() {
    return q('[class*="in-game_layout"]')
}

function getStatusBar() {
    return q('[class*="game_status_"]')
}

function getGameCanvas() {
    return q('[class*="game_canvas"]')
}

// Close the settings overlay by clicking its primary (Resume) button
function closeSettingsOverlay() {
    const overlay = getGameMenuOverlay()
    if (!overlay) return
    // Try the primary button inside the overlay's button group
    const primaryBtn =
        overlay.querySelector('[class*="button_variantPrimary"]') ||
        overlay.querySelector('[class*="buttons_buttons"] button') ||
        overlay.querySelector('button')
    if (primaryBtn) primaryBtn.click()
}

// ─── Templates ────────────────────────────────────────────────────────────────

const CUSTOMIZE_STATUS_BAR_BUTTON = `
  <button id="${CUSTOMIZE_STATUS_BAR_BUTTON_ID}" class="button_button__aR6_e button_variantSecondary__hvM_F">
    Customize status bar
  </button>
  <div class="game-menu_divider__IhA4t"></div>
`

const COLOR_WIDGET = `
  <input type="color" class="${COLOR_INPUT_CLASS}"></input>
  <input type="text" class="${TEXT_INPUT_CLASS}" style="width: 5.5rem;" required></input>
`

const GRADIENT_NODE = `
  <div style="display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto;">
    <div class="grid-element">
      <input type="number" class="${PERCENTAGE_INPUT_CLASS}" min="0" max="100" step="any" required></input>
      <div style="font-weight:700;">%</div>
    </div>
    <div class="grid-element" style="flex-direction:row-reverse">
      <button class="${DELETE_BUTTON_CLASS}">X</button>
      <button class="${STANDARD_BUTTON_CLASS} ${DOWN_BUTTON_CLASS}">↓</button>
      <button class="${STANDARD_BUTTON_CLASS} ${UP_BUTTON_CLASS}" style="margin-left:1rem;">↑</button>
      ${COLOR_WIDGET}
    </div>
  </div>
`

const colorNode = (label, id) => `
  <div id="${id}" style="display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto;">
    <div class="grid-element">
      <div style="font-weight:700;">${label}</div>
    </div>
    <div class="grid-element" style="flex-direction:row-reverse">
      ${COLOR_WIDGET}
    </div>
  </div>
`

// ─── Color evaluation ─────────────────────────────────────────────────────────

const evaluateColor = (color) => {
    const statusBar = getStatusBar()
    if (!statusBar) return "#000000"
    statusBar.style.setProperty(TEMP_COLOR_VAR, color)
    const hexColor = window.getComputedStyle(statusBar).getPropertyValue(TEMP_COLOR_VAR)
    if (!hexColor.startsWith("#")) return "#000000"
    if (hexColor.length > 7) return hexColor.substr(0, 7)
    if (hexColor.length === 4) return "#" + hexColor.substr(1).split("").map(c => c + c).join("")
    return hexColor
}

// ─── Style update ─────────────────────────────────────────────────────────────

const generateGradientString = () =>
    `linear-gradient(180deg, ${gradientNodes.map(n => `${n.color} ${n.percentage}%`).join(",")})`

const updateStatusBarStyles = () => {
    const style = document.getElementById(STYLE_ID)
    if (!style) return

    // Use wildcard selectors so CSS survives class hash changes too
    style.innerHTML = `
      [class*="slanted-wrapper_variantPurple"] {
        --variant-background-color: ${generateGradientString()}, ${backgroundColor};
      }
      [class*="slanted-wrapper_variantPurple"] [class*="status_label"] {
        color: ${textColors[0]}
      }
      [class*="slanted-wrapper_variantPurple"] [class*="status_value"] {
        color: ${textColors[1]}
      }
    `

    localStorage.setItem(CONFIG_KEY, JSON.stringify({
        gradient: gradientNodes,
        textColors: textColors,
        backgroundColor: backgroundColor
    }))
}

// ─── Node builders ────────────────────────────────────────────────────────────

const appendTextColorNode = (parent, label, index) => {
    parent.insertAdjacentHTML("beforeend", colorNode(label, ""))
    const textColorNode = parent.lastElementChild
    const colorInput = textColorNode.querySelector(`.${COLOR_INPUT_CLASS}`)
    const colorTextInput = textColorNode.querySelector(`.${TEXT_INPUT_CLASS}`)

    colorInput.value = evaluateColor(textColors[index])
    colorTextInput.value = textColors[index]

    colorInput.oninput = () => {
        textColors[index] = colorInput.value
        colorTextInput.value = textColors[index]
        updateStatusBarStyles()
    }
    colorTextInput.oninput = () => {
        textColors[index] = colorTextInput.value
        colorInput.value = evaluateColor(textColors[index])
        updateStatusBarStyles()
    }
}

const appendGradientNode = (parent) => {
    parent.insertAdjacentHTML("beforeend", GRADIENT_NODE)
    const gradientNode = parent.lastElementChild

    const percentageInput = gradientNode.querySelector(`.${PERCENTAGE_INPUT_CLASS}`)
    const colorInput = gradientNode.querySelector(`.${COLOR_INPUT_CLASS}`)
    const colorTextInput = gradientNode.querySelector(`.${TEXT_INPUT_CLASS}`)
    const deleteButton = gradientNode.querySelector(`.${DELETE_BUTTON_CLASS}`)
    const upButton = gradientNode.querySelector(`.${UP_BUTTON_CLASS}`)
    const downButton = gradientNode.querySelector(`.${DOWN_BUTTON_CLASS}`)

    const updateInputs = () => {
        const idx = getIndex(gradientNode)
        percentageInput.value = gradientNodes[idx].percentage
        colorInput.value = evaluateColor(gradientNodes[idx].color)
        colorTextInput.value = gradientNodes[idx].color
    }
    gradientNode.updateInputs = updateInputs
    updateInputs()

    percentageInput.oninput = () => {
        gradientNodes[getIndex(gradientNode)].percentage = percentageInput.value
        updateStatusBarStyles()
    }
    colorInput.oninput = () => {
        const idx = getIndex(gradientNode)
        gradientNodes[idx].color = colorInput.value
        colorTextInput.value = gradientNodes[idx].color
        updateStatusBarStyles()
    }
    colorTextInput.oninput = () => {
        const idx = getIndex(gradientNode)
        gradientNodes[idx].color = colorTextInput.value
        colorInput.value = evaluateColor(gradientNodes[idx].color)
        updateStatusBarStyles()
    }
    deleteButton.onclick = () => {
        gradientNodes.splice(getIndex(gradientNode), 1)
        gradientNode.remove()
        updateStatusBarStyles()
    }
    upButton.onclick = () => {
        const idx = getIndex(gradientNode)
        if (idx === 0) return
        ;[gradientNodes[idx].color, gradientNodes[idx - 1].color] =
            [gradientNodes[idx - 1].color, gradientNodes[idx].color]
        parent.children[idx - 1].updateInputs()
        updateInputs()
        updateStatusBarStyles()
    }
    downButton.onclick = () => {
        const idx = getIndex(gradientNode)
        if (idx >= gradientNodes.length - 1) return
        ;[gradientNodes[idx].color, gradientNodes[idx + 1].color] =
            [gradientNodes[idx + 1].color, gradientNodes[idx].color]
        parent.children[idx + 1].updateInputs()
        updateInputs()
        updateStatusBarStyles()
    }
}

// ─── Customize screen ─────────────────────────────────────────────────────────

const CUSTOMIZE_STATUS_BAR_SCREEN = `
  <div id="${CUSTOMIZE_STATUS_BAR_SCREEN_ID}" class="game-menu_inGameMenuOverlay__XWQpg">
    <style>
      .${PERCENTAGE_INPUT_CLASS}, .${COLOR_INPUT_CLASS},
      .${TEXT_INPUT_CLASS}, .${DELETE_BUTTON_CLASS}, .${STANDARD_BUTTON_CLASS} {
        background: rgba(255,255,255,0.1);
        color: white;
        border: none;
        border-radius: 5px;
        font-family: var(--default-font);
        font-size: var(--font-size-14);
        padding: 0.5rem;
      }
      .${PERCENTAGE_INPUT_CLASS}, .${COLOR_INPUT_CLASS} { width: 3rem; }
      .${PERCENTAGE_INPUT_CLASS}, .${TEXT_INPUT_CLASS} {
        text-align: center;
        -moz-appearance: textfield;
      }
      .${PERCENTAGE_INPUT_CLASS}::-webkit-outer-spin-button,
      .${PERCENTAGE_INPUT_CLASS}::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
      .${COLOR_INPUT_CLASS} { height: 100%; padding: 0.25rem; }
      .${COLOR_INPUT_CLASS}::-webkit-color-swatch-wrapper { padding: 0; }
      .${COLOR_INPUT_CLASS}::-webkit-color-swatch { border: none; border-radius: 5px; }
      .${TEXT_INPUT_CLASS}:invalid, .${PERCENTAGE_INPUT_CLASS}:invalid {
        background: rgba(209,27,38,0.1);
        color: var(--color-red-60);
      }
      .${DELETE_BUTTON_CLASS}, .${STANDARD_BUTTON_CLASS} { width: 2rem; user-select: none; }
      .${DELETE_BUTTON_CLASS} { background: rgba(209,27,38,0.1); }
      .${DELETE_BUTTON_CLASS}:hover, .${STANDARD_BUTTON_CLASS}:hover, .${COLOR_INPUT_CLASS}:hover { cursor: pointer; }
      .${DELETE_BUTTON_CLASS}:hover { background: var(--color-red-60); }
      .${STANDARD_BUTTON_CLASS}:hover { background: var(--color-grey-70); }
      #${CUSTOMIZE_STATUS_BAR_SCREEN_ID} .grid-element { display: flex; align-items: center; gap: 0.5rem; }
    </style>
    <div class="game-menu_inGameMenuContentWrapper__mGVx4">
      <div class="game-menu_innerContainer__Kxqz_">
        <p class="game-menu_header__Hs03p">Customize Status Bar</p>
        <div class="game-menu_volumeContainer__aWb0Y" style="display:flex;flex-direction:column;gap:0.4rem;">
          <p class="game-menu_subHeader__Ul5Vl">Background</p>
          ${colorNode("Color", BACKGROUND_COLOR_ID)}
        </div>
        <div class="game-menu_volumeContainer__aWb0Y" style="display:flex;flex-direction:column;gap:0.4rem;">
          <p class="game-menu_subHeader__Ul5Vl">Gradient</p>
          <div id="${GRADIENT_NODE_LIST_ID}" style="display:flex;flex-direction:column;gap:0.4rem;max-height:10rem;overflow-y:auto;"></div>
        </div>
        <button id="${ADD_GRADIENT_NODE_BUTTON_ID}" class="button_button__aR6_e button_variantSecondary__hvM_F">Add node</button>
        <div class="game-menu_volumeContainer__aWb0Y" style="display:flex;flex-direction:column;gap:0.4rem;">
          <p class="game-menu_subHeader__Ul5Vl">Text colors</p>
          <div id="${TEXT_COLOR_NODE_LIST_ID}" style="display:flex;flex-direction:column;gap:0.4rem;"></div>
        </div>
        <div class="game-menu_divider__IhA4tL"></div>
        <button id="${RESET_DEFAULTS_BUTTON_ID}" class="button_button__aR6_e button_variantSecondary__hvM_F">Reset Defaults</button>
        <div class="game-menu_divider__IhA4t"></div>
        <button id="${RESUME_BUTTON_ID}" class="button_button__aR6_e button_variantPrimary__u3WzI">Resume Game</button>
      </div>
    </div>
  </div>
`

const onCustomizeStatusBarButtonClick = () => {
    // Close the settings overlay safely
    closeSettingsOverlay()

    const gameLayout = getInGameLayout()
    if (!gameLayout) return
    gameLayout.insertAdjacentHTML("beforeend", CUSTOMIZE_STATUS_BAR_SCREEN)

    const backgroundColorDiv = document.getElementById(BACKGROUND_COLOR_ID)
    const colorInput = backgroundColorDiv.querySelector(`.${COLOR_INPUT_CLASS}`)
    const colorTextInput = backgroundColorDiv.querySelector(`.${TEXT_INPUT_CLASS}`)

    colorInput.value = evaluateColor(backgroundColor)
    colorTextInput.value = backgroundColor

    colorInput.oninput = () => {
        backgroundColor = colorInput.value
        colorTextInput.value = backgroundColor
        updateStatusBarStyles()
    }
    colorTextInput.oninput = () => {
        backgroundColor = colorTextInput.value
        colorInput.value = evaluateColor(backgroundColor)
        updateStatusBarStyles()
    }

    const gradientNodeList = document.getElementById(GRADIENT_NODE_LIST_ID)
    document.getElementById(ADD_GRADIENT_NODE_BUTTON_ID).onclick = () => {
        gradientNodes.push(defaultNode())
        appendGradientNode(gradientNodeList)
    }

    document.getElementById(RESET_DEFAULTS_BUTTON_ID).onclick = () => {
        gradientNodes = DEFAULT_GRADIENT_NODES
        textColors = DEFAULT_TEXT_COLORS
        backgroundColor = DEFAULT_BACKGROUND_COLOR
        updateStatusBarStyles()
    }

    document.getElementById(RESUME_BUTTON_ID).onclick = () => {
        const statusBar = getStatusBar()
        if (statusBar) {
            statusBar.style.zIndex = null
            const canvas = getGameCanvas()
            if (canvas) canvas.appendChild(statusBar)
        }
        document.getElementById(CUSTOMIZE_STATUS_BAR_SCREEN_ID)?.remove()
    }

    // Move the status bar up so it's visible through the backdrop blur
    const statusBar = getStatusBar()
    if (statusBar) {
        statusBar.style.zIndex = "30"
        gameLayout.appendChild(statusBar)
    }

    for (const _ of gradientNodes) {
        appendGradientNode(gradientNodeList)
    }

    const textColorNodeList = document.getElementById(TEXT_COLOR_NODE_LIST_ID)
    appendTextColorNode(textColorNodeList, "Labels", 0)
    appendTextColorNode(textColorNodeList, "Values", 1)
}

// ─── Inject button ────────────────────────────────────────────────────────────

const injectCustomizeStatusBarButton = (settingsScreen) => {
    settingsScreen.insertAdjacentHTML("afterend", CUSTOMIZE_STATUS_BAR_BUTTON)
    document.getElementById(CUSTOMIZE_STATUS_BAR_BUTTON_ID).onclick = onCustomizeStatusBarButtonClick
}

// ─── Mutation observer ────────────────────────────────────────────────────────

const onMutations = () => {
    if (!pathMatches("game/.+")) return

    if (!document.getElementById(STYLE_ID)) {
        const style = document.createElement("style")
        style.id = STYLE_ID
        document.body.appendChild(style)
        updateStatusBarStyles()
    }

    // Use wildcard selector for the settings divider injection point
    const settingsDivider = q(
        '[class*="in-game_layout"] > [class*="game-menu_inGameMenuOverlay"] [class*="game-menu_settingsContainer"] > [class*="game-menu_divider"]'
    )

    if (settingsDivider && !document.getElementById(CUSTOMIZE_STATUS_BAR_BUTTON_ID)) {
        injectCustomizeStatusBarButton(settingsDivider)
    }
}

const observer = new MutationObserver(onMutations)
observer.observe(document.body, OBSERVER_CONFIG)