GeoGuessr Background Replacer

Replaces the background of the geoguessr pages with your own images

// ==UserScript==
// @name         GeoGuessr Background Replacer
// @description  Replaces the background of the geoguessr pages with your own images
// @version      2.1.15
// @author       Tyow#3742
// @match        *://*.geoguessr.com/*
// @license      MIT
// @require      https://unpkg.com/@popperjs/[email protected]/dist/umd/popper.min.js
// @namespace    https://gf.qytechs.cn/users/1011193
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require https://update.gf.qytechs.cn/scripts/460322/1408713/Geoguessr%20Styles%20Scan.js
// ==/UserScript==

// Some code for popup adapted from blink script: https://gf.qytechs.cn/en/scripts/438579-geoguessr-blink-mode

/* ############################################################################### */
/* ##### DON'T MODIFY ANYTHING BELOW HERE UNLESS YOU KNOW WHAT YOU ARE DOING ##### */
/* ############################################################################### */

const guiHTMLHeader = `
<div id="backgroundReplacerPopupWrapper">
  <div id="backgroundReplacerSearchWrapper">
      <div id="backgroundReplacerInputWrapper">
        <div id="backgroundReplacerPopup" style="background: rgba(26, 26, 46, 0.9); padding: 15px; border-radius: 10px; max-height: 80vh; overflow-y: auto; width: 28em">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
            <span id="backgroundReplacerLabel1" style="margin: 0; padding-right: 6px;">Add Home Page image</span>
            <input type="url" id="homepageInput" name="homepage" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
          </div>
          <span>Home Page Images:</span>
          <div id="homePageImages" style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px; flex-direction: column"></div>
          <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
            <span id="backgroundReplacerLabel2" style="margin: 0; padding-right: 6px;">Add Other Page Image</span>
            <input type="url" id="otherpagesInput" name="otherpages" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
          </div>
          <span>Other Pages Images:</span>
          <div id="otherPagesImages" style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px; flex-direction: column"></div>
        </div>
        <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>
    </div>
  </div>
</div>
`
let homePageImageList = GM_getValue("homepageImages");
let otherImages = GM_getValue("otherImages");

// Defaults
if (homePageImageList == undefined) {
    homePageImageList = [
        "https://cdn.wallpapersafari.com/6/80/9ZbpYo.jpg",
        "https://cdn.wallpapersafari.com/25/72/dtkc16.jpg",
        "https://i.imgur.com/l9K9IOq.jpg",
    ];
    GM_setValue("homepageImages", homePageImageList);
}
if (otherImages == undefined) {
    otherImages = [
        "https://imgur.com/eK23SeH.jpg",
        "https://i.imgur.com/l9K9IOq.jpg"
    ];
    GM_setValue("otherImages", otherImages);
}

let hide = false;
let styles = GM_getValue("backgroundReplacerStyles");
if (!styles) {
    hide = true;
    styles = {};
}

let homePageImgURL;

//console.log(cn("label_variantWhite__"))

const setHomePageImg = (img = false) => {
    if (img) {
        homePageImgURL = img;
    } else if(homePageImageList.length) {
        homePageImgURL = homePageImageList[Math.floor((Math.random()*homePageImageList.length))];
    } else {
        homePageImgURL = "";
    }
//    console.log(homePageImgURL);
}

setHomePageImg();

let otherPagesImgURL;

const setOtherImg = (img = false) => {
    if (img) {
        otherPagesImgURL = img;
    } else if(otherImages.length) {
        otherPagesImgURL = otherImages[Math.floor((Math.random()*otherImages.length))];
    } else {
        otherPagesImgURL = "";
    }
}

setOtherImg();

