Custom Status Bar

Lets you customize the Geoguessr status bar via a GUI

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Custom Status Bar
// @description  Lets you customize the Geoguessr status bar via a GUI
// @version      1.3.0
// @license      MIT
// @author       zorby#1431
// @namespace    https://greasyfork.org/en/users/986787-zorby
// @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
}

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>
`

// Transforms an arbitrary css color value (like "rgb(0, 0, 0)" or "var(--some-color)") into a hex color
// (pretty hacky)
const evaluateColor = (color) => {
    const statusBar = document.querySelector(".game_status___YFni")
    statusBar.style.setProperty(TEMP_COLOR_VAR, color)
    const hexColor = window.getComputedStyle(statusBar).getPropertyValue(TEMP_COLOR_VAR)

    // Verify that it's indeed a hex color
    if (!hexColor.startsWith("#")) {
        console.log("could not evaluate color!")
        return "#000000"
    }

    // Strip the alpha channel
    if (hexColor.length > 7) {
        return hexColor.substr(0, 7)
    }

    // Expand the color, if it's in compressed form
    if (hexColor.length == 4) {
        return "#" + hexColor.substr(1).split("").map(char => char + char).join("")
    }

    return hexColor
}

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}`)

    console.log(evaluateColor(textColors[index]))
    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 generateGradientString = () => {
    return `linear-gradient(180deg, ${
        gradientNodes.map((node) => `${node.color} ${node.percentage}%`).join(",")
    })`
}

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

    style.innerHTML = `
      .slanted-wrapper_variantPurple__AujW3 {
        --variant-background-color: ${generateGradientString()}, ${backgroundColor};
      }

      .slanted-wrapper_variantPurple__AujW3 .status_label__mZ7Ok {
        color: ${textColors[0]}
      }

      .slanted-wrapper_variantPurple__AujW3 .status_value__w_Nh0 {
        color: ${textColors[1]}
      }
    `

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

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 = () => {
        percentageInput.value = gradientNodes[getIndex(gradientNode)].percentage
        colorInput.value = evaluateColor(gradientNodes[getIndex(gradientNode)].color)
        colorTextInput.value = gradientNodes[getIndex(gradientNode)].color
    }

    gradientNode.updateInputs = updateInputs

    updateInputs()

    percentageInput.oninput = () => {
        gradientNodes[getIndex(gradientNode)].percentage = percentageInput.value
        updateStatusBarStyles()
    }

    colorInput.oninput = () => {
        gradientNodes[getIndex(gradientNode)].color = colorInput.value
        colorTextInput.value = gradientNodes[getIndex(gradientNode)].color
        updateStatusBarStyles()
    }

    colorTextInput.oninput = () => {
        gradientNodes[getIndex(gradientNode)].color = colorTextInput.value
        colorInput.value = evaluateColor(gradientNodes[getIndex(gradientNode)].color)
        updateStatusBarStyles()
    }

    deleteButton.onclick = () => {
        gradientNodes.splice(getIndex(gradientNode), 1)
        gradientNode.remove()
        updateStatusBarStyles()
    }

    upButton.onclick = () => {
        let temp = gradientNodes[getIndex(gradientNode)].color
        gradientNodes[getIndex(gradientNode)].color = gradientNodes[getIndex(gradientNode) - 1].color
        gradientNodes[getIndex(gradientNode) - 1].color = temp
        parent.children[getIndex(gradientNode) - 1].updateInputs()
        updateInputs()
        updateStatusBarStyles()
    }

    downButton.onclick = () => {
        let temp = gradientNodes[getIndex(gradientNode)].color
        gradientNodes[getIndex(gradientNode)].color = gradientNodes[getIndex(gradientNode) + 1].color
        gradientNodes[getIndex(gradientNode) + 1].color = temp
        parent.children[getIndex(gradientNode) + 1].updateInputs()
        updateInputs()
        updateStatusBarStyles()
    }
}

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
    document.querySelector(".game-menu_inGameMenuOverlay__XWQpg .buttons_buttons__3yvvA .button_variantPrimary__u3WzI").click()

    const gameLayout = document.querySelector(".in-game_layout__kqBbg")
    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 addGradientNodeButton = document.getElementById(ADD_GRADIENT_NODE_BUTTON_ID)
    addGradientNodeButton.onclick = () => {
        gradientNodes.push(defaultNode())
        appendGradientNode(gradientNodeList)
    }

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

    const resumeButton = document.getElementById(RESUME_BUTTON_ID)
    resumeButton.onclick = () => {
        const statusBar = document.querySelector(".game_status___YFni")
        statusBar.style.zIndex = null
        document.querySelector(".game_canvas__twTKG").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 = document.querySelector(".game_status___YFni")
    statusBar.style.zIndex = "30"
    document.querySelector(".in-game_layout__kqBbg").appendChild(statusBar)

    const gradientNodeList = document.getElementById(GRADIENT_NODE_LIST_ID)
    for (const i in gradientNodes) {
        appendGradientNode(gradientNodeList)
    }

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

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

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()
    }

    const settingsScreen = document.querySelector(".in-game_layout__kqBbg > .game-menu_inGameMenuOverlay__XWQpg .game-menu_settingsContainer__NeJu2 > .game-menu_divider__IhA4t")

    if (settingsScreen && !document.querySelector(`#${CUSTOMIZE_STATUS_BAR_BUTTON_ID}`)) {
        injectCustomizeStatusBarButton(settingsScreen)
    }
}

const observer = new MutationObserver(onMutations)

observer.observe(document.body, OBSERVER_CONFIG)