GeoGuessr Background Replacer

Replaces the background of the geoguessr pages with your own images

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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://greasyfork.org/users/1011193
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require https://update.greasyfork.org/scripts/460322/1408713/Geoguessr%20Styles%20Scan.js
// ==/UserScript==

// Some code for popup adapted from blink script: https://greasyfork.org/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 });