let css = `.customBackground { bottom: 0;
display: block;
height: 100%;
object-fit: cover;
pointer-events: none;
position: fixed;
right: 0;
transition: .2s ease-in-out;
width: 100%;
z-index: -1;
}
.zindex {
  z-index: -1;
}
.deleteIcon {
    width: 25px;
    filter: brightness(0) invert(1);
    opacity: 60%;
}
.backgroundImage {
    width: 20em;
}
.deleteButton {
    width: 59.19px;
    margin-bottom: 8em;
}
.backgroundImageWrapper {
    display: flex;
    padding: .5em;
}
.backgroundImageWrapper {
    position: relative;
    display: flex; /* To lay out the imageContainer and button side by side */
    align-items: center; /* Vertically center align the contents */
}

.imageContainer {
    position: relative;
    width: /* e.g., 300px; */;
    height: /* e.g., 200px; */;
    overflow: hidden;
    cursor: pointer; /* This line changes the cursor */
}

.backgroundImage {
    width: 100%;
    height: 100%;
    transition: opacity 0.3s ease;
    vertical-align: bottom;
}

.overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.7);
    display: flex;
    justify-content: center;
    align-items: center;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.imageContainer:hover .backgroundImage {
    opacity: 0.3;
}

.imageContainer:hover .overlay {
    opacity: 1;
}

.overlay span {
    color: white;
    font-size: 18px;
}

/* Add some margin if you want space between the image and the button */
.backgroundImageWrapper button {
    margin-left: 10px; /* adjust as needed */
}


/* You can style the text within the overlay here */
.overlay span {
    color: white;
    font-size: 18px;
    text-align: center;
}
.deleteIconPicture {
   justifyContent:center;
}
.removeBackground {
   display: none
}

.shadow {
   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);
   background: rgba(0, 0, 0, 0.35);
}

.blurBefore::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.35);
  filter: blur(10px);
  z-index: -1;
}

.highscoreModification {
  border-radius: .5rem;
  background: var(--ds-color-white-10);
  backdrop-filter: blur(11px);
  padding: .5rem;
}

.mapInfoModification {
  padding: 1rem;
  border-radius: .5rem;
  background: var(--ds-color-white-10);
  backdrop-filter: blur(12px);
}

.textShadow {
    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
}
.cardModification {
    backdrop-filter: blur(12px);
}
.mapSelectorModification {
  background: var(--ds-color-white-10);
  backdrop-filter: blur(12px);
}
.highScoreTitleModification {
  background: var(--ds-color-white-10);
  backdrop-filter: blur(12px);
  border-radius: .5rem;
  padding: .5rem;
}
`;
GM_addStyle(css);


const showPopup = (showButton, popup) => {
    popup.style.display = 'block';
    Popper.createPopper(showButton, popup, {
        placement: 'bottom',
        modifiers: [
            {
                name: 'offset',
                options: {
                    offset: [0, 10],
                },
            },
        ],
    });
}

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

const iterativeSetTimeout = async (func, initDelay, cond) => {
    while (!cond()) {
        await delay(initDelay);
        await func();
        initDelay *= 2;
        stylesUsed.forEach(style => {
            styles[style] = cn(style);
        });
    }
};

// Caching system for styles
// Basically, we have a browser stored styles object,
// which contains the most recent classNames found by scanStyles()
// This is what the script will immediately use upon loading,
// so that there's no pause in delivering the UI to the user
// But the script will also fire off this function
// which will use the above iterativeSetTimeout function to call scanStyles
// This is so there aren't a thousand calls in quick succession.
// Once all the classNames we're looking for are found,
// it will update the local storage and the ui with the (possibly) new classnames
const stylesUsed = [
    "quick-search_wrapper__",
    "quick-search_searchInputWrapper__",
    "quick-search_searchInputButton__",
    "quick-search_iconSection__",
];

const uploadDownloadStyles = async () => {
    stylesUsed.forEach(style => {
//        styles[style] = cn(style);
//        console.log(style);
  //      console.log(cn(style));
    });
    await iterativeSetTimeout(scanStyles, 0.1, () => checkAllStylesFound(stylesUsed) !== undefined);
    if (hide) {
        document.querySelector("#backgroundReplacerPopupWrapper").hidden = "";
    }
    stylesUsed.forEach(style => {
        styles[style] = cn(style);
//        console.log(style);
  //      console.log(cn(style));
    });
    setStyles();
    GM_setValue("backgroundReplacerStyles", styles);
}

const getStyle = style => {
    return styles[style];
}

