Custom Status Bar

Lets you customize the status bar

当前为 2023-05-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Custom Status Bar
// @description  Lets you customize the status bar
// @version      1.0.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 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 = `
  <div class="game-menu_divider__f2BbL"></div>
  <button id="${CUSTOMIZE_STATUS_BAR_BUTTON_ID}" class="button_button__CnARx button_variantSecondary__lSxsR">
    Customize status bar
  </button>
`

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(".version4-in-game_layout__vjdlO > .game-menu_gameMenu__8ON8f .game-menu_optionsContainer__gLhxR")

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

const observer = new MutationObserver(onMutations)

observer.observe(document.body, OBSERVER_CONFIG)