GeoGuessr Background Replacer

Replaces the background of the geoguessr pages with your own images

当前为 2024-07-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GeoGuessr Background Replacer
  3. // @description Replaces the background of the geoguessr pages with your own images
  4. // @version 2.1.2
  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="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. stylesUsed.forEach(style => {
  227. styles[style] = cn(style);
  228. });
  229. }
  230. };
  231.  
  232. // Caching system for styles
  233. // Basically, we have a browser stored styles object,
  234. // which contains the most recent classNames found by scanStyles()
  235. // This is what the script will immediately use upon loading,
  236. // so that there's no pause in delivering the UI to the user
  237. // But the script will also fire off this function
  238. // which will use the above iterativeSetTimeout function to call scanStyles
  239. // This is so there aren't a thousand calls in quick succession.
  240. // Once all the classNames we're looking for are found,
  241. // it will update the local storage and the ui with the (possibly) new classnames
  242. const stylesUsed = [
  243. "header_item__",
  244. "quick-search_wrapper__",
  245. "slanted-wrapper_root__",
  246. "slanted-wrapper_variantGrayTransparent__",
  247. "slanted-wrapper_start__",
  248. "quick-search_searchInputWrapper__",
  249. "slanted-wrapper_end__",
  250. "slanted-wrapper_right__",
  251. "quick-search_searchInputButton__",
  252. "quick-search_iconSection__",
  253. ];
  254.  
  255. const uploadDownloadStyles = async () => {
  256. stylesUsed.forEach(style => {
  257. // styles[style] = cn(style);
  258. // console.log(style);
  259. // console.log(cn(style));
  260. });
  261. await iterativeSetTimeout(scanStyles, 0.1, () => checkAllStylesFound(stylesUsed) !== undefined);
  262. if (hide) {
  263. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "";
  264. }
  265. stylesUsed.forEach(style => {
  266. styles[style] = cn(style);
  267. // console.log(style);
  268. // console.log(cn(style));
  269. });
  270. setStyles();
  271. GM_setValue("backgroundReplacerStyles", styles);
  272. }
  273.  
  274. const getStyle = style => {
  275. return styles[style];
  276. }
  277.  
  278. const setStyles = () => {
  279. try {
  280. document.querySelector("#backgroundReplacerPopupWrapper").className = getStyle("header_item__");
  281. document.querySelector("#backgroundReplacerSearchWrapper").className = getStyle("quick-search_wrapper__");
  282. document.querySelector("#backgroundReplacerSlantedRoot").className = getStyle("slanted-wrapper_root__") + " " + getStyle("slanted-wrapper_variantGrayTransparent__");
  283. document.querySelector("#backgroundReplacerSlantedStart").className = getStyle("slanted-wrapper_start__")+ " " + getStyle("slanted-wrapper_right__");
  284. document.querySelector("#backgroundReplacerInputWrapper").className = getStyle("quick-search_searchInputWrapper__");
  285. document.querySelector("#backgroundReplacerSlantedEnd").className = getStyle("slanted-wrapper_end__")+ " " + getStyle("slanted-wrapper_right__");
  286. document.querySelector("#backgroundReplacerToggle").className = getStyle("quick-search_searchInputButton__");
  287. document.querySelector("#backgroundReplacerLabel1").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  288. document.querySelector("#backgroundReplacerLabel2").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  289. document.querySelector("#backgroundReplacerTogglePicture").className = getStyle("quick-search_iconSection__");
  290. document.querySelectorAll(".deleteButton").forEach(el => el.className = el.className + " " + getStyle("quick-search_searchInputButton__"));
  291. } catch (err) {
  292. console.error(err);
  293. }
  294. }
  295.  
  296.  
  297. const insertHeaderGui = async (header, gui) => {
  298.  
  299. header.insertAdjacentHTML('afterbegin', gui);
  300.  
  301. // Resolve class names
  302. if (hide) {
  303. document.querySelector("#backgroundReplacerPopupWrapper").hidden = "true"
  304. }
  305.  
  306. scanStyles().then(() => uploadDownloadStyles());
  307. setStyles();
  308.  
  309.  
  310.  
  311. const showButton = document.querySelector('#backgroundReplacerToggle');
  312. const popup = document.querySelector('#backgroundReplacerPopup');
  313. popup.style.display = 'none';
  314.  
  315. document.addEventListener('click', (e) => {
  316. const target = e.target;
  317. if (target == popup || popup.contains(target) || !document.contains(target)) return;
  318. if (target.matches('#backgroundReplacerToggle, #backgroundReplacerToggle *')) {
  319. e.preventDefault();
  320. showPopup(showButton, popup);
  321. } else {
  322. popup.style.display = 'none';
  323. }
  324.  
  325. if (document.querySelector('#enableScriptHeader')) {
  326. if (localStorage.getItem('blinkEnabled') === 'enabled') {
  327. document.querySelector('#enableScriptHeader').checked = true;
  328. }
  329. }
  330. });
  331. }
  332.  
  333. // Global to track whether the most recent image insertion was done on homepage
  334. let isHomePage = location.pathname == "/";
  335.  
  336. const insertBackground = (refresh=false) => {
  337. let inGame = false;
  338. let el = document.querySelector("[class^='background_wrapper']");
  339. if (!el) {
  340. inGame = true;
  341. el = document.querySelector("#__next");
  342. if (!el) return;
  343. // Because this element has multiple classes, we need to use a different selector
  344. const def = document.querySelector("[class*=in-game_backgroundDefault__]");
  345. let reg = /^in-game_backgroundDefault__/;
  346. if (def) {
  347. def.classList = Array.from(def.classList).filter(cl => !cl.match(reg));
  348. }
  349. const partyRoot = document.querySelector("[class^=party_root__]");
  350. if (partyRoot) {
  351. partyRoot.style.background = "none";
  352. }
  353. // Without this, you can see the background behind the map in a game summary
  354.  
  355. // Purple color used by geoguessr, with .9 alpha
  356. const purple9 = "rgba(12 12 46 / .9)";
  357. // .7 alpha
  358. const purple7 = "rgba(12 12 46 / .7)";
  359. const gameSummary = document.querySelector("[class^=game-summary_container__");
  360. if (gameSummary) {
  361. gameSummary.style.opacity = "1";
  362. gameSummary.style.backgroundColor = purple9;
  363. }
  364. const header = document.querySelector("[class^=game-summary_playedRoundsHeader__");
  365. if (header) {
  366. header.style.backgroundColor = purple7;
  367. }
  368.  
  369. }
  370. // We only want the zindex = -1 to exist in game settings, on other pages it's detrimental
  371. let img = document.querySelector('.customBackground');
  372. if (refresh) {
  373. img.remove();
  374. img = document.querySelector('.customBackground');
  375. }
  376. if (img) {
  377. if (!inGame) {
  378. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  379. }
  380. // Return if most recent insertion was in same area (homepage vs not)
  381. if (isHomePage == (location.pathname == "/")) {
  382. return;
  383. }
  384. img.remove();
  385. // Update isHomePage
  386. }
  387. if (!img) {
  388. img = document.createElement("img")
  389. img.classList.add("customBackground");
  390. if (inGame) {
  391. img.classList.add("zindex");
  392. } else {
  393. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  394. }
  395. }
  396. isHomePage = location.pathname == "/";
  397. if (isHomePage && homePageImgURL) {
  398. img.src = homePageImgURL;
  399. } else if (!isHomePage && otherPagesImgURL) {
  400. img.src = otherPagesImgURL;
  401. } else {
  402. return
  403. }
  404. el.appendChild(img);
  405. }
  406.  
  407. const updateStorage = (listName, newList) => {
  408. GM_setValue(listName, newList);
  409. }
  410.  
  411. const validate = (e, homepage) => {
  412. const patt = new RegExp(".*.(jpg|png|gif|jpeg|webp|svg|avif)","i");
  413. if (e.key == "Enter") {
  414. if (patt.test(e.target.value)) {
  415. if (homepage) {
  416. let homepageImages = GM_getValue("homepageImages");
  417. homepageImages.push(e.target.value);
  418. if (homepageImages.length == 1) {
  419. homePageImgURL = homepageImages[0];
  420. }
  421. GM_setValue("homepageImages", homepageImages);
  422. homePageImageList = homepageImages
  423. } else {
  424. let otherImagesNew = GM_getValue("otherImages");
  425. otherImagesNew.push(e.target.value);
  426. if (otherImagesNew.length == 1) {
  427. otherPagesImgURL = otherImagesNew[0];
  428. }
  429. GM_setValue("otherImages", otherImagesNew);
  430. otherImages = otherImagesNew;
  431. }
  432. refreshPopup();
  433. e.target.value = "";
  434. } else {
  435. 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");
  436. }
  437. }
  438. }
  439.  
  440. const removeImage = (image, div, list, listName) => {
  441. let result = window.confirm("Are you sure you want to remove this image?");
  442. if (!result) {
  443. return
  444. }
  445. let i = list.indexOf(image);
  446. if (i != -1) {
  447. list.splice(i, 1);
  448. updateStorage(listName, list);
  449. refreshPopup();
  450. if (listName == "otherImages" && !list.includes(image)) {
  451. setOtherImg();
  452. updateImage(true);
  453. }
  454. if (listName == "homepageImages" && !list.includes(image)) {
  455. setHomePageImg();
  456. updateImage(true);
  457. }
  458. }
  459. };
  460.  
  461. // displays an image in the popup
  462. const displayImage = (image, imagesDiv, list, listName) => {
  463. const img = document.createElement("img");
  464. const div = document.createElement("div");
  465. div.className = "backgroundImageWrapper";
  466. const container = document.createElement("div");
  467. container.className = "imageContainer";
  468.  
  469. img.src = image
  470. img.className = "backgroundImage";
  471. div.appendChild(container);
  472. container.appendChild(img);
  473.  
  474. const deleteIcon = document.createElement("img");
  475. deleteIcon.className = "deleteIcon";
  476. deleteIcon.src = "https://www.svgrepo.com/show/493964/delete-1.svg";
  477.  
  478. const deleteButton = document.createElement("button");
  479. deleteButton.className = getStyle("quick-search_searchInputButton__") + " " + "deleteButton";
  480. deleteButton.appendChild(deleteIcon);
  481. deleteButton.addEventListener("click", e => {
  482. removeImage(image, div, list, listName);
  483. });
  484.  
  485. const overlay = document.createElement("div");
  486. overlay.className = "overlay";
  487. const span = document.createElement("span");
  488. isHomePage = location.pathname == "/";
  489. span.innerText = ((isHomePage && listName == "homepageImages")
  490. || (!isHomePage && listName == "otherImages"))
  491. ? "Make current image" : "You're not on a page where this image will display";
  492. overlay.appendChild(span);
  493.  
  494. container.appendChild(overlay);
  495. div.appendChild(deleteButton);
  496.  
  497. imagesDiv.appendChild(div);
  498. container.addEventListener('click', function() {
  499. if (listName == "homepageImages") {
  500. setHomePageImg(image);
  501. }
  502. if (listName == "otherImages") {
  503. setOtherImg(image);
  504. }
  505. insertBackground(true);
  506. });
  507. }
  508.  
  509. const refreshPopup = () => {
  510. if (document.querySelector('[class^=header_header__]') && document.querySelector('#backgroundReplacerPopupWrapper')) {
  511. let div = document.querySelector("#homePageImages");
  512. while (div.children.length) {
  513. div.removeChild(div.children[0]);
  514. }
  515. div = document.querySelector("#otherPagesImages");
  516. while (div.children.length) {
  517. div.removeChild(div.children[0]);
  518. }
  519. addPopup(true);
  520. const showButton = document.querySelector('#backgroundReplacerToggle');
  521. const popup = document.querySelector('#backgroundReplacerPopup');
  522. showPopup(showButton, popup);
  523. }
  524. }
  525.  
  526. const addPopup = (refresh=false) => {
  527. if (refresh || (document.querySelector('[class^=header_header__]') && document.querySelector('#backgroundReplacerPopupWrapper') === null)) {
  528. if (!refresh) {
  529. insertHeaderGui(document.querySelector('[class^=header_context__]'), guiHTMLHeader)
  530. const homepageInput = document.querySelector("#homepageInput");
  531. homepageInput.addEventListener("keyup", e => {
  532. validate(e, true);
  533. });
  534. const otherpagesInput = document.querySelector("#otherpagesInput");
  535. otherpagesInput.addEventListener("keyup", e => {
  536. validate(e, false);
  537. });
  538. }
  539. const homePageImagesDiv = document.querySelector('#homePageImages');
  540. if (homePageImagesDiv) {
  541. // Loop through images and display them
  542. for (let i = 0; i < homePageImageList.length; i++) {
  543. displayImage(homePageImageList[i], homePageImagesDiv,homePageImageList, "homepageImages");
  544. }
  545. }
  546. const otherPagesImagesDiv = document.querySelector("#otherPagesImages");
  547. if (otherPagesImagesDiv) {
  548. // Loop through images and display them
  549. for (let i = 0; i < otherImages.length; i++) {
  550. displayImage(otherImages[i], otherPagesImagesDiv, otherImages, "otherImages");
  551. }
  552. }
  553. }
  554. }
  555.  
  556. const updateImage = (refresh=false) => {
  557. // Don't do anything while the page is loading
  558. if (document.querySelector("[class^=page-loading_loading__]")) return;
  559. addPopup();
  560. insertBackground(refresh);
  561. }
  562.  
  563.  
  564.  
  565. new MutationObserver(async (mutations) => {
  566. updateImage()
  567. }).observe(document.body, { subtree: true, childList: true });