const setStyles = () => {
    try {
        document.querySelector("#backgroundReplacerSearchWrapper").className = getStyle("quick-search_wrapper__");
        document.querySelector("#backgroundReplacerInputWrapper").className = getStyle("quick-search_searchInputWrapper__");
        document.querySelector("#backgroundReplacerToggle").className = getStyle("quick-search_searchInputButton__");
        document.querySelector("#backgroundReplacerLabel1").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
        document.querySelector("#backgroundReplacerLabel2").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
        document.querySelector("#backgroundReplacerTogglePicture").className = getStyle("quick-search_iconSection__");
        document.querySelectorAll(".deleteButton").forEach(el => el.className = el.className + " " + getStyle("quick-search_searchInputButton__"));
    } catch (err) {
        console.error(err);
    }
}


const insertHeaderGui = async (header, gui) => {

    header.insertAdjacentHTML('afterbegin', gui);

    // Resolve class names
    if (hide) {
        document.querySelector("#backgroundReplacerPopupWrapper").hidden = "true"
    }

    scanStyles().then(() => uploadDownloadStyles());
    setStyles();



    const showButton = document.querySelector('#backgroundReplacerToggle');
    const popup = document.querySelector('#backgroundReplacerPopup');
    popup.style.display = 'none';

    document.addEventListener('click', (e) => {
        const target = e.target;
        if (target == popup || popup.contains(target) || !document.contains(target)) return;
        if (target.matches('#backgroundReplacerToggle, #backgroundReplacerToggle *')) {
            e.preventDefault();
            showPopup(showButton, popup);
        } else {
            popup.style.display = 'none';
        }
    });
}

// Global to track whether the most recent image insertion was done on homepage
let isHomePage = location.pathname == "/";

const addShadows = () => {
    const mapSelector = document.querySelector("[class^='map-selector_selector__'")
    if (mapSelector && !mapSelector.classList.contains("mapSelectorModification")) {
        mapSelector.classList.add("mapSelectorModification")
        // mapSelector.classList.add("blurBefore")
    }
    const highscoreRoot = document.querySelector("[class^='map-highscore_root__'")
    if (highscoreRoot && !highscoreRoot.classList.contains("highscoreModification")) {
        highscoreRoot.classList.add("highscoreModification")
    }
    const mapStatsText = document.querySelectorAll("[class^='map-stats_mapStatMetricValue__'")
    if (mapStatsText) {
        for (const el of mapStatsText) {
           if (!el.classList.contains("textShadow")) {
               el.classList.add("textShadow")
           }
        }
    }
    const mapInfo = document.querySelector("[class^='map-block_mapInfo__'")
    if (mapInfo && !mapInfo.classList.contains("mapInfoModification")) {
        mapInfo.classList.add("mapInfoModification")
    }
    const howItWorks = document.querySelector("[class^='ranked-system-how-it-works-page_root__'")
    if (howItWorks && !howItWorks.classList.contains("mapInfoModification")) {
        howItWorks.classList.add("mapInfoModification")
    }
    const userStatsCards = document.querySelectorAll("[class^='user-stats-overview_card__'")
    if (userStatsCards) {
        for (const el of userStatsCards) {
           if (!el.classList.contains("cardModification")) {
               el.classList.add("cardModification")
           }
        }
    }
    const mapStats = document.querySelectorAll("[class^='map-stats_mapStat__'")
    if (mapStats) {
        for (const el of mapStats) {
           if (!el.classList.contains("cardModification")) {
               el.classList.add("cardModification")
           }
        }
    }
    const cards = document.querySelectorAll("[class^='card_card__'")
    if (cards) {
        for (const el of cards) {
           if (!el.classList.contains("cardModification")) {
               el.classList.add("cardModification")
           }
        }
    }
    // const highScoreTitle = document.querySelectorAll("[class^='headline_heading__'")
    // for (const el of highScoreTitle) {
    //     if (el.textContent == "Highscore" && !el.classList.contains("highScoreTitleModification")) {
    //         el.classList.add("highScoreTitleModification")
    //     }
    // }
    const mapsCenter = document.querySelector("[class^='maps_center__'")
    if (mapsCenter && !mapsCenter.classList.contains("mapsCenterMoved")) {
        const mapsSwitch = document.querySelector("[class^='map-highscore_switchContainer__'")
        highscoreRoot.insertBefore(mapsCenter, mapsSwitch);
        mapsCenter.classList.add("mapsCenterMoved")
    }
}

