// ==UserScript==
// @name Geoguessrtips
// @namespace https://www.leonbrandt.com
// @version 2.2.0
// @description 这是一个指定地图提示plonkit的脚本
// @author Leon Brandt
// @homepage https://gf.qytechs.cn/zh-CN/users/1129612-yukejun
// @match https://www.geoguessr.com/*
// @grant GM_xmlhttpRequest
// @run-at document-idle
// ==/UserScript==
let currentMapId = null;
let lastUrl = window.location.href; // 记录最后一次检测的URL
function checkAndUpdateMapId() {
const urlPattern = /^https:\/\/www\.geoguessr\.com\/maps\//;
const currentUrl = window.location.href;
if (lastUrl !== currentUrl) {
lastUrl = currentUrl; // 更新最后一次检测的URL
console.log("Detected URL change. Current URL:", currentUrl);
if (urlPattern.test(currentUrl)) {
const match = currentUrl.match(/maps\/([^\/]+)/);
if (match && match[1]) {
const newMapId = match[1];
if (currentMapId !== newMapId) {
currentMapId = newMapId;
localStorage.setItem('currentMapId', currentMapId); // 更新LocalStorage
console.log("Updated mapId:", currentMapId);
}
} else {
console.error("Failed to extract mapId from URL:", currentUrl);
}
} else {
console.log("The URL does not match the required pattern for map IDs. Attempting to retrieve ID from storage.");
currentMapId = localStorage.getItem('currentMapId');
if (currentMapId) {
console.log("Using stored mapId:", currentMapId);
} else {
console.error("No mapId available in LocalStorage.");
}
}
}
}
// 设置定时器以定期检查URL变化
setInterval(checkAndUpdateMapId, 1000); // 每秒检查一次
// 在脚本开始时调用 checkAndUpdateMapId 函数确保ID是最新的
checkAndUpdateMapId();
// 添加模态窗口到页面中
function addModalToPage() {
// 模态窗口的HTML内容,增加了关闭按钮并改进了样式
const modalHTML = `
<div id="customModal" style="
display: none;
position: fixed;
z-index: 1000;
top: 10vh; /* 10% from the top of the viewport */
left: 10vw; /* 10% from the left of the viewport */
background-color: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 80vw; /* 80% of the viewport width */
max-width: 300px; /* maximum width */
transform: translate(-10vw, -10vh);
text-align: left;
transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out;
opacity: 0;
visibility: hidden;
">
<h2 style="margin-top: 0; margin-bottom: 20px; font-size: 1.5em; color: #333;">小技巧</h2>
<div id="modalContent">Loading...</div>
<span id="modalClose" style="
position: absolute;
top: 10px;
right: 15px;
cursor: pointer;
font-size: 1.5em;
color: #333;
">×</span>
</div>
`;
// 找到目标元素或将模态窗口插入到<body>中
const targetElement = document.body;
targetElement.insertAdjacentHTML('beforeend', modalHTML);
// 关闭按钮的事件监听器
const modalClose = document.getElementById('modalClose');
modalClose.addEventListener('click', function() {
const modal = document.getElementById('customModal');
modal.style.opacity = '0';
modal.style.visibility = 'hidden';
});
const savedTop = localStorage.getItem('modalTop');
const savedLeft = localStorage.getItem('modalLeft');
// 如果localStorage中有位置信息,则使用这些信息设置模态窗口的位置
if (savedTop && savedLeft) {
const modal = document.getElementById('customModal');
modal.style.top = savedTop;
modal.style.left = savedLeft;
// 由于我们设置了top和left,移除之前的transform
modal.style.transform = 'none';
}
// 确保模态已经被添加到页面
makeModalDraggable();
}
// 初始化时调用 addModalToPage 函数以设置模态窗口的位置
addModalToPage();
// 拖动模态窗口的功能
function makeModalDraggable() {
const modal = document.getElementById('customModal');
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
// 当在模态窗口上按下鼠标时
modal.addEventListener('mousedown', function(e) {
isDragging = true;
// 获取模态窗口的当前位置
const rect = modal.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
// 初始化模态窗口的位置,忽略transform属性
modal.style.left = rect.left + 'px';
modal.style.top = rect.top + 'px';
// 移除transform属性,以防止突然跳跃
modal.style.transform = 'none';
modal.style.cursor = 'move';
});
// 当在文档上移动鼠标时
document.addEventListener('mousemove', function(e) {
if (isDragging) {
modal.style.left = e.clientX - offsetX + 'px';
modal.style.top = e.clientY - offsetY + 'px';
}
});
// 当在文档上释放鼠标时
document.addEventListener('mouseup', function() {
if (isDragging) {
isDragging = false;
modal.style.cursor = 'default';
// 保存当前位置到localStorage
localStorage.setItem('modalTop', modal.style.top);
localStorage.setItem('modalLeft', modal.style.left);
}
});
}
// 全局点击事件,用于检测当点击事件发生在模态窗口之外时
document.addEventListener('click', function(event) {
const modal = document.getElementById('customModal');
// 检查模态窗口是否存在并且是可见的
if (modal && modal.style.opacity === '1') {
// 现在只有当模态窗口可见时,才会检查点击是否发生在模态窗口之外
if (!modal.contains(event.target)) {
modal.style.opacity = '0';
modal.style.visibility = 'hidden';
modal.style.display = 'none';
}
}
});
// 显示模态窗口并设置其内容
function showModal(content, serverText) {
const modal = document.getElementById('customModal');
const modalContent = document.getElementById('modalContent');
// 将 serverText 作为普通文本添加到地址信息下方,而不是在 textarea 中
// 修改后的 textarea 样式
modalContent.innerHTML = content + '<br><br>' + serverText + '<br><br>' +
'<textarea id="customTextarea" style="' +
'width: calc(100% - 16px);' + // 使用 calc 保证完整填充宽度减去内边距
'height: 6rem;' + // 使用 rem 单位
'resize: none;' +
'padding: 0.5rem;' +
'border: 1px solid #ccc;' +
'border-radius: 0.25rem;' +
'font-family: Arial, sans-serif;' +
'font-size: 1rem;' +
'line-height: 1.5;' +
'margin-bottom: 1rem;' +
'color: #333;' +
'box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);' +
'background-color: #fff;' +
'"></textarea>' +
'<br>' +
'<button id="saveBtn" style="' +
'padding: 0.5rem 1rem;' + // 更多内边距
'font-size: 1rem;' +
'color: white;' +
'background-color: #007bff;' +
'border: none;' +
'border-radius: 0.25rem;' +
'cursor: pointer;' +
'transition: background-color 0.2s;' +
'">保存</button>' +
'<div id="saveStatus" style="display: none; transition: opacity 1s; opacity: 1;"></div>';
// 设置模态窗口的样式为可见
modal.style.opacity = '1';
modal.style.visibility = 'visible';
modal.style.display = 'block';
const saveBtn = document.getElementById('saveBtn');
// 添加新的事件监听器
saveBtn.addEventListener("click", saveTextFunction);
}
// 提取保存文本的功能,以便可以从多个地方引用
async function saveTextFunction() {
const modalContent = document.getElementById('modalContent');
const updatedText = modalContent.querySelector('textarea').value;
const coords = getCoordinates();
const dataToSend = {
lat: coords.lat,
lng: coords.lng,
text: updatedText,
mapId: currentMapId // 添加这一行
};
const response = await fetch("http://localhost:8000/update_text", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(dataToSend)
});
const result = await response.json();
const saveStatusDiv = document.getElementById('saveStatus');
if (result.success) {
saveStatusDiv.innerText = "文本已保存!";
} else {
saveStatusDiv.innerText = "保存失败!";
}
// 显示保存状态
saveStatusDiv.style.display = 'block';
// 在1秒后开始使其透明度降为0
setTimeout(() => {
saveStatusDiv.style.opacity = '0';
}, 1000);
// 重新预加载数据
fetchDataAndShowModal();
}
// 下面是一系列获取当前游戏坐标的函数
function coordinateClimber(isStreaks){
let timeout = 10
let path = document.querySelector('div[data-qa="panorama"]');
while (timeout > 0){
const props = path[Object.keys(path).find(key => key.startsWith("__reactFiber$"))];
const checkReturns = iterateReturns(props,isStreaks)
if(checkReturns){
return checkReturns
}
path = path.parentNode
timeout--
}
alert("Failed to find co-ordinates. Please make an issue on GitHub or GreasyFork. " +
"Please make sure you mention the game mode in your report.")
}
function iterateReturns(element,isStreaks){
let timeout = 10
let path = element
while(timeout > 0){
if(path){
const coords = checkProps(path.memoizedProps,isStreaks)
if(coords){
return coords
}
}
if(!path["return"]){
return
}
path = path["return"]
timeout--
}
}
function checkProps(props,isStreaks){
if(props?.panoramaRef){
const found = props.panoramaRef.current.location.latLng
return [found.lat(),found.lng()]
}
if(props.streakLocationCode && isStreaks){
return props.streakLocationCode
}
if(props.gameState){
const x = props.gameState[props.gameState.rounds.length-1]
return [x.lat,x.lng]
}
if(props.lat){
return [props.lat,props.lng]
}
}
function getCoordinates() {
const coords = coordinateClimber();
return {
lat: coords[0],
lng: coords[1]
};
}
// 使用OpenStreetMap API获取坐标对应的地址
function getAddress(lat, lon) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json`,
onload: function(response) {
if (response.status === 200) {
resolve(JSON.parse(response.responseText));
} else {
reject('Failed to fetch address');
}
}
});
});
}
let lastFetchedData = null; // 用于存储最后一次获取的地址信息
// 获取数据并显示模态窗口
async function fetchDataAndShowModal() {
const coords = getCoordinates();
currentMapId = localStorage.getItem('currentMapId'); // 确保重新获取 localStorage 中的 currentMapId
if (!currentMapId) {
console.error("No mapId available to send to the server.");
return;
}
// 获取地址信息
try {
const addressData = await getAddress(coords.lat, coords.lng);
const addressInfo = `
Country: ${addressData.address.country}<br>
`;
/*在addressInfo添加以下信息可显示更详细的地址信息:
Country: ${addressData.address.country}<br>
State: ${addressData.address.state}<br>
County: ${addressData.address.county}<br>
City: ${addressData.address.city}<br>
Road: ${addressData.address.road}<br>
Postcode: ${addressData.address.postcode}<br>
Village/Suburb: ${(addressData.address.village || addressData.address.suburb)}<br>
Postal Address: ${addressData.display_name}<br>
*/
const dataToSend = {
lat: coords.lat,
lng: coords.lng,
mapId: currentMapId
};
GM_xmlhttpRequest({
method: "POST",
url: "http://localhost:8000/compare_coordinates",
data: JSON.stringify(dataToSend),
headers: {
"Content-Type": "application/json"
},
onload: function(response) {
const result = JSON.parse(response.responseText);
let serverResponse = result.match ? result.text : "Coordinates do not match.";
lastFetchedData = { address: addressInfo, serverText: serverResponse };
}
});
} catch (error) {
console.error("Error fetching address:", error);
}
}
// 在主函数中
(function() {
'use strict';
addModalToPage();
// 在页面加载后3秒执行 fetchDataAndShowModal 函数
setTimeout(fetchDataAndShowModal, 3000);
const selectors = [
'button[data-qa="close-round-result"]',
'button[data-qa="play-again-button"]',
'button[data-qa="start-game-button"]',
'#saveBtn'
];
function addEventToButton() {
selectors.forEach(selector => {
const btn = document.querySelector(selector);
if (btn) {
btn.removeEventListener("click", fetchDataAndShowModalDelayed); // 防止重复添加
btn.addEventListener("click", fetchDataAndShowModalDelayed);
}
});
}
function fetchDataAndShowModalDelayed() {
setTimeout(fetchDataAndShowModal, 3000); // 延迟3秒执行
}
// 使用 MutationObserver 监听页面元素变化
const observer = new MutationObserver(addEventToButton);
observer.observe(document.body, {
childList: true,
subtree: true
});
addEventToButton(); // 初始化时尝试添加
// 使用 addEventListener 替代 onkeydown
document.addEventListener("keydown", function(evt) {
const targetTagName = (evt.target || evt.srcElement).tagName;
// 检查按下的键是否是“1”
if (evt.keyCode == 49 && targetTagName != 'INPUT' && targetTagName != 'TEXTAREA') {
const modal = document.getElementById('customModal');
// 检查模态窗口是否已经可见
if (modal.style.opacity === '1') {
// 模态窗口已经显示,将其隐藏
modal.style.opacity = '0';
modal.style.visibility = 'hidden';
modal.style.display = 'none';
} else {
// 模态窗口未显示,将其显示
if (lastFetchedData) { // 确保有数据显示
showModal(lastFetchedData.address, lastFetchedData.serverText);
}
}
}
});
})();