GeoGuessr Background Replacer

Replaces the background of the geoguessr pages with your own images

当前为 2023-06-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GeoGuessr Background Replacer
  3. // @description Replaces the background of the geoguessr pages with your own images
  4. // @version 2.0
  5. // @author Tyow#3742
  6. // @match *://*.geoguessr.com/*
  7. // @license MIT
  8. // @require https://unpkg.com/@popperjs/core@2.11.5/dist/umd/popper.min.js
  9. // @namespace https://greasyfork.org/users/1011193
  10. // @grant GM_addStyle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @require https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151668
  14. // ==/UserScript==
  15.  
  16. // Some code for popup adapted from blink script: https://greasyfork.org/en/scripts/438579-geoguessr-blink-mode
  17.  
  18. /* ############################################################################### */
  19. /* ##### DON'T MODIFY ANYTHING BELOW HERE UNLESS YOU KNOW WHAT YOU ARE DOING ##### */
  20. /* ############################################################################### */
  21.  
  22. const guiHTMLHeader = `
  23. <div id="backgroundReplacerPopupWrapper">
  24. <div id="backgroundReplacerSearchWrapper">
  25. <div id="backgroundReplacerSlantedRoot">
  26. <div id="backgroundReplacerSlantedStart"></div>
  27. <div id="backgroundReplacerInputWrapper">
  28. <div id="backgroundReplacerPopup" style="background: rgba(26, 26, 46, 0.9); padding: 15px; border-radius: 10px; max-height: 80vh; overflow-y: auto; width: 28em">
  29. <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
  30. <span id="backgroundReplacerLabel1" style="margin: 0; padding-right: 6px;">Add Home Page image</span>
  31. <input type="url" id="homepageInput" name="homepage" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
  32. </div>
  33. <span>Home Page Images:</span>
  34. <div id="homePageImages" style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px; flex-direction: column"></div>
  35. <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
  36. <span id="backgroundReplacerLabel2" style="margin: 0; padding-right: 6px;">Add Other Page Image</span>
  37. <input type="url" id="otherpagesInput" name="otherpages" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
  38. </div>
  39. <span>Other Pages Images:</span>
  40. <div id="otherPagesImages" style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px; flex-direction: column"></div>
  41. </div>
  42. <button style="width: 59.19px" id="backgroundReplacerToggle"><picture id="backgroundReplacerTogglePicture" style="justify-content: center"><img src="https://www.svgrepo.com/show/342899/wallpaper.svg" style="width: 15px; filter: brightness(0) invert(1); opacity: 60%;"></picture></button>
  43. </div>
  44. <div id="backgroundReplacerSlantedEnd"></div>
  45. </div>
  46. </div>
  47. </div>
  48. `
  49. let homePageImageList = GM_getValue("homepageImages");
  50. let otherImages = GM_getValue("otherImages");
  51.  
  52. // Defaults
  53. if (homePageImageList == undefined) {
  54. homePageImageList = [
  55. "https://cdn.wallpapersafari.com/6/80/9ZbpYo.jpg",
  56. "https://cdn.wallpapersafari.com/25/72/dtkc16.jpg",
  57. "https://i.imgur.com/l9K9IOq.jpg",
  58. ];
  59. GM_setValue("homepageImages", homePageImageList);
  60. }
  61. if (otherImages == undefined) {
  62. otherImages = [
  63. "https://imgur.com/eK23SeH.jpg",
  64. "https://i.imgur.com/l9K9IOq.jpg"
  65. ];
  66. GM_setValue("otherImages", otherImages);
  67. }
  68.  
  69. let hide = false;
  70. let styles = GM_getValue("backgroundReplacerStyles");
  71. if (!styles) {
  72. hide = true;
  73. styles = {};
  74. }
  75.  
  76. let homePageImgURL;
  77.  
  78.  
  79.  
  80. const setHomePageImg = () => {
  81. if(homePageImageList.length) {
  82. homePageImgURL = homePageImageList[Math.floor((Math.random()*homePageImageList.length))];
  83. } else {
  84. homePageImgURL = "";
  85. }
  86. }
  87.  
  88. setHomePageImg();
  89.  
  90. let otherPagesImgURL;
  91.  
  92. const setOtherImg = () => {
  93. if(otherImages.length) {
  94. otherPagesImgURL = otherImages[Math.floor((Math.random()*otherImages.length))];
  95. } else {
  96. otherPagesImgURL = "";
  97. }
  98. }
  99.  
  100. setOtherImg();
  101.  
  102. let css = `.customBackground { bottom: 0;
  103. display: block;
  104. height: 100%;
  105. object-fit: cover;
  106. pointer-events: none;
  107. position: fixed;
  108. right: 0;
  109. transition: .2s ease-in-out;
  110. width: 100%;
  111. }
  112. .zindex {
  113. z-index: -1;
  114. }
  115. .deleteIcon {
  116. width: 25px;
  117. filter: brightness(0) invert(1);
  118. opacity: 60%;
  119. }
  120. .backgroundImage {
  121. width: 20em;
  122. }
  123. .deleteButton {
  124. width: 59.19px;
  125. margin-bottom: 8em;
  126. }
  127. .backgroundImageWrapper {
  128. display: flex;
  129. padding: .5em;
  130. }
  131. .deleteIconPicture {
  132. justifyContent:center;
  133. }
  134.  
  135. `;
  136. GM_addStyle(css);
  137.  
  138.  
  139. const showPopup = (showButton, popup) => {
  140. popup.style.display = 'block';
  141. Popper.createPopper(showButton, popup, {
  142. placement: 'bottom',
  143. modifiers: [
  144. {
  145. name: 'offset',
  146. options: {
  147. offset: [0, 10],
  148. },
  149. },
  150. ],
  151. });
  152. }
  153.  
  154. const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  155.  
  156. const iterativeSetTimeout = async (func, delay, cond) => {
  157. while (!cond()) {
  158. await delay(delay);
  159. await func();
  160. delay *= 2;
  161. }
  162. };
  163.  
  164. // Caching system for styles
  165. // Basically, we have a browser stored styles object,
  166. // which contains the most recent classNames found by scanStyles()
  167. // This is what the script will immediately use upon loading,
  168. // so that there's no pause in delivering the UI to the user
  169. // But the script will also fire off this function
  170. // which will use the above iterativeSetTimeout function to call scanStyles
  171. // This is so there aren't a thousand calls in quick succession.
  172. // Once all the classNames we're looking for are found,
  173. // it will update the local storage and the ui with the (possibly) new classnames
  174. const uploadDownloadStyles = async () => {
  175. await iterativeSetTimeout(scanStyles, 0.1, () => checkAllStylesFound(["header_item__",
  176. "quick-search_wrapper__",
  177. "slanted-wrapper_root__",
  178. "slanted-wrapper_variantGrayTransparent__",
  179. "slanted-wrapper_start__",
  180. "slanted-wrapper_right__",
  181. "quick-search_searchInputWrapper__",
  182. "slanted-wrapper_end__",
  183. "slanted-wrapper_right__",
  184. "quick-search_searchInputButton__",
  185. "game-options_optionLabel__",
  186. "game-options_optionLabel__",
  187. "quick-search_iconSection__",
  188. "quick-search_searchInputButton__"]) !== undefined);
  189. if (hide) {
  190. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "";
  191. }
  192. styles["header_item__"] = cn("header_item__");
  193. styles["quick-search_wrapper__"] = cn("quick-search_wrapper__");
  194. styles["slanted-wrapper_root__"] = cn("slanted-wrapper_root__");
  195. styles["slanted-wrapper_variantGrayTransparent__"] = cn("slanted-wrapper_variantGrayTransparent__");
  196. styles["slanted-wrapper_start__"] = cn("slanted-wrapper_start__");
  197. styles["slanted-wrapper_right__"] = cn("slanted-wrapper_right__");
  198. styles["quick-search_searchInputWrapper__"] = cn("quick-search_searchInputWrapper__");
  199. styles["slanted-wrapper_end__"] = cn("slanted-wrapper_end__");
  200. styles["quick-search_searchInputButton__"] = cn("quick-search_searchInputButton__");
  201. styles["game-options_optionLabel__"] = cn("game-options_optionLabel__");
  202. styles["quick-search_iconSection__"] = cn("quick-search_iconSection__");
  203. styles["quick-search_searchInputButton__"] = cn("quick-search_searchInputButton__");
  204. GM_setValue("backgroundReplacerStyles", styles);
  205. setStyles()
  206. }
  207.  
  208. const setStyles = () => {
  209. document.querySelector("#backgroundReplacerPopupWrapper").className = styles["header_item__"];
  210. document.querySelector("#backgroundReplacerSearchWrapper").className = styles["quick-search_wrapper__"];
  211. document.querySelector("#backgroundReplacerSlantedRoot").className = styles["slanted-wrapper_root__"]+ " " + styles["slanted-wrapper_variantGrayTransparent__"];
  212. document.querySelector("#backgroundReplacerSlantedStart").className = styles["slanted-wrapper_start__"]+ " " + styles["slanted-wrapper_right__"];
  213. document.querySelector("#backgroundReplacerInputWrapper").className = styles["quick-search_searchInputWrapper__"];
  214. document.querySelector("#backgroundReplacerSlantedEnd").className = styles["slanted-wrapper_end__"]+ " " + styles["slanted-wrapper_right__"];
  215. document.querySelector("#backgroundReplacerToggle").className = styles["quick-search_searchInputButton__"];
  216. document.querySelector("#backgroundReplacerLabel1").className = styles["game-options_optionLabel__"];
  217. document.querySelector("#backgroundReplacerLabel2").className = styles["game-options_optionLabel__"];
  218. document.querySelector("#backgroundReplacerTogglePicture").className = styles["quick-search_iconSection__"];
  219. document.querySelectorAll(".deleteButton").forEach(el => el.className = el.className + " " + cn("quick-search_searchInputButton__"));
  220. }
  221.  
  222.  
  223. const insertHeaderGui = async (header, gui) => {
  224.  
  225. header.insertAdjacentHTML('afterbegin', gui);
  226.  
  227. // Resolve class names
  228. if (hide) {
  229. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "true"
  230. }
  231.  
  232. scanStyles().then(() => uploadDownloadStyles());
  233. setStyles();
  234.  
  235.  
  236.  
  237. const showButton = document.querySelector('#backgroundReplacerToggle');
  238. const popup = document.querySelector('#backgroundReplacerPopup');
  239. popup.style.display = 'none';
  240.  
  241. document.addEventListener('click', (e) => {
  242. const target = e.target;
  243. if (target == popup || popup.contains(target) || !document.contains(target)) return;
  244. if (target.matches('#backgroundReplacerToggle, #backgroundReplacerToggle *')) {
  245. e.preventDefault();
  246. showPopup(showButton, popup);
  247. } else {
  248. popup.style.display = 'none';
  249. }
  250.  
  251. if (document.querySelector('#enableScriptHeader')) {
  252. if (localStorage.getItem('blinkEnabled') === 'enabled') {
  253. document.querySelector('#enableScriptHeader').checked = true;
  254. }
  255. }
  256. });
  257. }
  258.  
  259. // Global to track whether the most recent image insertion was done on homepage
  260. let isHomePage = location.pathname == "/";
  261.  
  262. const insertBackground = (refresh=false) => {
  263. let inGame = false;
  264. let el = document.querySelector("[class^='background_wrapper']");
  265. if (!el) {
  266. inGame = true;
  267. el = document.querySelector("#__next");
  268. if (!el) return;
  269. // Because this element has multiple classes, we need to use a different selector
  270. const def = document.querySelector("[class*=in-game_backgroundDefault__]");
  271. let reg = /^in-game_backgroundDefault__/;
  272. if (def) {
  273. def.classList = Array.from(def.classList).filter(cl => !cl.match(reg));
  274. }
  275. const partyRoot = document.querySelector("[class^=party_root__]");
  276. if (partyRoot) {
  277. partyRoot.style.background = "none";
  278. }
  279. // Without this, you can see the background behind the map in a game summary
  280.  
  281. // Purple color used by geoguessr, with .9 alpha
  282. const purple9 = "rgba(12 12 46 / .9)";
  283. // .7 alpha
  284. const purple7 = "rgba(12 12 46 / .7)";
  285. const gameSummary = document.querySelector("[class^=game-summary_container__");
  286. if (gameSummary) {
  287. gameSummary.style.opacity = "1";
  288. gameSummary.style.backgroundColor = purple9;
  289. }
  290. const header = document.querySelector("[class^=game-summary_playedRoundsHeader__");
  291. if (header) {
  292. header.style.backgroundColor = purple7;
  293. }
  294.  
  295. }
  296. // We only want the zindex = -1 to exist in game settings, on other pages it's detrimental
  297. let img = document.querySelector('.customBackground');
  298. if (refresh) {
  299. img.remove();
  300. img = document.querySelector('.customBackground');
  301. }
  302. if (img) {
  303. if (!inGame) {
  304. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  305. }
  306. // Return if most recent insertion was in same area (homepage vs not)
  307. if (isHomePage == (location.pathname == "/")) {
  308. return;
  309. }
  310. img.remove();
  311. // Update isHomePage
  312. }
  313. if (!img) {
  314. img = document.createElement("img")
  315. img.classList.add("customBackground");
  316. if (inGame) {
  317. img.classList.add("zindex");
  318. } else {
  319. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  320. }
  321. }
  322. isHomePage = location.pathname == "/";
  323. if (isHomePage && homePageImgURL) {
  324. img.src = homePageImgURL;
  325. } else if (!isHomePage && otherPagesImgURL) {
  326. img.src = otherPagesImgURL;
  327. } else {
  328. return
  329. }
  330. el.appendChild(img);
  331. }
  332.  
  333. const updateStorage = (listName, newList) => {
  334. GM_setValue(listName, newList);
  335. }
  336.  
  337. const validate = (e, homepage) => {
  338. const patt = new RegExp(".*.(jpg|png|gif|jpeg|webp|svg)","i");
  339. if (e.key == "Enter") {
  340. if (patt.test(e.target.value)) {
  341. if (homepage) {
  342. let homepageImages = GM_getValue("homepageImages");
  343. homepageImages.push(e.target.value);
  344. if (homepageImages.length == 1) {
  345. homePageImgURL = homepageImages[0];
  346. }
  347. GM_setValue("homepageImages", homepageImages);
  348. homePageImageList = homepageImages
  349. } else {
  350. let otherImagesNew = GM_getValue("otherImages");
  351. otherImagesNew.push(e.target.value);
  352. if (otherImagesNew.length == 1) {
  353. otherPagesImgURL = otherImagesNew[0];
  354. }
  355. GM_setValue("otherImages", otherImagesNew);
  356. otherImages = otherImagesNew;
  357. }
  358. refreshPopup();
  359. e.target.value = "";
  360. } else {
  361. window.alert("This link doesn't seem to be to an image file, it should end in .jpg, .jpeg, .png, .gif, .webp, or .svg");
  362. }
  363. }
  364. }
  365.  
  366. const removeImage = (image, div, list, listName) => {
  367. let result = window.confirm("Are you sure you want to remove this image?");
  368. if (!result) {
  369. return
  370. }
  371. let i = list.indexOf(image);
  372. if (i != -1) {
  373. list.splice(i, 1);
  374. updateStorage(listName, list);
  375. refreshPopup();
  376. if (listName == "otherImages" && !list.includes(image)) {
  377. setOtherImg();
  378. updateImage(true);
  379. }
  380. if (listName == "homepageImages" && !list.includes(image)) {
  381. setHomePageImg();
  382. updateImage(true);
  383. }
  384. }
  385. };
  386.  
  387. const displayImage = (image, imagesDiv, list, listName) => {
  388. const el = document.createElement("img");
  389. const div = document.createElement("div");
  390. div.className = "backgroundImageWrapper";
  391.  
  392. el.src = image
  393. el.className = "backgroundImage";
  394. div.appendChild(el);
  395.  
  396. const deleteIcon = document.createElement("img");
  397. deleteIcon.className = "deleteIcon";
  398. deleteIcon.src = "https://www.svgrepo.com/show/493964/delete-1.svg";
  399.  
  400. const deleteButton = document.createElement("button");
  401. deleteButton.className = styles["quick-search_searchInputButton__"] + " " + "deleteButton";
  402. deleteButton.appendChild(deleteIcon);
  403. deleteButton.addEventListener("click", e => {
  404. removeImage(image, div, list, listName);
  405. });
  406.  
  407. div.appendChild(deleteButton);
  408.  
  409. imagesDiv.appendChild(div);
  410. }
  411.  
  412. const refreshPopup = () => {
  413. if (document.querySelector('[class^=header_header__]') && document.querySelector('#backgroundReplacerPopupWrapper')) {
  414. let div = document.querySelector("#homePageImages");
  415. while (div.children.length) {
  416. div.removeChild(div.children[0]);
  417. }
  418. div = document.querySelector("#otherPagesImages");
  419. while (div.children.length) {
  420. div.removeChild(div.children[0]);
  421. }
  422. addPopup(true);
  423. const showButton = document.querySelector('#backgroundReplacerToggle');
  424. const popup = document.querySelector('#backgroundReplacerPopup');
  425. showPopup(showButton, popup);
  426. }
  427. }
  428.  
  429. const addPopup = (refresh=false) => {
  430. if (refresh || (document.querySelector('[class^=header_header__]') && document.querySelector('#backgroundReplacerPopupWrapper') === null)) {
  431. if (!refresh) {
  432. insertHeaderGui(document.querySelector('[class^=header_context__]'), guiHTMLHeader)
  433. const homepageInput = document.querySelector("#homepageInput");
  434. homepageInput.addEventListener("keyup", e => {
  435. validate(e, true);
  436. });
  437. const otherpagesInput = document.querySelector("#otherpagesInput");
  438. otherpagesInput.addEventListener("keyup", e => {
  439. validate(e, false);
  440. });
  441. }
  442. const homePageImagesDiv = document.querySelector('#homePageImages');
  443. if (homePageImagesDiv) {
  444. // Loop through images and display them
  445. for (let i = 0; i < homePageImageList.length; i++) {
  446. displayImage(homePageImageList[i], homePageImagesDiv,homePageImageList, "homepageImages");
  447. }
  448. }
  449. const otherPagesImagesDiv = document.querySelector("#otherPagesImages");
  450. if (otherPagesImagesDiv) {
  451. // Loop through images and display them
  452. for (let i = 0; i < otherImages.length; i++) {
  453. displayImage(otherImages[i], otherPagesImagesDiv, otherImages, "otherImages");
  454. }
  455. }
  456. }
  457. }
  458.  
  459. const updateImage = (refresh=false) => {
  460. // Don't do anything while the page is loading
  461. if (document.querySelector("[class^=page-loading_loading__]")) return;
  462. addPopup();
  463. insertBackground(refresh);
  464. }
  465.  
  466.  
  467.  
  468. new MutationObserver(async (mutations) => {
  469. updateImage()
  470. }).observe(document.body, { subtree: true, childList: true });