// ==UserScript==
// @name A Better Search
// @namespace http://tampermonkey.net/
// @version 0.1.0
// @description My attempt to improve the search function in geoguessr.
// @author Lemson
// @match https://www.geoguessr.com/
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license MIT
// @grant GM_addStyle
// ==/UserScript==
const CSS = `
.top-search-bar{
width: 30%;
margin-left: 34%;
position: absolute;
border-radius: 2rem;
padding-left: 1rem;
font-size: 1rem;
background-color: rgba(0,0,0,.8);
color: white;
border: 1px solid white;
scale: 1;
}
.inactive-search-bar{
scale: 0;
}
.modal {
width: 70%;
height: 70%;
background-color: #d9d9d9;
border-radius: 2.5rem;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
scale: 0;
transition: 0.1s;
box-shadow: 0 0 5rem 1rem rgb(0, 0, 0);
display: flex;
z-index:5;
}
.active {
transition: 0.1s;
scale: 1;
}
.sidebar {
height: 100%;
width: 4rem;
background-color: #545454;
border-top-left-radius: 2.5rem;
border-bottom-left-radius: 2.5rem;
border-right: 1px solid black;
}
.maincontent {
display: flex;
flex-direction: column;
width: 100%;
}
.search {
height: 2.5rem;
margin: 2rem;
display: flex;
justify-content: space-between;
}
.searchbar {
width: 50%;
border-radius: 1.1rem;
border: 0.1rem solid black;
padding-left: 1rem;
box-shadow: 0 0 7rem black;
font-size: 1rem;
}
.filter-btn {
height: 100%;
width: 2.5rem;
border-radius: 0.4rem;
border: 1px solid black;
background-color: rgb(192, 192, 192);
}
.filter-btn:active {
background-color: rgb(145, 145, 145);
}
.filter-window{
width: 40%;
height: 70%;
border: .2rem solid black;
transform: translateX(52%);
z-index: 1000;
}
.filter-window-content{
width:100%;
height: 100%;
display: flex;
flex-direction: row;
}
.filter-window-content>*{
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
}
.filter-header{
display: flex;
justify-content: space-between;
}
.apply-filter-button{
width: 4rem;
height: 2rem;
border: 1px solid red;
}
.apply-filter-button:active{
background-color: rgba(0,0,0,.5);
}
.filter-close-button:hover{
transition: 0.1s;
scale: 1.1;
cursor: pointer;
}
.close {
margin-left: 5rem;
font-size: 2rem;
cursor: pointer;
}
.close:hover{
transition: .2s;
scale: 1.1;
}
.windows {
width: 95%;
height: 80%;
margin-left: 2rem;
border: 1px solid black;
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.results {
background-color: rgba(0, 0, 0, 0.112);
width: 100%;
height: 100%;
overflow: auto;
overflow-x: hidden;
}
.selection-info {
background-color: rgba(0, 0, 0, 0.112);
border-left: 0.1rem solid black;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
overflow-x: hidden;
}
.result-container {
width: 99%;
border: 1px solid rgb(0, 0, 0);
margin: 0.2rem;
display: flex;
}
.result-container:hover {
background-color: rgba(0, 0, 0, 0.107);
}
.result-container:active {
background-color: rgba(0, 0, 0, 0.25);
}
.result-title-author {
width: 90%;
}
.result-title {
font-size: 1.6rem;
}
.result-creator {
font-size: 1rem;
}
.author-space {
text-decoration: underline;
color: rgb(15, 0, 88);
}
.likesdisplay {
width: 10%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: red;
font-size: 1.2rem;
-webkit-text-stroke: .5px black;
}
.selected {
background-color: rgba(0, 0, 255, 0.1);
}
.selected-header {
display: flex;
flex-direction: row;
padding-left: 1rem;
border-bottom: .1rem solid black;
justify-content: space-between;
padding-right: 1rem;
}
.header-likes {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.selected-title {
font-size: 1.4rem;
}
.selected-creator {
font-size: .8rem;
font-weight: bold;
color: black;
}
.selected-creator:hover{
color: blue;
}
.selected-desc {
margin: .5rem;
border: 1px solid black;
padding: .3rem;
border-radius: .5rem;
height: 3.5rem;
overflow: auto;
}
.tags {
display: flex;
flex-direction: row;
gap: .7rem;
margin-left: .5rem;
flex-wrap: wrap;
row-gap: 0.5rem;
}
.searchAndFilter{
width: 100%;
}
.tag{
border: 1px solid black;
padding: .2rem;
padding-left: .5rem;
padding-right: .5rem;
border-radius: 2rem;
height: .9rem;
font-size: .8rem;
}
.game {
position: relative;
width: 100%;
height: 100%;
margin: 0.5rem;
margin-top: 1.5rem;
border-radius: 1rem;
border: 1px solid black;
overflow: auto;
display: flex; /* Use flexbox layout */
}
.game:before {
content: "";
background-image: url('https://img.freepik.com/free-photo/planet-earth-background_23-2150564685.jpg');
background-size: cover;
background-position: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -10;
overflow: hidden;
}
.game-settings {
width: 50%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.ol-selected-leaderboard{
width: 50%;
height: 100%;
}
::-webkit-scrollbar {
width: 0.1rem;
}
::-webkit-scrollbar-thumb {
background-color: rgb(0, 0, 0);
}
.game-mode-select{
color: white;
display: flex;
align-items: center;
flex-direction: column;
margin-top: .6rem;
width: 100%;
justify-content: center;
margin-top: .7vh;
}
.game-mode-select>h1{
font-weight: 500;
letter-spacing: .06vw;
margin-bottom: .4vh;
font-size: 1.6vh;
}
.gamemode-buttons{
display: flex;
flex-direction: row;
justify-content: space-evenly;
background-color: rgba(255, 255, 255, 0.06);
border: .1rem solid white;
border-radius: .4rem;
width: 95%;
text-wrap: nowrap;
}
.gamemode-buttons>*{
font-size: 1.2rem;
color: white;
padding-left: 1rem;
padding-right: 1rem;
border-radius: .5rem;
margin: .2rem;
font-style: italic;
}
.selected-mode {
background-color: #7950E5;
}
.round-select{
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-top: 1.2rem;
width: 100%;
}
.round-select>h1{
font-size: 1.3rem;
font-weight: 500;
letter-spacing: .1rem;
margin-bottom: .3rem;
}
.round-numbers{
display: flex;
flex-direction: row;
justify-content: space-evenly;
background-color: rgba(255, 255, 255, 0.06);
border: .1rem solid white;
border-radius: .4rem;
width: 95%;
height: 3rem;
}
.round-numbers>*{
color: white;
width: 3.5rem;
margin-top: .2rem;
margin-bottom: .2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: .5rem;
}
.round-time{
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-top: 1.2rem;
width: 95%;
background-color: rgba(0,0,0,0.7);;
border: .1rem solid white;
border-radius: .5rem;
padding-top: 1rem;
padding-bottom: 1rem;
}
.round-time>h1, .round-time>h2{
font-size: 1.3rem;
font-weight: 500;
letter-spacing: .1rem;
margin-bottom: .3rem;
}
.time-slider{
width: 90%;
padding:0;
}
.start-game-btn{
width: 95%;
height: 4rem;
background-color: #97E851;
border-radius: 2rem;
box-shadow: inset 0 1px 10px white;
font-size: 2rem;
font-weight: bold;
font-style: italic;
color: white;
-webkit-text-stroke: 1px black;
margin-top: 1rem;
border: .1rem solid white;
}
.like-filter, .loc-filter, .avg-score-filter, .games-played-filter{
margin-top:1rem;
}
.filter-title{
font-size:1rem;
}
.apply-filter-button{
border: 1px solid black;
}
`;
GM_addStyle(CSS);
const HTML = `
<div id="overlay-main" class="modal">
<div class="sidebar"></div>
<div class="maincontent">
<div class="search">
<div class="searchAndFilter">
<input type="text" placeholder="Search..." class="searchbar" id="search-term" />
<button class="filter-btn">Filter</button>
<dialog class="filter-window">
<header class="filter-header">
<h1>Filters</h1>
<h2 class="filter-close-button">✖</h2>
</header>
<div class="filter-window-content">
<div class="variable-filters">
<div class="like-filter">
<h1 class="filter-title">Minimum likes</h1>
<div>
<input type="checkbox" name="toggle-min-likes" id="toggle-min-likes" />
<input type="number" name="num-min-likes" id="num-min-likes" placeholder="Minimum likes..." />
</div>
</div>
<div class="loc-filter">
<h1 class="filter-title">Minimum locations</h1>
<div>
<input type="checkbox" name="toggle-min-loc" id="toggle-min-loc" />
<input type="number" name="num-min-loc" id="num-min-loc" placeholder="Minimum location..." />
</div>
</div>
<div class="avg-score-filter">
<h1 class="filter-title">Minimum average score</h1>
<div>
<input type="checkbox" name="toggle-min-avg-score" id="toggle-min-avg-score" />
<input
type="number"
name="num-min-avg-score"
id="num-min-avg-score"
placeholder="Minimum average score..."
/>
</div>
</div>
<div class="games-played-filter">
<h1 class="filter-title">Minimum games played</h1>
<div>
<input type="checkbox" name="toggle-min-games-played" id="toggle-min-games-played" />
<input
type="number"
name="num-min-games-played"
id="num-min-games-played"
placeholder="Minimum games played..."
/>
</div>
</div>
</div>
<div class="toggle-filters">
<button class="apply-filter-button">Apply</button>
</div>
</div>
</dialog>
</div>
<button class="close">✖</button>
</div>
<div class="windows">
<div class="results"></div>
<div class="selection-info"></div>
</div>
</div>
</div>
`;
let div = document.createElement("div");
div.innerHTML = HTML;
document.body.append(div);
const startGameBaseUrl = "https://www.geoguessr.com/api/v3/games";
let searchBarParent = document.querySelector("[class^='header_logoWrapper__']");
searchBarParent.style = "justify-content: space-between;";
let topSearchBar = document.createElement("input");
topSearchBar.placeholder = "Search for a map...";
topSearchBar.classList.add("top-search-bar");
topSearchBar.id = "search-term";
searchBarParent.appendChild(topSearchBar);
const everything = document.querySelector(".modal");
const resultsBox = document.querySelector(".results");
const infoBox = document.querySelector(".selection-info");
const overlay = document.querySelector(".modal");
const closeButton = document.querySelector(".close");
const searchBar = document.querySelector(".searchbar");
let chosenMode = "no move";
let roundAmount = 5;
let roundTimeSeconds = 30;
//Filter stuff \/
const filterPopup = document.querySelector(".filter-window");
const filterButton = document.querySelector(".filter-btn");
const applyFilterBtn = document.querySelector(".apply-filter-button");
const closeFiltersButton = document.querySelector(".filter-close-button");
//Get all checkbox for filters
const minLikesToggle = document.getElementById("toggle-min-likes");
const minLocToggle = document.getElementById("toggle-min-loc");
const minAvgScoreToggle = document.getElementById("toggle-min-avg-score");
const minGamesPlayedToggle = document.getElementById("toggle-min-games-played");
//Get all input boxes for filters
const minLikesInput = document.getElementById("num-min-likes");
const minLocInput = document.getElementById("num-min-loc");
const minAvgScoreInput = document.getElementById("num-min-avg-score");
const minGamesPlayed = document.getElementById("num-min-games-played");
//Set the checkbox to saved value
minLikesToggle.checked = localStorage.getItem("minLikesToggle") === "true";
minLocToggle.checked = localStorage.getItem("minLocToggle") === "true";
minAvgScoreToggle.checked = localStorage.getItem("minAvgScoreToggle") === "true";
minGamesPlayedToggle.checked = localStorage.getItem("minGamesPlayedToggle") === "true";
//Set the value to saved value
minLikesInput.value = localStorage.getItem("minLikeNum");
minLocInput.value = localStorage.getItem("minLocNum");
minAvgScoreInput.value = localStorage.getItem("minAvgScoreNum");
minGamesPlayed.value = localStorage.getItem("minGamesPlayed");
let filterMinLikes = true;
let filterMinLocs = false;
let filterAverageScore = false;
let filterGamesplayed = false;
let filterDifficulty = false;
let filterDesc = false;
let filterOfficial = false;
let filterHandpicked = false;
const saveFilters = () => {
console.log("saving filters...");
//Save checkboxes
localStorage.setItem("minLikesToggle", minLikesToggle.checked);
localStorage.setItem("minLocToggle", minLocToggle.checked);
localStorage.setItem("minAvgScoreToggle", minAvgScoreToggle.checked);
localStorage.setItem("minGamesPlayedToggle", minGamesPlayedToggle.checked);
console.log(minGamesPlayedToggle.checked);
//Save values
localStorage.setItem("minLikeNum", minLikesInput.value);
localStorage.setItem("minLocNum", minLocInput.value);
localStorage.setItem("minAvgScoreNum", minAvgScoreInput.value);
localStorage.setItem("minGamesPlayed", minGamesPlayed.value);
};
const search = async (searchTerm) => {
try {
let resp = await fetch(`https://www.geoguessr.com/api/v3/search/map?page=0&count=50&q=${searchTerm}`);
if (!resp.ok) {
throw new Error(`Error searching for ${searchTerm}`);
}
const data = await resp.json();
// Get additional map data concurrently
const extraMapData = await getAdditionalMapDataConcurrently(data);
// Combine data and extraMapData so the filter can use all the crazy cool stats.
const combinedData = combineDatas(data, extraMapData);
console.log(combinedData);
applyFilters(combinedData);
/* if (activeFilters > 0) {
applyFilters(combinedData);
} else createResults(combinedData); */
} catch (error) {
console.error(error);
}
};
const getAdditionalMapDataConcurrently = async (data) => {
const promises = data.map((map) => fetch(`https://www.geoguessr.com/api/maps/${map.id}`));
const responses = await Promise.all(promises);
const extraMapData = await Promise.all(responses.map((resp) => resp.json()));
return extraMapData;
};
const combineDatas = (data, extraMapData) => {
const combinedData = [];
const minLength = Math.min(data.length, extraMapData.length);
for (let i = 0; i < minLength; i++) {
combinedData.push({ ...extraMapData[i], ...data[i] });
}
return combinedData;
};
const applyFilters = (searchResults) => {
let filteredResults = searchResults;
if (minLikesToggle.checked) {
filteredResults = filteredResults.filter((result) => {
return result.likes >= minLikesInput.value;
});
}
if (minLocToggle.checked) {
filteredResults = filteredResults.filter((result) => {
return result.coordinateCount >= minLocInput.value;
});
}
if (minAvgScoreToggle.checked) {
filteredResults = filteredResults.filter((result) => {
return result.averageScore >= minAvgScoreInput.value;
});
}
if (minGamesPlayedToggle.checked) {
filteredResults = filteredResults.filter((result) => {
return result.numberOfGamesPlayed >= minGamesPlayed.value;
});
}
createResults(filteredResults);
};
const createResults = (searchData) => {
//removes any HTML, if there is any. So that new HTML can be added.
resultsBox.innerHTML = null;
searchData.forEach((item) => {
//Create and add the HTML for result
let resultContainerHTML = `
<div class="result-container">
<div class="result-title-author">
<h1 class="result-title">${item.name}</h1>
<h2 class="result-creator">created by: <a target="_blank" id="author-link" href="user/${item.creatorId}">${item.creator}</a></h2>
</div>
<div class="likesdisplay">
<div class="heartSVG">♥</div>
<h3 class="like-ount">${item.likes}</h3>
</div>
</div>`;
//Create element to append the created HTML onto
let resultContainerParent = document.createElement("div");
resultContainerParent.innerHTML = resultContainerHTML;
resultsBox.appendChild(resultContainerParent);
//click on result
resultContainerParent.addEventListener("click", function () {
let resultContainer = resultContainerParent.querySelector(".result-container");
let clearSelections = document.querySelectorAll(".result-container");
clearSelections.forEach((thing) => {
//Remove selections
thing.classList.remove("selected");
});
displaySelectedMap(item, resultContainer);
});
});
};
const startGame = (map) => {
let gameSettings;
if (chosenMode == "move") {
gameSettings = {
forbidMoving: false,
forbidRotating: false,
forbidZooming: false,
map: map,
rounds: roundAmount,
timeLimit: roundTimeSeconds,
};
}
if (chosenMode == "no move") {
gameSettings = {
forbidMoving: true,
forbidRotating: false,
forbidZooming: false,
map: map,
rounds: roundAmount,
timeLimit: roundTimeSeconds,
};
}
if (chosenMode == "nmpz") {
gameSettings = {
forbidMoving: true,
forbidRotating: true,
forbidZooming: true,
map: map,
rounds: roundAmount,
timeLimit: roundTimeSeconds,
};
}
//start game?
fetch(startGameBaseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(gameSettings),
})
.then((response) => response.json())
.then((data) => {
console.log(`https://www.geoguessr.com/game/${data.token}`);
return data;
})
.then((data) => {
window.location.href = `https://www.geoguessr.com/game/${data.token}`;
})
.catch((error) => console.error("error", error));
};
const setMode = (mode, buttons, button) => {
buttons.forEach((a) => {
a.classList.remove("selected-mode");
});
switch (mode) {
case "MOVE":
chosenMode = "move";
button.classList.add("selected-mode");
break;
case "NO MOVE":
chosenMode = "no move";
button.classList.add("selected-mode");
break;
case "NMPZ":
chosenMode = "nmpz";
button.classList.add("selected-mode");
break;
}
console.log(mode);
};
const setRounds = (roundNum, nums, selectedNum) => {
nums.forEach((a) => {
a.classList.remove("selected-mode");
});
switch (roundNum) {
case "1":
roundAmount = 1;
selectedNum.classList.add("selected-mode");
break;
case "2":
roundAmount = 2;
selectedNum.classList.add("selected-mode");
break;
case "3":
roundAmount = 3;
selectedNum.classList.add("selected-mode");
break;
case "4":
roundAmount = 4;
selectedNum.classList.add("selected-mode");
break;
case "5":
roundAmount = 5;
selectedNum.classList.add("selected-mode");
break;
}
console.log(roundNum);
};
const displaySelectedMap = (item, resultContainer) => {
resultContainer.classList.add("selected");
//clear the infobox
infoBox.innerHTML = null;
if (item.description == "" || item.description == null) {
item.description = "The creator of this map has not added a description";
}
item.created = item.created.slice(0, 10);
let officialState;
if (!item.isUserMap) {
officialState = "Official Map";
} else {
officialState = "Usermade Map";
}
//yuck
let selectedItemHTML = `
<header class="selected-header">
<div class="header-title-container">
<h1 class="selected-title">${item.name}</h1>
<a class="selected-creator" href="user/${item.creatorId}" target="_blank">${item.creator}</a>
</div>
<div class="header-likes">
<h3 class="header-likes-num">${item.likes}</h3>
<div>♥</div>
</div>
</header>
<div class="selected-info-container">
<div class="selected-desc">${item.description}</div>
<div class="tags">
<div class="tag">
<b>${item.coordinateCount} locations</b>
</div>
<div class="tag">
<b>Created: ${item.created}</b>
</div>
<div class="tag">
<b>${item.numberOfGamesPlayed} games played</b>
</div>
<div class="tag">
<b>${officialState}</b>
</div>
</div>
</div>
<div class="game">
<div class="game-settings">
<div class="game-mode-select">
<h1>Game settings</h1>
<div class="gamemode-buttons">
<button class="mode-button selected-mode">MOVE</button>
<button class="mode-button">NO MOVE</button>
<button class="mode-button">NMPZ</button>
</div>
</div>
<div class="round-select">
<h1>ROUNDS</h1>
<div class="round-numbers">
<button class="round-number ">1</button>
<button class="round-number">2</button>
<button class="round-number">3</button>
<button class="round-number">4</button>
<button class="round-number selected-mode">5</button>
</div>
</div>
<div class="round-time">
<h1>ROUND TIME</h1>
<input type="range" min="0" max="600" step="10" class="time-slider" value="30"/>
<h2 class="time-display">30 seconds</h2>
</div>
<button class="start-game-btn">START GAME!</button>
</div>
<div class="ol-selected-leaderboard"></div>
</div>
`;
infoBox.innerHTML = selectedItemHTML;
const slider = document.querySelector(".time-slider");
const timeDisplay = document.querySelector(".time-display");
slider.addEventListener("input", function () {
if (slider.value < 60) {
timeDisplay.textContent = `${slider.value} seconds`;
} else if (slider.value >= 60) {
let timeMin = Math.floor(slider.value / 60);
let timeSec = slider.value % 60;
if (timeSec == "0") timeDisplay.textContent = `${timeMin} minutes`;
else timeDisplay.textContent = `${timeMin} minutes, ${timeSec} seconds`;
}
roundTimeSeconds = slider.value;
});
const startButton = document.querySelector(".start-game-btn");
const modeButtons = document.querySelectorAll(".mode-button");
const roundNumbers = document.querySelectorAll(".round-number");
modeButtons.forEach((button) => {
button.addEventListener("click", () => setMode(button.textContent, modeButtons, button));
});
roundNumbers.forEach((number) => {
number.addEventListener("click", () => setRounds(number.textContent, roundNumbers, number));
});
//start button, get every piece of data that's needed to start a game on the currently selected settings.
startButton.addEventListener("click", () => {
//item.id is map
startGame(item.id);
});
};
document.addEventListener("keydown", function (event) {
if (document.activeElement == searchBar && event.key === "Enter") {
let searchTerm = searchBar.value;
search(searchTerm);
}
});
closeButton.addEventListener("click", function () {
overlay.classList.remove("active");
});
document.addEventListener("keydown", function (event) {
if (document.activeElement == topSearchBar && event.key === "Enter") {
let searchTerm = topSearchBar.value;
everything.classList.add("active");
search(searchTerm);
}
});
function checkUrl() {
setTimeout(() => {
if (window.location.href != "https://www.geoguessr.com/") {
topSearchBar.classList.add("inactive-search-bar");
} else if (window.location.href == "https://www.geoguessr.com/") {
topSearchBar.classList.remove("inactive-search-bar");
}
}, 200);
}
document.addEventListener("click", checkUrl);
filterButton.addEventListener("click", () => filterPopup.show());
closeFiltersButton.addEventListener("click", () => filterPopup.close());
applyFilterBtn.addEventListener("click", saveFilters);