Custom Status Bar

Lets you customize the Geoguessr status bar via a GUI

  1. // ==UserScript==
  2. // @name Custom Status Bar
  3. // @description Lets you customize the Geoguessr status bar via a GUI
  4. // @version 1.3.0
  5. // @license MIT
  6. // @author zorby#1431
  7. // @namespace https://greasyfork.org/en/users/986787-zorby
  8. // @match https://www.geoguessr.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
  10. // ==/UserScript==
  11.  
  12. function pathMatches(path) {
  13. return location.pathname.match(new RegExp(`^/(?:[^/]+/)?${path}$`))
  14. }
  15.  
  16. function getIndex(element) {
  17. if (!element) return -1
  18.  
  19. let i = 0
  20. while (element = element.previousElementSibling) {
  21. i++
  22. }
  23.  
  24. return i
  25. }
  26.  
  27. const OBSERVER_CONFIG = {
  28. characterDataOldValue: false,
  29. subtree: true,
  30. childList: true,
  31. characterData: false
  32. }
  33.  
  34. const SCRIPT_PREFIX = "csb__"
  35. const CONFIG_KEY = SCRIPT_PREFIX + "config"
  36. const STYLE_ID = SCRIPT_PREFIX + "style"
  37. const PERCENTAGE_INPUT_CLASS = SCRIPT_PREFIX + "percentage-input"
  38. const COLOR_INPUT_CLASS = SCRIPT_PREFIX + "color-input"
  39. const TEXT_INPUT_CLASS = SCRIPT_PREFIX + "text-input"
  40. const DELETE_BUTTON_CLASS = SCRIPT_PREFIX + "delete-button"
  41. const STANDARD_BUTTON_CLASS = SCRIPT_PREFIX + "standard-button"
  42. const DOWN_BUTTON_CLASS = SCRIPT_PREFIX + "down-button"
  43. const UP_BUTTON_CLASS = SCRIPT_PREFIX + "up-button"
  44. const CUSTOMIZE_STATUS_BAR_BUTTON_ID = SCRIPT_PREFIX + "customize-status-bar-button"
  45. const ADD_GRADIENT_NODE_BUTTON_ID = SCRIPT_PREFIX + "add-gradient-node-button"
  46. const CUSTOMIZE_STATUS_BAR_SCREEN_ID = SCRIPT_PREFIX + "customize-status-bar-screen"
  47. const GRADIENT_NODE_LIST_ID = SCRIPT_PREFIX + "gradient-node-list"
  48. const TEXT_COLOR_NODE_LIST_ID = SCRIPT_PREFIX + "text-color-node-list"
  49. const RESUME_BUTTON_ID = SCRIPT_PREFIX + "resume-button"
  50. const BACKGROUND_COLOR_ID = SCRIPT_PREFIX + "background-color"
  51. const TEMP_COLOR_VAR = "--" + SCRIPT_PREFIX + "temp-color"
  52. const RESET_DEFAULTS_BUTTON_ID = SCRIPT_PREFIX + "reset-defaults-button"
  53.  
  54. const defaultNode = () => ({
  55. color: "#000000",
  56. percentage: 100
  57. })
  58.  
  59. const DEFAULT_BACKGROUND_COLOR = "var(--ds-color-purple-80)"
  60.  
  61. const DEFAULT_GRADIENT_NODES = [
  62. {
  63. color: "#a19bd999",
  64. percentage: 0
  65. },
  66. {
  67. color: "#00000000",
  68. percentage: 50
  69. },
  70. {
  71. color: "#00000000",
  72. percentage: 50
  73. }
  74. ]
  75.  
  76. const DEFAULT_TEXT_COLORS = [
  77. "var(--ds-color-purple-20)",
  78. "var(--ds-color-white)"
  79. ]
  80.  
  81. const configString = localStorage.getItem(CONFIG_KEY)
  82. let gradientNodes = DEFAULT_GRADIENT_NODES
  83. let textColors = DEFAULT_TEXT_COLORS
  84. let backgroundColor = DEFAULT_BACKGROUND_COLOR
  85.  
  86. if (configString) {
  87. const config = JSON.parse(configString)
  88.  
  89. gradientNodes = config.gradient
  90. textColors = config.textColors
  91. backgroundColor = config.backgroundColor
  92. }
  93.  
  94. const CUSTOMIZE_STATUS_BAR_BUTTON = `
  95. <button id="${CUSTOMIZE_STATUS_BAR_BUTTON_ID}" class="button_button__aR6_e button_variantSecondary__hvM_F">
  96. Customize status bar
  97. </button>
  98. <div class="game-menu_divider__IhA4t"></div>
  99. `
  100.  
  101. const COLOR_WIDGET = `
  102. <input type="color" class="${COLOR_INPUT_CLASS}" ></input>
  103. <input type="text" class="${TEXT_INPUT_CLASS}" style="width: 5.5rem;" required></input>
  104. `
  105.  
  106. const GRADIENT_NODE = `
  107. <div style="
  108. display: grid;
  109. grid-template-columns: 1fr 1fr;
  110. grid-template-rows: auto;
  111. ">
  112. <div class="grid-element">
  113. <input type="number" class="${PERCENTAGE_INPUT_CLASS}" min="0" max="100" step="any" required></input>
  114. <div style="font-weight: 700;">%</div>
  115. </div>
  116. <div class="grid-element" style="flex-direction: row-reverse">
  117. <button class="${DELETE_BUTTON_CLASS}">X</button>
  118. <button class="${STANDARD_BUTTON_CLASS} ${DOWN_BUTTON_CLASS}">↓</button>
  119. <button class="${STANDARD_BUTTON_CLASS} ${UP_BUTTON_CLASS}" style="margin-left: 1rem;">↑</button>
  120. ${COLOR_WIDGET}
  121. </div>
  122. </div>
  123. `
  124.  
  125. const colorNode = (label, id) => `
  126. <div id="${id}" style="
  127. display: grid;
  128. grid-template-columns: 1fr 1fr;
  129. grid-template-rows: auto;
  130. ">
  131. <div class="grid-element">
  132. <div style="font-weight: 700;">${label}</div>
  133. </div>
  134. <div class="grid-element" style="flex-direction: row-reverse">
  135. ${COLOR_WIDGET}
  136. </div>
  137. </div>
  138. `
  139.  
  140. // Transforms an arbitrary css color value (like "rgb(0, 0, 0)" or "var(--some-color)") into a hex color
  141. // (pretty hacky)
  142. const evaluateColor = (color) => {
  143. const statusBar = document.querySelector(".game_status___YFni")
  144. statusBar.style.setProperty(TEMP_COLOR_VAR, color)
  145. const hexColor = window.getComputedStyle(statusBar).getPropertyValue(TEMP_COLOR_VAR)
  146.  
  147. // Verify that it's indeed a hex color
  148. if (!hexColor.startsWith("#")) {
  149. console.log("could not evaluate color!")
  150. return "#000000"
  151. }
  152.  
  153. // Strip the alpha channel
  154. if (hexColor.length > 7) {
  155. return hexColor.substr(0, 7)
  156. }
  157.  
  158. // Expand the color, if it's in compressed form
  159. if (hexColor.length == 4) {
  160. return "#" + hexColor.substr(1).split("").map(char => char + char).join("")
  161. }
  162.  
  163. return hexColor
  164. }
  165.  
  166. const appendTextColorNode = (parent, label, index) => {
  167. parent.insertAdjacentHTML("beforeend", colorNode(label, ""))
  168. const textColorNode = parent.lastElementChild
  169.  
  170. const colorInput = textColorNode.querySelector(`.${COLOR_INPUT_CLASS}`)
  171. const colorTextInput = textColorNode.querySelector(`.${TEXT_INPUT_CLASS}`)
  172.  
  173. console.log(evaluateColor(textColors[index]))
  174. colorInput.value = evaluateColor(textColors[index])
  175. colorTextInput.value = textColors[index]
  176.  
  177. colorInput.oninput = () => {
  178. textColors[index] = colorInput.value
  179. colorTextInput.value = textColors[index]
  180. updateStatusBarStyles()
  181. }
  182.  
  183. colorTextInput.oninput = () => {
  184. textColors[index] = colorTextInput.value
  185. colorInput.value = evaluateColor(textColors[index])
  186. updateStatusBarStyles()
  187. }
  188. }
  189.  
  190. const generateGradientString = () => {
  191. return `linear-gradient(180deg, ${
  192. gradientNodes.map((node) => `${node.color} ${node.percentage}%`).join(",")
  193. })`
  194. }
  195.  
  196. const updateStatusBarStyles = () => {
  197. const style = document.getElementById(STYLE_ID)
  198.  
  199. style.innerHTML = `
  200. .slanted-wrapper_variantPurple__AujW3 {
  201. --variant-background-color: ${generateGradientString()}, ${backgroundColor};
  202. }
  203.  
  204. .slanted-wrapper_variantPurple__AujW3 .status_label__mZ7Ok {
  205. color: ${textColors[0]}
  206. }
  207.  
  208. .slanted-wrapper_variantPurple__AujW3 .status_value__w_Nh0 {
  209. color: ${textColors[1]}
  210. }
  211. `
  212.  
  213. localStorage.setItem(CONFIG_KEY, JSON.stringify({
  214. "gradient": gradientNodes,
  215. "textColors": textColors,
  216. "backgroundColor": backgroundColor
  217. }))
  218. }
  219.  
  220. const appendGradientNode = (parent) => {
  221. parent.insertAdjacentHTML("beforeend", GRADIENT_NODE)
  222. const gradientNode = parent.lastElementChild
  223.  
  224. const percentageInput = gradientNode.querySelector(`.${PERCENTAGE_INPUT_CLASS}`)
  225. const colorInput = gradientNode.querySelector(`.${COLOR_INPUT_CLASS}`)
  226. const colorTextInput = gradientNode.querySelector(`.${TEXT_INPUT_CLASS}`)
  227. const deleteButton = gradientNode.querySelector(`.${DELETE_BUTTON_CLASS}`)
  228. const upButton = gradientNode.querySelector(`.${UP_BUTTON_CLASS}`)
  229. const downButton = gradientNode.querySelector(`.${DOWN_BUTTON_CLASS}`)
  230.  
  231. const updateInputs = () => {
  232. percentageInput.value = gradientNodes[getIndex(gradientNode)].percentage
  233. colorInput.value = evaluateColor(gradientNodes[getIndex(gradientNode)].color)
  234. colorTextInput.value = gradientNodes[getIndex(gradientNode)].color
  235. }
  236.  
  237. gradientNode.updateInputs = updateInputs
  238.  
  239. updateInputs()
  240.  
  241. percentageInput.oninput = () => {
  242. gradientNodes[getIndex(gradientNode)].percentage = percentageInput.value
  243. updateStatusBarStyles()
  244. }
  245.  
  246. colorInput.oninput = () => {
  247. gradientNodes[getIndex(gradientNode)].color = colorInput.value
  248. colorTextInput.value = gradientNodes[getIndex(gradientNode)].color
  249. updateStatusBarStyles()
  250. }
  251.  
  252. colorTextInput.oninput = () => {
  253. gradientNodes[getIndex(gradientNode)].color = colorTextInput.value
  254. colorInput.value = evaluateColor(gradientNodes[getIndex(gradientNode)].color)
  255. updateStatusBarStyles()
  256. }
  257.  
  258. deleteButton.onclick = () => {
  259. gradientNodes.splice(getIndex(gradientNode), 1)
  260. gradientNode.remove()
  261. updateStatusBarStyles()
  262. }
  263.  
  264. upButton.onclick = () => {
  265. let temp = gradientNodes[getIndex(gradientNode)].color
  266. gradientNodes[getIndex(gradientNode)].color = gradientNodes[getIndex(gradientNode) - 1].color
  267. gradientNodes[getIndex(gradientNode) - 1].color = temp
  268. parent.children[getIndex(gradientNode) - 1].updateInputs()
  269. updateInputs()
  270. updateStatusBarStyles()
  271. }
  272.  
  273. downButton.onclick = () => {
  274. let temp = gradientNodes[getIndex(gradientNode)].color
  275. gradientNodes[getIndex(gradientNode)].color = gradientNodes[getIndex(gradientNode) + 1].color
  276. gradientNodes[getIndex(gradientNode) + 1].color = temp
  277. parent.children[getIndex(gradientNode) + 1].updateInputs()
  278. updateInputs()
  279. updateStatusBarStyles()
  280. }
  281. }
  282.  
  283. const CUSTOMIZE_STATUS_BAR_SCREEN = `
  284. <div id="${CUSTOMIZE_STATUS_BAR_SCREEN_ID}" class="game-menu_inGameMenuOverlay__XWQpg">
  285. <style>
  286. .${PERCENTAGE_INPUT_CLASS}, .${COLOR_INPUT_CLASS},
  287. .${TEXT_INPUT_CLASS}, .${DELETE_BUTTON_CLASS}, .${STANDARD_BUTTON_CLASS} {
  288. background: rgba(255,255,255,0.1);
  289. color: white;
  290. border: none;
  291. border-radius: 5px;
  292. font-family: var(--default-font);
  293. font-size: var(--font-size-14);
  294. padding: 0.5rem;
  295. }
  296.  
  297. .${PERCENTAGE_INPUT_CLASS}, .${COLOR_INPUT_CLASS} {
  298. width: 3rem;
  299. }
  300.  
  301. .${PERCENTAGE_INPUT_CLASS}, .${TEXT_INPUT_CLASS} {
  302. text-align: center;
  303. -moz-appearance: textfield;
  304. }
  305.  
  306. .${PERCENTAGE_INPUT_CLASS}::-webkit-outer-spin-button,
  307. .${PERCENTAGE_INPUT_CLASS}::-webkit-inner-spin-button {
  308. -webkit-appearance: none;
  309. margin: 0;
  310. }
  311.  
  312. .${COLOR_INPUT_CLASS} {
  313. height: 100%;
  314. padding: 0.25rem;
  315. }
  316.  
  317. .${COLOR_INPUT_CLASS}::-webkit-color-swatch-wrapper {
  318. padding: 0;
  319. }
  320.  
  321. .${COLOR_INPUT_CLASS}::-webkit-color-swatch {
  322. border: none;
  323. border-radius: 5px;
  324. }
  325.  
  326. .${TEXT_INPUT_CLASS}:invalid, .${PERCENTAGE_INPUT_CLASS}:invalid {
  327. background: rgba(209, 27, 38, 0.1);
  328. color: var(--color-red-60);
  329. }
  330.  
  331. .${DELETE_BUTTON_CLASS}, .${STANDARD_BUTTON_CLASS} {
  332. width: 2rem;
  333. user-select: none;
  334. }
  335.  
  336. .${DELETE_BUTTON_CLASS} {
  337. background: rgba(209, 27, 38, 0.1);
  338. }
  339.  
  340. .${DELETE_BUTTON_CLASS}:hover, .${STANDARD_BUTTON_CLASS}:hover, .${COLOR_INPUT_CLASS}:hover {
  341. cursor: pointer;
  342. }
  343.  
  344. .${DELETE_BUTTON_CLASS}:hover {
  345. background: var(--color-red-60);
  346. }
  347.  
  348. .${STANDARD_BUTTON_CLASS}:hover {
  349. background: var(--color-grey-70);
  350. }
  351.  
  352. #${CUSTOMIZE_STATUS_BAR_SCREEN_ID} .grid-element {
  353. display: flex;
  354. align-items: center;
  355. gap: 0.5rem;
  356. }
  357. </style>
  358. <div class="game-menu_inGameMenuContentWrapper__mGVx4">
  359. <div class="game-menu_innerContainer__Kxqz_">
  360. <p class="game-menu_header__Hs03p">Customize Status Bar</p>
  361. <div class="game-menu_volumeContainer__aWb0Y" style="display: flex; flex-direction: column; gap: 0.4rem;">
  362. <p class="game-menu_subHeader__Ul5Vl">Background</p>
  363. ${colorNode("Color", BACKGROUND_COLOR_ID)}
  364. </div>
  365. <div class="game-menu_volumeContainer__aWb0Y" style="display: flex; flex-direction: column; gap: 0.4rem;">
  366. <p class="game-menu_subHeader__Ul5Vl">Gradient</p>
  367. <div id="${GRADIENT_NODE_LIST_ID}" style="display: flex; flex-direction: column; gap: 0.4rem; max-height: 10rem; overflow-y: auto;"></div>
  368. </div>
  369. <button id="${ADD_GRADIENT_NODE_BUTTON_ID}" class="button_button__aR6_e button_variantSecondary__hvM_F">Add node</button>
  370. <div class="game-menu_volumeContainer__aWb0Y" style="display: flex; flex-direction: column; gap: 0.4rem;">
  371. <p class="game-menu_subHeader__Ul5Vl">Text colors</p>
  372. <div id="${TEXT_COLOR_NODE_LIST_ID}" style="display: flex; flex-direction: column; gap: 0.4rem;"></div>
  373. </div>
  374. <div class="game-menu_divider__IhA4tL"></div>
  375. <button id="${RESET_DEFAULTS_BUTTON_ID}" class="button_button__aR6_e button_variantSecondary__hvM_F">Reset Defaults</button>
  376. <div class="game-menu_divider__IhA4t"></div>
  377. <button id="${RESUME_BUTTON_ID}" class="button_button__aR6_e button_variantPrimary__u3WzI">Resume Game</button>
  378. </div>
  379. </div>
  380. </div>
  381. `
  382.  
  383.  
  384. const onCustomizeStatusBarButtonClick = () => {
  385. // Close the settings overlay
  386. document.querySelector(".game-menu_inGameMenuOverlay__XWQpg .buttons_buttons__3yvvA .button_variantPrimary__u3WzI").click()
  387.  
  388. const gameLayout = document.querySelector(".in-game_layout__kqBbg")
  389. gameLayout.insertAdjacentHTML("beforeend", CUSTOMIZE_STATUS_BAR_SCREEN)
  390.  
  391. const backgroundColorDiv = document.getElementById(BACKGROUND_COLOR_ID)
  392.  
  393. const colorInput = backgroundColorDiv.querySelector(`.${COLOR_INPUT_CLASS}`)
  394. const colorTextInput = backgroundColorDiv.querySelector(`.${TEXT_INPUT_CLASS}`)
  395.  
  396. colorInput.value = evaluateColor(backgroundColor)
  397. colorTextInput.value = backgroundColor
  398.  
  399. colorInput.oninput = () => {
  400. backgroundColor = colorInput.value
  401. colorTextInput.value = backgroundColor
  402. updateStatusBarStyles()
  403. }
  404.  
  405. colorTextInput.oninput = () => {
  406. backgroundColor = colorTextInput.value
  407. colorInput.value = evaluateColor(backgroundColor)
  408. updateStatusBarStyles()
  409. }
  410.  
  411. const addGradientNodeButton = document.getElementById(ADD_GRADIENT_NODE_BUTTON_ID)
  412. addGradientNodeButton.onclick = () => {
  413. gradientNodes.push(defaultNode())
  414. appendGradientNode(gradientNodeList)
  415. }
  416.  
  417. const resetButton = document.getElementById(RESET_DEFAULTS_BUTTON_ID)
  418. resetButton.onclick = () => {
  419. gradientNodes = DEFAULT_GRADIENT_NODES
  420. textColors = DEFAULT_TEXT_COLORS
  421. backgroundColor = DEFAULT_BACKGROUND_COLOR
  422. updateStatusBarStyles()
  423. }
  424.  
  425. const resumeButton = document.getElementById(RESUME_BUTTON_ID)
  426. resumeButton.onclick = () => {
  427. const statusBar = document.querySelector(".game_status___YFni")
  428. statusBar.style.zIndex = null
  429. document.querySelector(".game_canvas__twTKG").appendChild(statusBar)
  430. document.getElementById(CUSTOMIZE_STATUS_BAR_SCREEN_ID).remove()
  431. }
  432.  
  433. // Move the status bar up so it's visible through the backdrop blur
  434. const statusBar = document.querySelector(".game_status___YFni")
  435. statusBar.style.zIndex = "30"
  436. document.querySelector(".in-game_layout__kqBbg").appendChild(statusBar)
  437.  
  438. const gradientNodeList = document.getElementById(GRADIENT_NODE_LIST_ID)
  439. for (const i in gradientNodes) {
  440. appendGradientNode(gradientNodeList)
  441. }
  442.  
  443. const textColorNodeList = document.getElementById(TEXT_COLOR_NODE_LIST_ID)
  444. appendTextColorNode(textColorNodeList, "Labels", 0)
  445. appendTextColorNode(textColorNodeList, "Values", 1)
  446. }
  447.  
  448. const injectCustomizeStatusBarButton = (settingsScreen) => {
  449. settingsScreen.insertAdjacentHTML("afterend", CUSTOMIZE_STATUS_BAR_BUTTON)
  450. document.getElementById(CUSTOMIZE_STATUS_BAR_BUTTON_ID).onclick = onCustomizeStatusBarButtonClick
  451. }
  452.  
  453. const onMutations = () => {
  454. if (!pathMatches("game/.+")) return
  455.  
  456. if (!document.getElementById(STYLE_ID)) {
  457. const style = document.createElement("style")
  458. style.id = STYLE_ID
  459. document.body.appendChild(style)
  460. updateStatusBarStyles()
  461. }
  462.  
  463. const settingsScreen = document.querySelector(".in-game_layout__kqBbg > .game-menu_inGameMenuOverlay__XWQpg .game-menu_settingsContainer__NeJu2 > .game-menu_divider__IhA4t")
  464.  
  465. if (settingsScreen && !document.querySelector(`#${CUSTOMIZE_STATUS_BAR_BUTTON_ID}`)) {
  466. injectCustomizeStatusBarButton(settingsScreen)
  467. }
  468. }
  469.  
  470. const observer = new MutationObserver(onMutations)
  471.  
  472. observer.observe(document.body, OBSERVER_CONFIG)