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.4
  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
  197. }
  198. `;
  199. GM_addStyle(css);
  200.  
  201.  
  202. const showPopup = (showButton, popup) => {
  203. popup.style.display = 'block';
  204. Popper.createPopper(showButton, popup, {
  205. placement: 'bottom',
  206. modifiers: [
  207. {
  208. name: 'offset',
  209. options: {
  210. offset: [0, 10],
  211. },
  212. },
  213. ],
  214. });
  215. }
  216.  
  217. const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  218.  
  219. const iterativeSetTimeout = async (func, initDelay, cond) => {
  220. while (!cond()) {
  221. await delay(initDelay);
  222. await func();
  223. initDelay *= 2;
  224. stylesUsed.forEach(style => {
  225. styles[style] = cn(style);
  226. });
  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. "quick-search_searchInputWrapper__",
  244. "quick-search_searchInputButton__",
  245. "quick-search_iconSection__",
  246. ];
  247.  
  248. const uploadDownloadStyles = async () => {
  249. stylesUsed.forEach(style => {
  250. // styles[style] = cn(style);
  251. // console.log(style);
  252. // console.log(cn(style));
  253. });
  254. await iterativeSetTimeout(scanStyles, 0.1, () => checkAllStylesFound(stylesUsed) !== undefined);
  255. if (hide) {
  256. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "";
  257. }
  258. stylesUsed.forEach(style => {
  259. styles[style] = cn(style);
  260. // console.log(style);
  261. // console.log(cn(style));
  262. });
  263. setStyles();
  264. GM_setValue("backgroundReplacerStyles", styles);
  265. }
  266.  
  267. const getStyle = style => {
  268. return styles[style];
  269. }
  270.  
  271. const setStyles = () => {
  272. try {
  273. document.querySelector("#backgroundReplacerPopupWrapper").className = getStyle("header_item__");
  274. document.querySelector("#backgroundReplacerSearchWrapper").className = getStyle("quick-search_wrapper__");
  275. document.querySelector("#backgroundReplacerInputWrapper").className = getStyle("quick-search_searchInputWrapper__");
  276. document.querySelector("#backgroundReplacerToggle").className = getStyle("quick-search_searchInputButton__");
  277. document.querySelector("#backgroundReplacerLabel1").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  278. document.querySelector("#backgroundReplacerLabel2").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  279. document.querySelector("#backgroundReplacerTogglePicture").className = getStyle("quick-search_iconSection__");
  280. document.querySelectorAll(".deleteButton").forEach(el => el.className = el.className + " " + getStyle("quick-search_searchInputButton__"));
  281. } catch (err) {
  282. console.error(err);
  283. }
  284. }
  285.  
  286.  
  287. const insertHeaderGui = async (header, gui) => {
  288.  
  289. header.insertAdjacentHTML('afterbegin', gui);
  290.  
  291. // Resolve class names
  292. if (hide) {
  293. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "true"
  294. }
  295.  
  296. scanStyles().then(() => uploadDownloadStyles());
  297. setStyles();
  298.  
  299.  
  300.  
  301. const showButton = document.querySelector('#backgroundReplacerToggle');
  302. const popup = document.querySelector('#backgroundReplacerPopup');
  303. popup.style.display = 'none';
  304.  
  305. document.addEventListener('click', (e) => {
  306. const target = e.target;
  307. if (target == popup || popup.contains(target) || !document.contains(target)) return;
  308. if (target.matches('#backgroundReplacerToggle, #backgroundReplacerToggle *')) {
  309. e.preventDefault();
  310. showPopup(showButton, popup);
  311. } else {
  312. popup.style.display = 'none';
  313. }
  314. });
  315. }
  316.  
  317. // Global to track whether the most recent image insertion was done on homepage
  318. let isHomePage = location.pathname == "/";
  319.  
  320. const insertBackground = (refresh=false) => {
  321. let inGame = false;
  322. let el = document.querySelector("[class^='background_wrapper']");
  323. if (!el) {
  324. inGame = true;
  325. el = document.querySelector("#__next");
  326. if (!el) return;
  327. // Because this element has multiple classes, we need to use a different selector
  328. const def = document.querySelector("[class*=in-game_backgroundDefault__]");
  329. let reg = /^in-game_backgroundDefault__/;
  330. if (def) {
  331. def.classList = Array.from(def.classList).filter(cl => !cl.match(reg));
  332. }
  333. const partyRoot = document.querySelector("[class^=party_root__]");
  334. if (partyRoot) {
  335. partyRoot.style.background = "none";
  336. }
  337. // Without this, you can see the background behind the map in a game summary
  338.  
  339. // Purple color used by geoguessr, with .9 alpha
  340. const purple9 = "rgba(12 12 46 / .9)";
  341. // .7 alpha
  342. const purple7 = "rgba(12 12 46 / .7)";
  343. const gameSummary = document.querySelector("[class^=game-summary_container__");
  344. if (gameSummary) {
  345. gameSummary.style.opacity = "1";
  346. gameSummary.style.backgroundColor = purple9;
  347. }
  348. const header = document.querySelector("[class^=game-summary_playedRoundsHeader__");
  349. if (header) {
  350. header.style.backgroundColor = purple7;
  351. }
  352.  
  353. }
  354. // We only want the zindex = -1 to exist in game settings, on other pages it's detrimental
  355. let img = document.querySelector('.customBackground');
  356. if (refresh) {
  357. img.remove();
  358. img = document.querySelector('.customBackground');
  359. }
  360. if (img) {
  361. if (!inGame) {
  362. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  363. }
  364. // Return if most recent insertion was in same area (homepage vs not)
  365. if (isHomePage == (location.pathname == "/")) {
  366. return;
  367. }
  368. img.remove();
  369. // Update isHomePage
  370. }
  371. if (!img) {
  372. img = document.createElement("img")
  373. img.classList.add("customBackground");
  374. if (inGame) {
  375. img.classList.add("zindex");
  376. } else {
  377. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  378. }
  379. }
  380. isHomePage = location.pathname == "/";
  381. if (isHomePage && homePageImgURL) {
  382. img.src = homePageImgURL;
  383. const startPageWrapper = document.querySelector("[class^=startpage_newWrapper__")
  384. startPageWrapper.classList.add('removeBackground');
  385. } else if (!isHomePage && otherPagesImgURL) {
  386. img.src = otherPagesImgURL;
  387. } else {
  388. return
  389. }
  390. el.appendChild(img);
  391. }
  392.  
  393. const updateStorage = (listName, newList) => {
  394. GM_setValue(listName, newList);
  395. }
  396.  
  397. const validate = (e, homepage) => {
  398. const patt = new RegExp(".*.(jpg|png|gif|jpeg|webp|svg|avif)","i");
  399. if (e.key == "Enter") {
  400. if (patt.test(e.target.value)) {
  401. if (homepage) {
  402. let homepageImages = GM_getValue("homepageImages");
  403. homepageImages.push(e.target.value);
  404. if (homepageImages.length == 1) {
  405. homePageImgURL = homepageImages[0];
  406. }
  407. GM_setValue("homepageImages", homepageImages);
  408. homePageImageList = homepageImages
  409. } else {
  410. let otherImagesNew = GM_getValue("otherImages");
  411. otherImagesNew.push(e.target.value);
  412. if (otherImagesNew.length == 1) {
  413. otherPagesImgURL = otherImagesNew[0];
  414. }
  415. GM_setValue("otherImages", otherImagesNew);
  416. otherImages = otherImagesNew;
  417. }
  418. refreshPopup();
  419. e.target.value = "";
  420. } else {
  421. 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");
  422. }
  423. }
  424. }
  425.  
  426. const removeImage = (image, div, list, listName) => {
  427. let result = window.confirm("Are you sure you want to remove this image?");
  428. if (!result) {
  429. return
  430. }
  431. let i = list.indexOf(image);
  432. if (i != -1) {
  433. list.splice(i, 1);
  434. updateStorage(listName, list);
  435. refreshPopup();
  436. if (listName == "otherImages" && !list.includes(image)) {
  437. setOtherImg();
  438. updateImage(true);
  439. }
  440. if (listName == "homepageImages" && !list.includes(image)) {
  441. setHomePageImg();
  442. updateImage(true);
  443. }
  444. }
  445. };
  446.  
  447. // displays an image in the popup
  448. const displayImage = (image, imagesDiv, list, listName) => {
  449. const img = document.createElement("img");
  450. const div = document.createElement("div");
  451. div.className = "backgroundImageWrapper";
  452. const container = document.createElement("div");
  453. container.className = "imageContainer";
  454.  
  455. img.src = image
  456. img.className = "backgroundImage";
  457. div.appendChild(container);
  458. container.appendChild(img);
  459.  
  460. const deleteIcon = document.createElement("img");
  461. deleteIcon.className = "deleteIcon";
  462. deleteIcon.src = "https://www.svgrepo.com/show/493964/delete-1.svg";
  463.  
  464. const deleteButton = document.createElement("button");
  465. deleteButton.className = getStyle("quick-search_searchInputButton__") + " " + "deleteButton";
  466. deleteButton.appendChild(deleteIcon);
  467. deleteButton.addEventListener("click", e => {
  468. removeImage(image, div, list, listName);
  469. });
  470.  
  471. const overlay = document.createElement("div");
  472. overlay.className = "overlay";
  473. const span = document.createElement("span");
  474. isHomePage = location.pathname == "/";
  475. span.innerText = ((isHomePage && listName == "homepageImages")
  476. || (!isHomePage && listName == "otherImages"))
  477. ? "Make current image" : "You're not on a page where this image will display";
  478. overlay.appendChild(span);
  479.  
  480. container.appendChild(overlay);
  481. div.appendChild(deleteButton);
  482.  
  483. imagesDiv.appendChild(div);
  484. container.addEventListener('click', function() {
  485. if (listName == "homepageImages") {
  486. setHomePageImg(image);
  487. }
  488. if (listName == "otherImages") {
  489. setOtherImg(image);
  490. }
  491. insertBackground(true);
  492. });
  493. }
  494.  
  495. const refreshPopup = () => {
  496. if (document.querySelector('[class^=header_header__]') && document.querySelector('#backgroundReplacerPopupWrapper')) {
  497. let div = document.querySelector("#homePageImages");
  498. while (div.children.length) {
  499. div.removeChild(div.children[0]);
  500. }
  501. div = document.querySelector("#otherPagesImages");
  502. while (div.children.length) {
  503. div.removeChild(div.children[0]);
  504. }
  505. addPopup(true);
  506. const showButton = document.querySelector('#backgroundReplacerToggle');
  507. const popup = document.querySelector('#backgroundReplacerPopup');
  508. showPopup(showButton, popup);
  509. }
  510. }
  511.  
  512. const addPopup = (refresh=false) => {
  513. if (refresh || (document.querySelector('[class^=header-tablet-desktop_root__]') && document.querySelector('#backgroundReplacerPopupWrapper') === null)) {
  514. if (!refresh) {
  515. insertHeaderGui(document.querySelector('[class^=header-tablet-desktop_desktopSectionRight__]'), guiHTMLHeader)
  516. const homepageInput = document.querySelector("#homepageInput");
  517. homepageInput.addEventListener("keyup", e => {
  518. validate(e, true);
  519. });
  520. const otherpagesInput = document.querySelector("#otherpagesInput");
  521. otherpagesInput.addEventListener("keyup", e => {
  522. validate(e, false);
  523. });
  524. }
  525. const homePageImagesDiv = document.querySelector('#homePageImages');
  526. if (homePageImagesDiv) {
  527. // Loop through images and display them
  528. for (let i = 0; i < homePageImageList.length; i++) {
  529. displayImage(homePageImageList[i], homePageImagesDiv,homePageImageList, "homepageImages");
  530. }
  531. }
  532. const otherPagesImagesDiv = document.querySelector("#otherPagesImages");
  533. if (otherPagesImagesDiv) {
  534. // Loop through images and display them
  535. for (let i = 0; i < otherImages.length; i++) {
  536. displayImage(otherImages[i], otherPagesImagesDiv, otherImages, "otherImages");
  537. }
  538. }
  539. }
  540. }
  541.  
  542. const updateImage = (refresh=false) => {
  543. // Don't do anything while the page is loading
  544. if (document.querySelector("[class^=page-loading_loading__]")) return;
  545. addPopup();
  546. insertBackground(refresh);
  547. }
  548.  
  549.  
  550.  
  551. new MutationObserver(async (mutations) => {
  552. updateImage()
  553. }).observe(document.body, { subtree: true, childList: true });