const homepageModification = () => {
    const startPageWrapper = document.querySelector("[class^=startpage_background__")
    if (startPageWrapper && !startPageWrapper.classList.contains("removeBackground")) {
        startPageWrapper.classList.add('removeBackground');
    }
    const startPageNewWrapper = document.querySelector("[class^=startpage_newWrapper__")
    if (startPageNewWrapper && startPageNewWrapper.style.background != "none") {
        startPageNewWrapper.style.background = "none"
    };
}

const otherPageModification = () => {
    const backgroundWrapper = document.querySelector("[class^=background_background__")
    if (backgroundWrapper && !backgroundWrapper.classList.contains("removeBackground")) {
        backgroundWrapper.classList.add('removeBackground');
    }
}

const extraStyling = () => {
    addShadows()
    homepageModification()
    otherPageModification()
}

const insertBackground = (refresh=false) => {
    let inGame = false;
    let el = document.querySelector("[class^='background_wrapper']");
    if (!el) {
        inGame = true;
        el = document.querySelector("#__next");
        if (!el) return;
        // Because this element has multiple classes, we need to use a different selector
        const def = document.querySelector("[class*=in-game_backgroundDefault__]");
        let reg = /^in-game_backgroundDefault__/;
        if (def) {
            def.classList = Array.from(def.classList).filter(cl => !cl.match(reg));
        }
        const partyRoot = document.querySelector("[class^=party_root__]");
        if (partyRoot) {
            partyRoot.style.background = "none";
        }
        // Without this, you can see the background behind the map in a game summary

        // Purple color used by geoguessr, with .9 alpha
        const purple9 = "rgba(12 12 46 / .9)";
        // .7 alpha
        const purple7 = "rgba(12 12 46 / .7)";
        const gameSummary = document.querySelector("[class^=game-summary_container__");
        if (gameSummary) {
            gameSummary.style.opacity = "1";
            gameSummary.style.backgroundColor = purple9;
        }
        const header = document.querySelector("[class^=game-summary_playedRoundsHeader__");
        if (header) {
            header.style.backgroundColor = purple7;
        }

    }
    // We only want the zindex = -1 to exist in game settings, on other pages it's detrimental
    let img = document.querySelector('.customBackground');
    if (refresh) {
        img.remove();
        img = document.querySelector('.customBackground');
    }
    if (img) {
        if (!inGame) {
            img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
        }
        // Return if most recent insertion was in same area (homepage vs not)
        if (isHomePage == (location.pathname == "/")) {
            return;
        }
        img.remove();
        // Update isHomePage
    }
    if (!img) {
        img = document.createElement("img")
        img.classList.add("customBackground");
        if (inGame) {
            img.classList.add("zindex");
        } else {
            img.classList = Array.from(img.classList).filter(cl => cl != 'zindex');
        }
    }
    isHomePage = location.pathname == "/";
    if (isHomePage && homePageImgURL) {
        img.src = homePageImgURL;
    } else if (!isHomePage && otherPagesImgURL) {
        img.src = otherPagesImgURL;
    } else {
        return
    }
    el.appendChild(img);
}

const updateStorage = (listName, newList) => {
    GM_setValue(listName, newList);
}

const validate = (e, homepage) => {
    const patt = new RegExp(".*.(jpg|png|gif|jpeg|webp|svg|avif)","i");
    if (e.key == "Enter") {
        if (patt.test(e.target.value)) {
            if (homepage) {
                let homepageImages = GM_getValue("homepageImages");
                homepageImages.push(e.target.value);
                if (homepageImages.length == 1) {
                    homePageImgURL = homepageImages[0];
                }
                GM_setValue("homepageImages", homepageImages);
                homePageImageList = homepageImages
            } else {
                let otherImagesNew = GM_getValue("otherImages");
                otherImagesNew.push(e.target.value);
                if (otherImagesNew.length == 1) {
                    otherPagesImgURL = otherImagesNew[0];
                }
                GM_setValue("otherImages", otherImagesNew);
                otherImages = otherImagesNew;
            }
            refreshPopup();
            e.target.value = "";
        } else {
            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");
        }
    }
}

const removeImage = (image, div, list, listName) => {
    let result = window.confirm("Are you sure you want to remove this image?");
    if (!result) {
        return
    }
    let i = list.indexOf(image);
    if (i != -1) {
        list.splice(i, 1);
        updateStorage(listName, list);
        refreshPopup();
        if (listName == "otherImages" && !list.includes(image)) {
            setOtherImg();
            updateImage(true);
        }
        if (listName == "homepageImages" && !list.includes(image)) {
            setHomePageImg();
            updateImage(true);
        }
    }
};

