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.5
  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. .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 extraStyling = () => {
  370. addShadows()
  371. }
  372.  
  373. const insertBackground = (refresh=false) => {
  374. let inGame = false;
  375. let el = document.querySelector("[class^='background_wrapper']");
  376. if (!el) {
  377. inGame = true;
  378. el = document.querySelector("#__next");
  379. if (!el) return;
  380. // Because this element has multiple classes, we need to use a different selector
  381. const def = document.querySelector("[class*=in-game_backgroundDefault__]");
  382. let reg = /^in-game_backgroundDefault__/;
  383. if (def) {
  384. def.classList = Array.from(def.classList).filter(cl => !cl.match(reg));
  385. }
  386. const partyRoot = document.querySelector("[class^=party_root__]");
  387. if (partyRoot) {
  388. partyRoot.style.background = "none";
  389. }
  390. // Without this, you can see the background behind the map in a game summary
  391.  
  392. // Purple color used by geoguessr, with .9 alpha
  393. const purple9 = "rgba(12 12 46 / .9)";
  394. // .7 alpha
  395. const purple7 = "rgba(12 12 46 / .7)";
  396. const gameSummary = document.querySelector("[class^=game-summary_container__");
  397. if (gameSummary) {
  398. gameSummary.style.opacity = "1";
  399. gameSummary.style.backgroundColor = purple9;
  400. }
  401. const header = document.querySelector("[class^=game-summary_playedRoundsHeader__");
  402. if (header) {
  403. header.style.backgroundColor = purple7;
  404. }
  405.  
  406. }
  407. // We only want the zindex = -1 to exist in game settings, on other pages it's detrimental
  408. let img = document.querySelector('.customBackground');
  409. if (refresh) {
  410. img.remove();
  411. img = document.querySelector('.customBackground');
  412. }
  413. if (img) {
  414. if (!inGame) {
  415. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  416. }
  417. // Return if most recent insertion was in same area (homepage vs not)
  418. if (isHomePage == (location.pathname == "/")) {
  419. return;
  420. }
  421. img.remove();
  422. // Update isHomePage
  423. }
  424. if (!img) {
  425. img = document.createElement("img")
  426. img.classList.add("customBackground");
  427. if (inGame) {
  428. img.classList.add("zindex");
  429. } else {
  430. img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
  431. }
  432. }
  433. isHomePage = location.pathname == "/";
  434. if (isHomePage && homePageImgURL) {
  435. img.src = homePageImgURL;
  436. const startPageWrapper = document.querySelector("[class^=startpage_newWrapper__")
  437. startPageWrapper.classList.add('removeBackground');
  438. } else if (!isHomePage && otherPagesImgURL) {
  439. img.src = otherPagesImgURL;
  440. } else {
  441. return
  442. }
  443. el.appendChild(img);
  444. }
  445.  
  446. const updateStorage = (listName, newList) => {
  447. GM_setValue(listName, newList);
  448. }
  449.  
  450. const validate = (e, homepage) => {
  451. const patt = new RegExp(".*.(jpg|png|gif|jpeg|webp|svg|avif)","i");
  452. if (e.key == "Enter") {
  453. if (patt.test(e.target.value)) {
  454. if (homepage) {
  455. let homepageImages = GM_getValue("homepageImages");
  456. homepageImages.push(e.target.value);
  457. if (homepageImages.length == 1) {
  458. homePageImgURL = homepageImages[0];
  459. }
  460. GM_setValue("homepageImages", homepageImages);
  461. homePageImageList = homepageImages
  462. } else {
  463. let otherImagesNew = GM_getValue("otherImages");
  464. otherImagesNew.push(e.target.value);
  465. if (otherImagesNew.length == 1) {
  466. otherPagesImgURL = otherImagesNew[0];
  467. }
  468. GM_setValue("otherImages", otherImagesNew);
  469. otherImages = otherImagesNew;
  470. }
  471. refreshPopup();
  472. e.target.value = "";
  473. } else {
  474. 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");
  475. }
  476. }
  477. }
  478.  
  479. const removeImage = (image, div, list, listName) => {
  480. let result = window.confirm("Are you sure you want to remove this image?");
  481. if (!result) {
  482. return
  483. }
  484. let i = list.indexOf(image);
  485. if (i != -1) {
  486. list.splice(i, 1);
  487. updateStorage(listName, list);
  488. refreshPopup();
  489. if (listName == "otherImages" && !list.includes(image)) {
  490. setOtherImg();
  491. updateImage(true);
  492. }
  493. if (listName == "homepageImages" && !list.includes(image)) {
  494. setHomePageImg();
  495. updateImage(true);
  496. }
  497. }
  498. };
  499.  
  500. // displays an image in the popup
  501. const displayImage = (image, imagesDiv, list, listName) => {
  502. const img = document.createElement("img");
  503. const div = document.createElement("div");
  504. div.className = "backgroundImageWrapper";
  505. const container = document.createElement("div");
  506. container.className = "imageContainer";
  507.  
  508. img.src = image
  509. img.className = "backgroundImage";
  510. div.appendChild(container);
  511. container.appendChild(img);
  512.  
  513. const deleteIcon = document.createElement("img");
  514. deleteIcon.className = "deleteIcon";
  515. deleteIcon.src = "https://www.svgrepo.com/show/493964/delete-1.svg";
  516.  
  517. const deleteButton = document.createElement("button");
  518. deleteButton.className = getStyle("quick-search_searchInputButton__") + " " + "deleteButton";
  519. deleteButton.appendChild(deleteIcon);
  520. deleteButton.addEventListener("click", e => {
  521. removeImage(image, div, list, listName);
  522. });
  523.  
  524. const overlay = document.createElement("div");
  525. overlay.className = "overlay";
  526. const span = document.createElement("span");
  527. isHomePage = location.pathname == "/";
  528. span.innerText = ((isHomePage && listName == "homepageImages")
  529. || (!isHomePage && listName == "otherImages"))
  530. ? "Make current image" : "You're not on a page where this image will display";
  531. overlay.appendChild(span);
  532.  
  533. container.appendChild(overlay);
  534. div.appendChild(deleteButton);
  535.  
  536. imagesDiv.appendChild(div);
  537. container.addEventListener('click', function() {
  538. if (listName == "homepageImages") {
  539. setHomePageImg(image);
  540. }
  541. if (listName == "otherImages") {
  542. setOtherImg(image);
  543. }
  544. insertBackground(true);
  545. });
  546. }
  547.  
  548. const refreshPopup = () => {
  549. if (document.querySelector("#backgroundReplacerPopupWrapper") != null && document.querySelector('[class^=header-tablet-desktop_root__]') != null) {
  550. let div = document.querySelector("#homePageImages");
  551. while (div.children.length) {
  552. div.removeChild(div.children[0]);
  553. }
  554. div = document.querySelector("#otherPagesImages");
  555. while (div.children.length) {
  556. div.removeChild(div.children[0]);
  557. }
  558. addPopup(true);
  559. const showButton = document.querySelector('#backgroundReplacerToggle');
  560. const popup = document.querySelector('#backgroundReplacerPopup');
  561. showPopup(showButton, popup);
  562. }
  563. }
  564.  
  565. const addPopup = (refresh=false) => {
  566. if (refresh || (document.querySelector('[class^=header-tablet-desktop_root__]') && document.querySelector('#backgroundReplacerPopupWrapper') === null)) {
  567. console.log(refresh)
  568. if (!refresh) {
  569. insertHeaderGui(document.querySelector('[class^=header-tablet-desktop_desktopSectionRight__]'), guiHTMLHeader)
  570. const homepageInput = document.querySelector("#homepageInput");
  571. homepageInput.addEventListener("keyup", e => {
  572. validate(e, true);
  573. });
  574. const otherpagesInput = document.querySelector("#otherpagesInput");
  575. otherpagesInput.addEventListener("keyup", e => {
  576. validate(e, false);
  577. });
  578. }
  579. const homePageImagesDiv = document.querySelector('#homePageImages');
  580. if (homePageImagesDiv) {
  581. // Loop through images and display them
  582. for (let i = 0; i < homePageImageList.length; i++) {
  583. displayImage(homePageImageList[i], homePageImagesDiv,homePageImageList, "homepageImages");
  584. }
  585. }
  586. const otherPagesImagesDiv = document.querySelector("#otherPagesImages");
  587. if (otherPagesImagesDiv) {
  588. // Loop through images and display them
  589. for (let i = 0; i < otherImages.length; i++) {
  590. displayImage(otherImages[i], otherPagesImagesDiv, otherImages, "otherImages");
  591. }
  592. }
  593. }
  594. }
  595.  
  596. const updateImage = (refresh=false) => {
  597. // Don't do anything while the page is loading
  598. if (document.querySelector("[class^=page-loading_loading__]")) return;
  599. addPopup();
  600. insertBackground(refresh);
  601. extraStyling()
  602. }
  603.  
  604.  
  605.  
  606. new MutationObserver(async (mutations) => {
  607. updateImage()
  608. }).observe(document.body, { subtree: true, childList: true });