GeoGuessr Background Replacer

Replaces the background of the geoguessr pages with your own images

当前为 2024-11-22 提交的版本,查看 最新版本

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