GeoGuessr Background Replacer

Replaces the background of the geoguessr pages with your own images

当前为 2023-10-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GeoGuessr Background Replacer
  3. // @description Replaces the background of the geoguessr pages with your own images
  4. // @version 2.1.1
  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. console.log(cn("label_variantWhite__"))
  79.  
  80. const setHomePageImg = (img = false) => {
  81. if (img) {
  82. homePageImgURL = img;
  83. } else if(homePageImageList.length) {
  84. homePageImgURL = homePageImageList[Math.floor((Math.random()*homePageImageList.length))];
  85. } else {
  86. homePageImgURL = "";
  87. }
  88. console.log(homePageImgURL);
  89. }
  90.  
  91. setHomePageImg();
  92.  
  93. let otherPagesImgURL;
  94.  
  95. const setOtherImg = (img = false) => {
  96. if (img) {
  97. otherPagesImgURL = img;
  98. } else if(otherImages.length) {
  99. otherPagesImgURL = otherImages[Math.floor((Math.random()*otherImages.length))];
  100. } else {
  101. otherPagesImgURL = "";
  102. }
  103. }
  104.  
  105. setOtherImg();
  106.  
  107. let css = `.customBackground { bottom: 0;
  108. display: block;
  109. height: 100%;
  110. object-fit: cover;
  111. pointer-events: none;
  112. position: fixed;
  113. right: 0;
  114. transition: .2s ease-in-out;
  115. width: 100%;
  116. }
  117. .zindex {
  118. z-index: -1;
  119. }
  120. .deleteIcon {
  121. width: 25px;
  122. filter: brightness(0) invert(1);
  123. opacity: 60%;
  124. }
  125. .backgroundImage {
  126. width: 20em;
  127. }
  128. .deleteButton {
  129. width: 59.19px;
  130. margin-bottom: 8em;
  131. }
  132. .backgroundImageWrapper {
  133. display: flex;
  134. padding: .5em;
  135. }
  136. .backgroundImageWrapper {
  137. position: relative;
  138. display: flex; /* To lay out the imageContainer and button side by side */
  139. align-items: center; /* Vertically center align the contents */
  140. }
  141.  
  142. .imageContainer {
  143. position: relative;
  144. width: /* e.g., 300px; */;
  145. height: /* e.g., 200px; */;
  146. overflow: hidden;
  147. cursor: pointer; /* This line changes the cursor */
  148. }
  149.  
  150. .backgroundImage {
  151. width: 100%;
  152. height: 100%;
  153. transition: opacity 0.3s ease;
  154. vertical-align: bottom;
  155. }
  156.  
  157. .overlay {
  158. position: absolute;
  159. top: 0;
  160. left: 0;
  161. width: 100%;
  162. height: 100%;
  163. background: rgba(0,0,0,0.7);
  164. display: flex;
  165. justify-content: center;
  166. align-items: center;
  167. opacity: 0;
  168. transition: opacity 0.3s ease;
  169. }
  170.  
  171. .imageContainer:hover .backgroundImage {
  172. opacity: 0.3;
  173. }
  174.  
  175. .imageContainer:hover .overlay {
  176. opacity: 1;
  177. }
  178.  
  179. .overlay span {
  180. color: white;
  181. font-size: 18px;
  182. }
  183.  
  184. /* Add some margin if you want space between the image and the button */
  185. .backgroundImageWrapper button {
  186. margin-left: 10px; /* adjust as needed */
  187. }
  188.  
  189.  
  190. /* You can style the text within the overlay here */
  191. .overlay span {
  192. color: white;
  193. font-size: 18px;
  194. text-align: center;
  195. }
  196. .deleteIconPicture {
  197. justifyContent:center;
  198. }
  199.  
  200. `;
  201. GM_addStyle(css);
  202.  
  203.  
  204. const showPopup = (showButton, popup) => {
  205. popup.style.display = 'block';
  206. Popper.createPopper(showButton, popup, {
  207. placement: 'bottom',
  208. modifiers: [
  209. {
  210. name: 'offset',
  211. options: {
  212. offset: [0, 10],
  213. },
  214. },
  215. ],
  216. });
  217. }
  218.  
  219. const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  220.  
  221. const iterativeSetTimeout = async (func, initDelay, cond) => {
  222. while (!cond()) {
  223. await delay(initDelay);
  224. await func();
  225. initDelay *= 2;
  226. console.log(Date.now());
  227. }
  228. };
  229.  
  230. // Caching system for styles
  231. // Basically, we have a browser stored styles object,
  232. // which contains the most recent classNames found by scanStyles()
  233. // This is what the script will immediately use upon loading,
  234. // so that there's no pause in delivering the UI to the user
  235. // But the script will also fire off this function
  236. // which will use the above iterativeSetTimeout function to call scanStyles
  237. // This is so there aren't a thousand calls in quick succession.
  238. // Once all the classNames we're looking for are found,
  239. // it will update the local storage and the ui with the (possibly) new classnames
  240. const stylesUsed = [
  241. "header_item__",
  242. "quick-search_wrapper__",
  243. "slanted-wrapper_root__",
  244. "slanted-wrapper_variantGrayTransparent__",
  245. "slanted-wrapper_start__",
  246. "quick-search_searchInputWrapper__",
  247. "slanted-wrapper_end__",
  248. "slanted-wrapper_right__",
  249. "quick-search_searchInputButton__",
  250. "quick-search_iconSection__",
  251. ];
  252.  
  253. const uploadDownloadStyles = async () => {
  254. stylesUsed.forEach(style => {
  255. // styles[style] = cn(style);
  256. console.log(style);
  257. console.log(cn(style));
  258. });
  259. await iterativeSetTimeout(scanStyles, 0.1, () => checkAllStylesFound(stylesUsed) !== undefined);
  260. if (hide) {
  261. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "";
  262. }
  263. stylesUsed.forEach(style => {
  264. styles[style] = cn(style);
  265. console.log(style);
  266. console.log(cn(style));
  267. });
  268. setStyles();
  269. GM_setValue("backgroundReplacerStyles", styles);
  270. }
  271.  
  272. const getStyle = style => {
  273. return styles[style];
  274. }
  275.  
  276. const setStyles = () => {
  277. try {
  278. document.querySelector("#backgroundReplacerPopupWrapper").className = getStyle("header_item__");
  279. document.querySelector("#backgroundReplacerSearchWrapper").className = getStyle("quick-search_wrapper__");
  280. document.querySelector("#backgroundReplacerSlantedRoot").className = getStyle("slanted-wrapper_root__") + " " + getStyle("slanted-wrapper_variantGrayTransparent__");
  281. document.querySelector("#backgroundReplacerSlantedStart").className = getStyle("slanted-wrapper_start__")+ " " + getStyle("slanted-wrapper_right__");
  282. document.querySelector("#backgroundReplacerInputWrapper").className = getStyle("quick-search_searchInputWrapper__");
  283. document.querySelector("#backgroundReplacerSlantedEnd").className = getStyle("slanted-wrapper_end__")+ " " + getStyle("slanted-wrapper_right__");
  284. document.querySelector("#backgroundReplacerToggle").className = getStyle("quick-search_searchInputButton__");
  285. document.querySelector("#backgroundReplacerLabel1").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  286. document.querySelector("#backgroundReplacerLabel2").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  287. document.querySelector("#backgroundReplacerTogglePicture").className = getStyle("quick-search_iconSection__");
  288. document.querySelectorAll(".deleteButton").forEach(el => el.className = el.className + " " + getStyle("quick-search_searchInputButton__"));
  289. } catch (err) {
  290. console.error(err);
  291. }
  292. }
  293.  
  294.  
  295. const insertHeaderGui = async (header, gui) => {
  296.  
  297. header.insertAdjacentHTML('afterbegin', gui);
  298.  
  299. // Resolve class names
  300. if (hide) {
  301. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "true"
  302. }
  303.  
  304. scanStyles().then(() => uploadDownloadStyles());
  305. setStyles();
  306.  
  307.  
  308.  
  309. const showButton = document.querySelector('#backgroundReplacerToggle');
  310. const popup = document.querySelector('#backgroundReplacerPopup');
  311. popup.style.display = 'none';
  312.  
  313. document.addEventListener('click', (e) => {
  314. const target = e.target;
  315. if (target == popup || popup.contains(target) || !document.contains(target)) return;
  316. if (target.matches('#backgroundReplacerToggle, #backgroundReplacerToggle *')) {
  317. e.preventDefault();
  318. showPopup(showButton, popup);
  319. } else {
  320. popup.style.display = 'none';
  321. }
  322.  
  323. if (document.querySelector('#enableScriptHeader')) {
  324. if (localStorage.getItem('blinkEnabled') === 'enabled') {
  325. document.querySelector('#enableScriptHeader').checked = true;
  326. }
  327. }
  328. });
  329. }
  330.  
  331. // Global to track whether the most recent image insertion was done on homepage
  332. let isHomePage = location.pathname == "/";
  333.  
  334. const insertBackground = (refresh=false) => {
  335. let inGame = false;
  336. let el = document.querySelector("[class^='background_wrapper']");
  337. if (!el) {
  338. inGame = true;
  339. el = document.querySelector("#__next");
  340. if (!el) return;
  341. // Because this element has multiple classes, we need to use a different selector
  342. const def = document.querySelector("[class*=in-game_backgroundDefault__]");
  343. let reg = /^in-game_backgroundDefault__/;
  344. if (def) {
  345. def.classList = Array.from(def.classList).filter(cl => !cl.match(reg));
  346. }
  347. const partyRoot = document.querySelector("[class^=party_root__]");
  348. if (partyRoot) {
  349. partyRoot.style.background = "none";
  350. }
  351. // Without this, you can see the background behind the map in a game summary
  352.  
  353. // Purple color used by geoguessr, with .9 alpha
  354. const purple9 = "rgba(12 12 46 / .9)";
  355. // .7 alpha
  356. const purple7 = "rgba(12 12 46 / .7)";
  357. const gameSummary = document.querySelector("[class^=game-summary_container__");
  358. if (gameSummary) {
  359. gameSummary.style.opacity = "1";
  360. gameSummary.style.backgroundColor = purple9;
  361. }
  362. const header = document.querySelector("[class^=game-summary_playedRoundsHeader__");
  363. if (header) {
  364. header.style.backgroundColor = purple7;
  365. }
  366.  
  367. }
  368. // We only want the zindex = -1 to exist in game settings, on other pages it's detrimental
  369. let img = document.querySelector('.customBackground');
  370. if (refresh) {
  371. img.remove();
  372. img = document.querySelector('.customBackground');
  373. }
  374. if (img) {
  375. if (!inGame) {
  376. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  377. }
  378. // Return if most recent insertion was in same area (homepage vs not)
  379. if (isHomePage == (location.pathname == "/")) {
  380. return;
  381. }
  382. img.remove();
  383. // Update isHomePage
  384. }
  385. if (!img) {
  386. img = document.createElement("img")
  387. img.classList.add("customBackground");
  388. if (inGame) {
  389. img.classList.add("zindex");
  390. } else {
  391. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  392. }
  393. }
  394. isHomePage = location.pathname == "/";
  395. if (isHomePage && homePageImgURL) {
  396. img.src = homePageImgURL;
  397. } else if (!isHomePage && otherPagesImgURL) {
  398. img.src = otherPagesImgURL;
  399. } else {
  400. return
  401. }
  402. el.appendChild(img);
  403. }
  404.  
  405. const updateStorage = (listName, newList) => {
  406. GM_setValue(listName, newList);
  407. }
  408.  
  409. const validate = (e, homepage) => {
  410. const patt = new RegExp(".*.(jpg|png|gif|jpeg|webp|svg|avif)","i");
  411. if (e.key == "Enter") {
  412. if (patt.test(e.target.value)) {
  413. if (homepage) {
  414. let homepageImages = GM_getValue("homepageImages");
  415. homepageImages.push(e.target.value);
  416. if (homepageImages.length == 1) {
  417. homePageImgURL = homepageImages[0];
  418. }
  419. GM_setValue("homepageImages", homepageImages);
  420. homePageImageList = homepageImages
  421. } else {
  422. let otherImagesNew = GM_getValue("otherImages");
  423. otherImagesNew.push(e.target.value);
  424. if (otherImagesNew.length == 1) {
  425. otherPagesImgURL = otherImagesNew[0];
  426. }
  427. GM_setValue("otherImages", otherImagesNew);
  428. otherImages = otherImagesNew;
  429. }
  430. refreshPopup();
  431. e.target.value = "";
  432. } else {
  433. window.alert("This link doesn't seem to be to an image file, it should end in .jpg, .jpeg, .png, .gif, .webp, .avif, or .svg");
  434. }
  435. }
  436. }
  437.  
  438. const removeImage = (image, div, list, listName) => {
  439. let result = window.confirm("Are you sure you want to remove this image?");
  440. if (!result) {
  441. return
  442. }
  443. let i = list.indexOf(image);
  444. if (i != -1) {
  445. list.splice(i, 1);
  446. updateStorage(listName, list);
  447. refreshPopup();
  448. if (listName == "otherImages" && !list.includes(image)) {
  449. setOtherImg();
  450. updateImage(true);
  451. }
  452. if (listName == "homepageImages" && !list.includes(image)) {
  453. setHomePageImg();
  454. updateImage(true);
  455. }
  456. }
  457. };
  458.  
  459. // displays an image in the popup
  460. const displayImage = (image, imagesDiv, list, listName) => {
  461. const img = document.createElement("img");
  462. const div = document.createElement("div");
  463. div.className = "backgroundImageWrapper";
  464. const container = document.createElement("div");
  465. container.className = "imageContainer";
  466.  
  467. img.src = image
  468. img.className = "backgroundImage";
  469. div.appendChild(container);
  470. container.appendChild(img);
  471.  
  472. const deleteIcon = document.createElement("img");
  473. deleteIcon.className = "deleteIcon";
  474. deleteIcon.src = "https://www.svgrepo.com/show/493964/delete-1.svg";
  475.  
  476. const deleteButton = document.createElement("button");
  477. deleteButton.className = getStyle("quick-search_searchInputButton__") + " " + "deleteButton";
  478. deleteButton.appendChild(deleteIcon);
  479. deleteButton.addEventListener("click", e => {
  480. removeImage(image, div, list, listName);
  481. });
  482.  
  483. const overlay = document.createElement("div");
  484. overlay.className = "overlay";
  485. const span = document.createElement("span");
  486. isHomePage = location.pathname == "/";
  487. span.innerText = ((isHomePage && listName == "homepageImages")
  488. || (!isHomePage && listName == "otherImages"))
  489. ? "Make current image" : "You're not on a page where this image will display";
  490. overlay.appendChild(span);
  491.  
  492. container.appendChild(overlay);
  493. div.appendChild(deleteButton);
  494.  
  495. imagesDiv.appendChild(div);
  496. container.addEventListener('click', function() {
  497. if (listName == "homepageImages") {
  498. setHomePageImg(image);
  499. }
  500. if (listName == "otherImages") {
  501. setOtherImg(image);
  502. }
  503. insertBackground(true);
  504. });
  505. }
  506.  
  507. const refreshPopup = () => {
  508. if (document.querySelector('[class^=header_header__]') && document.querySelector('#backgroundReplacerPopupWrapper')) {
  509. let div = document.querySelector("#homePageImages");
  510. while (div.children.length) {
  511. div.removeChild(div.children[0]);
  512. }
  513. div = document.querySelector("#otherPagesImages");
  514. while (div.children.length) {
  515. div.removeChild(div.children[0]);
  516. }
  517. addPopup(true);
  518. const showButton = document.querySelector('#backgroundReplacerToggle');
  519. const popup = document.querySelector('#backgroundReplacerPopup');
  520. showPopup(showButton, popup);
  521. }
  522. }
  523.  
  524. const addPopup = (refresh=false) => {
  525. if (refresh || (document.querySelector('[class^=header_header__]') && document.querySelector('#backgroundReplacerPopupWrapper') === null)) {
  526. if (!refresh) {
  527. insertHeaderGui(document.querySelector('[class^=header_context__]'), guiHTMLHeader)
  528. const homepageInput = document.querySelector("#homepageInput");
  529. homepageInput.addEventListener("keyup", e => {
  530. validate(e, true);
  531. });
  532. const otherpagesInput = document.querySelector("#otherpagesInput");
  533. otherpagesInput.addEventListener("keyup", e => {
  534. validate(e, false);
  535. });
  536. }
  537. const homePageImagesDiv = document.querySelector('#homePageImages');
  538. if (homePageImagesDiv) {
  539. // Loop through images and display them
  540. for (let i = 0; i < homePageImageList.length; i++) {
  541. displayImage(homePageImageList[i], homePageImagesDiv,homePageImageList, "homepageImages");
  542. }
  543. }
  544. const otherPagesImagesDiv = document.querySelector("#otherPagesImages");
  545. if (otherPagesImagesDiv) {
  546. // Loop through images and display them
  547. for (let i = 0; i < otherImages.length; i++) {
  548. displayImage(otherImages[i], otherPagesImagesDiv, otherImages, "otherImages");
  549. }
  550. }
  551. }
  552. }
  553.  
  554. const updateImage = (refresh=false) => {
  555. // Don't do anything while the page is loading
  556. if (document.querySelector("[class^=page-loading_loading__]")) return;
  557. addPopup();
  558. insertBackground(refresh);
  559. }
  560.  
  561.  
  562.  
  563. new MutationObserver(async (mutations) => {
  564. updateImage()
  565. }).observe(document.body, { subtree: true, childList: true });