Custom Status Bar

Lets you customize the status bar

目前为 2023-05-22 提交的版本。查看 最新版本

// ==UserScript==
// @name         Custom Status Bar
// @description  Lets you customize the status bar
// @version      1.1.0
// @license      MIT
// @author       zorby#1431
// @namespace    https://gf.qytechs.cn/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 defaultNode = () => ({
    color: "#000000",
    percentage: 100
})

const DEFAULT_GRADIENT_NODES = [
    {
        color: "#000000",
        percentage: 0
    },
    {
        color: "#000000",
        percentage: 100
    }
]

const DEFAULT_TEXT_COLORS = [
    "#b0b0b0",
    "#ffffff"
]

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

if (configString) {
    const config = JSON.parse(configString)

    gradientNodes = config.gradient
    textColors = config.textColors
}

const CUSTOMIZE_STATUS_BAR_BUTTON = `
  <button id="${CUSTOMIZE_STATUS_BAR_BUTTON_ID}" class="button_button__CnARx button_variantSecondary__lSxsR">
    Customize status bar
  </button>
  <div class="game-menu_divider__f2BbL"></div>
`

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}">v</button>
      <button class="${STANDARD_BUTTON_CLASS} ${UP_BUTTON_CLASS}" style="margin-left: 1rem;">^</button>
      <input type="color" class="${COLOR_INPUT_CLASS}" ></input>
      <input type="text" class="${TEXT_INPUT_CLASS}" style="width: 4.5rem;" pattern="[0-9a-fA-F]{6}" required></input>
      <div style="font-weight: 700;">#</div>
    </div>
  </div>
`

const appendTextColorNode = (parent, label, index) => {
    const html = `
      <div 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">
          <input type="color" class="${COLOR_INPUT_CLASS}" ></input>
          <input type="text" class="${TEXT_INPUT_CLASS}" style="width: 4.5rem;" pattern="[0-9a-fA-F]{6}" required></input>
          <div style="font-weight: 700;">#</div>
        </div>
      </div>
    `

    parent.insertAdjacentHTML("beforeend", html)
    const textColorNode = parent.lastElementChild

    const colorInput = textColorNode.querySelector(`.${COLOR_INPUT_CLASS}`)
    const colorTextInput = textColorNode.querySelector(`.${TEXT_INPUT_CLASS}`)

    colorInput.value = textColors[index]
    colorTextInput.value = textColors[index].substring(1)

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

    colorTextInput.oninput = () => {
        textColors[index] = "#" + colorTextInput.value
        colorInput.value = 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__95_Ub {
        --variant-background-color: ${generateGradientString()};
      }

      .slanted-wrapper_variantPurple__95_Ub .status_label__SNHKT {
        color: ${textColors[0]}
      }

      .slanted-wrapper_variantPurple__95_Ub .status_value__xZMNY {
        color: ${textColors[1]}
      }
    `

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

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 = gradientNodes[getIndex(gradientNode)].color
        colorTextInput.value = gradientNodes[getIndex(gradientNode)].color.substring(1)
    }

    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.substring(1)
        updateStatusBarStyles()
    }

    colorTextInput.oninput = () => {
        gradientNodes[getIndex(gradientNode)].color = "#" + colorTextInput.value
        colorInput.value = 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_gameMenu__8ON8f">
    <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_innerContainer__jEQ9E">
      <p class="game-menu_header__KeQ7F">Customize Status Bar</p>
      <div class="game-menu_volumeContainer__dRQtK" style="display: flex; flex-direction: column; gap: 0.4rem;">
        <p class="game-menu_subHeader___oVKH">Gradient</p>
        <div id="${GRADIENT_NODE_LIST_ID}" style="display: flex; flex-direction: column; gap: 0.4rem; max-height: 10rem; overflow-y: auto;"></div>
        <button id="${ADD_GRADIENT_NODE_BUTTON_ID}" class="button_button__CnARx button_variantSecondary__lSxsR">Add node</button>
      </div>
      <div class="game-menu_volumeContainer__dRQtK" style="display: flex; flex-direction: column; gap: 0.4rem;">
        <p class="game-menu_subHeader___oVKH">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__f2BbL"></div>
      <button id="${RESUME_BUTTON_ID}" class="button_button__CnARx button_variantPrimary__xc8Hp">Resume Game</button>
    </div>
  </div>
`


const onCustomizeStatusBarButtonClick = () => {
    document.querySelector(".game-menu_gameMenu__8ON8f .button_variantPrimary__xc8Hp").click()

    const gameLayout = document.querySelector(".game-layout")
    gameLayout.insertAdjacentHTML("beforeend", CUSTOMIZE_STATUS_BAR_SCREEN)

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

    const resumeButton = document.getElementById(RESUME_BUTTON_ID)
    resumeButton.onclick = () => {
        document.querySelector(".game-layout__status").style.zIndex = null
        document.getElementById(CUSTOMIZE_STATUS_BAR_SCREEN_ID).remove()
    }

    document.querySelector(".game-layout__status").style.zIndex = "30"

    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__7zzGJ > .game-menu_gameMenu__8ON8f .game-menu_divider__f2BbL")

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

const observer = new MutationObserver(onMutations)

observer.observe(document.body, OBSERVER_CONFIG)

QingJ © 2025

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