// displays an image in the popup
const displayImage = (image, imagesDiv, list, listName) => {
    const img = document.createElement("img");
    const div = document.createElement("div");
    div.className = "backgroundImageWrapper";
    const container = document.createElement("div");
    container.className = "imageContainer";

    img.src = image
    img.className = "backgroundImage";
    div.appendChild(container);
    container.appendChild(img);

    const deleteIcon = document.createElement("img");
    deleteIcon.className = "deleteIcon";
    deleteIcon.src = "https://www.svgrepo.com/show/493964/delete-1.svg";

    const deleteButton = document.createElement("button");
    deleteButton.className = getStyle("quick-search_searchInputButton__") + " " + "deleteButton";
    deleteButton.appendChild(deleteIcon);
    deleteButton.addEventListener("click", e => {
        removeImage(image, div, list, listName);
    });

    const overlay = document.createElement("div");
    overlay.className = "overlay";
    const span = document.createElement("span");
    isHomePage = location.pathname == "/";
    span.innerText = ((isHomePage && listName == "homepageImages")
                      || (!isHomePage && listName == "otherImages"))
                      ? "Make current image" : "You're not on a page where this image will display";
    overlay.appendChild(span);

    container.appendChild(overlay);
    div.appendChild(deleteButton);

    imagesDiv.appendChild(div);
    container.addEventListener('click', function() {
        if (listName == "homepageImages") {
            setHomePageImg(image);
        }
        if (listName == "otherImages") {
            setOtherImg(image);
        }
        insertBackground(true);
    });
}

const refreshPopup = () => {
    if (document.querySelector("#backgroundReplacerPopupWrapper") != null && document.querySelector('[class^=header-tablet-desktop_root__]') != null) {
        let div = document.querySelector("#homePageImages");
        while (div.children.length) {
            div.removeChild(div.children[0]);
        }
        div = document.querySelector("#otherPagesImages");
        while (div.children.length) {
            div.removeChild(div.children[0]);
        }
        addPopup(true);
        const showButton = document.querySelector('#backgroundReplacerToggle');
        const popup = document.querySelector('#backgroundReplacerPopup');
        showPopup(showButton, popup);
    }
}

const addPopup = (refresh=false) => {
    if ((refresh || (document.querySelector('[class^=header-tablet-desktop_root__]') || document.querySelector('[class^=header-desktop_root__]')) && document.querySelector('#backgroundReplacerPopupWrapper') === null)) {
        if (!refresh) {
            let section = document.querySelector('[class^=header-tablet-desktop_desktopSectionRight__]')
            if (!section) section = document.querySelector('[class^=header-desktop_desktopSectionRight__]')
            insertHeaderGui(section, guiHTMLHeader)
            const homepageInput = document.querySelector("#homepageInput");
            homepageInput.addEventListener("keyup", e => {
                validate(e, true);
            });
            const otherpagesInput = document.querySelector("#otherpagesInput");
            otherpagesInput.addEventListener("keyup", e => {
                validate(e, false);
            });
        }
        const homePageImagesDiv = document.querySelector('#homePageImages');
        if (homePageImagesDiv) {
            // Loop through images and display them
            for (let i = 0; i < homePageImageList.length; i++) {
                displayImage(homePageImageList[i], homePageImagesDiv,homePageImageList, "homepageImages");
            }
        }
        const otherPagesImagesDiv = document.querySelector("#otherPagesImages");
        if (otherPagesImagesDiv) {
            // Loop through images and display them
            for (let i = 0; i < otherImages.length; i++) {
                displayImage(otherImages[i], otherPagesImagesDiv, otherImages, "otherImages");
            }
        }
    }
}

const updateImage = (refresh=false) => {
    // Don't do anything while the page is loading
    if (document.querySelector("[class^=page-loading_loading__]")) return;
    addPopup();
    insertBackground(refresh);
    extraStyling()
}



new MutationObserver(async (mutations) => {
    updateImage()
}).observe(document.body, { subtree: true, childList: true });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址