您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
GeoSaver allows you to save and organize positions from single-player GeoGuessr rounds with descriptions, tags, and Street View links, use quicksaves in all modes,
当前为
// ==UserScript== // @name GeoSaver // @namespace http://tampermonkey.net/ // @version 1.1 / 31.03.2025 // @description GeoSaver allows you to save and organize positions from single-player GeoGuessr rounds with descriptions, tags, and Street View links, use quicksaves in all modes, // @description manage your saved data via a menu and settings, and ensures fair play by disabling advanced features in multiplayer. // @author SnApeS / Lukas // @match *://*.geoguessr.com/* // @grant none // @tag GeoSaver // @tag games // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // ===================================================== // 0. Global Variables and Persistence for Folders & Menu Position // ===================================================== window.roundLock = false; // Persist folder list function getSavedFolders() { let foldersStr = localStorage.getItem("geoguessr_saved_folders"); try { return foldersStr ? JSON.parse(foldersStr) : []; } catch(e) { return []; } } function addFolder(folderName) { if (!folderName) return; let folders = getSavedFolders(); if (!folders.includes(folderName)) { folders.push(folderName); localStorage.setItem("geoguessr_saved_folders", JSON.stringify(folders)); } } function removeFolder(folderName) { let folders = getSavedFolders(); folders = folders.filter(f => f !== folderName); localStorage.setItem("geoguessr_saved_folders", JSON.stringify(folders)); } // Persist menu position function loadMenuPosition() { let posStr = localStorage.getItem("geosaver_menu_position"); if (posStr) { try { return JSON.parse(posStr); } catch(e) { return null; } } return null; } function saveMenuPosition(pos) { localStorage.setItem("geosaver_menu_position", JSON.stringify(pos)); } // ===================================================== // 1. Utility Functions // ===================================================== function showPopup(message) { const popup = document.createElement('div'); popup.style.position = 'fixed'; popup.style.top = '20px'; popup.style.right = '20px'; popup.style.backgroundColor = '#333'; popup.style.color = '#fff'; popup.style.padding = '10px 15px'; popup.style.borderRadius = '5px'; popup.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; popup.style.zIndex = '12000'; popup.textContent = message; document.body.appendChild(popup); setTimeout(() => { popup.remove(); }, 2000); } // ===================================================== // 2. Google Maps StreetView Overrides (with Round Handling) // ===================================================== window.cachedStartPosition = null; function overrideStreetViewPanorama() { if (window.google && google.maps && google.maps.StreetViewPanorama) { const origSetPosition = google.maps.StreetViewPanorama.prototype.setPosition; google.maps.StreetViewPanorama.prototype.setPosition = function(position) { window.myGeoGuessrPosition = position; window.myGeoGuessrPanorama = this; const panoId = (this.getPano && typeof this.getPano === 'function') ? this.getPano() : null; const imageDate = (this.get && typeof this.get === 'function') ? this.get('imageDate') : null; const captureYear = imageDate ? new Date(imageDate).getFullYear() : null; // If no start position is saved or the panorama ID changes (new round) if (!window.cachedStartPosition || window.cachedStartPosition.pano !== panoId) { window.cachedStartPosition = { lat: (typeof position.lat === "function") ? position.lat() : position.lat, lng: (typeof position.lng === "function") ? position.lng() : position.lng, pano: panoId, year: captureYear }; window.roundLock = true; console.log("New start position:", window.cachedStartPosition); } return origSetPosition.apply(this, arguments); }; console.log("overrideStreetViewPanorama executed."); } else { setTimeout(overrideStreetViewPanorama, 500); } } overrideStreetViewPanorama(); function overrideStreetViewPov() { if (window.google && google.maps && google.maps.StreetViewPanorama) { const origSetPov = google.maps.StreetViewPanorama.prototype.setPov; google.maps.StreetViewPanorama.prototype.setPov = function(pov) { window.myGeoGuessrPov = pov; console.log("POV updated:", pov); return origSetPov.apply(this, arguments); }; console.log("overrideStreetViewPov executed."); } else { setTimeout(overrideStreetViewPov, 500); } } overrideStreetViewPov(); // ===================================================== // 3. Language Strings and Localization Setup // ===================================================== // Retrieve saved language from localStorage or default to ENG let savedLanguage = localStorage.getItem("geosaver_language"); let currentLanguage = savedLanguage ? savedLanguage : "ENG"; const langStrings = { ENG: { displayName: "English", title: "GeoSaver", save: "Save", saveBtn: "Save Location", training: "Practice", archive: "Archive", archiveBtn: "Archive", chooseFolder: "Choose Folder", newFolder: "Create New Folder...", note: "Note", tags: "Tags", close: "Close", streetViewLink: "Show Street View", chooseFolderMessage: "Please select a folder.", moveToTrainingBtn: "Move to Training", moveToFolder: "Move to Folder", deletePositionBtn: "Delete", deleteAllArchivedBtn: "Delete All", settingsTitle: "Settings", quickSaveActive: "QuickSave active", dragHint: "Drag me!", languageLabel: "Language", backupSave: "Save Backup", backupRestore: "Restore Backup", noCurrentPosition: "No current position found.", invalidPosition: "Invalid position.", positionSaved: "Position saved", quickSaveSaved: "QuickSave position saved\nTime: {time}", confirmDeletePosition: "Are you sure you want to delete this position?", confirmDeleteFolder: "Are you sure you want to delete the folder \"{folder}\"?", confirmDeleteAllArchived: "Are you sure you want to delete all archived positions in folder \"{folder}\"?", enterNewFolderName: "Please enter a new folder name.", selectFolder: "Please select a folder.", positionMoved: "Position moved to {folder}" }, DE: { displayName: "Deutsch", title: "GeoSaver", save: "Speichern", saveBtn: "Position speichern", training: "Training", archive: "Archiv", archiveBtn: "Archivieren", chooseFolder: "Ordner auswählen", newFolder: "Neuen Ordner erstellen...", note: "Notiz", tags: "Stichworte", close: "Schließen", streetViewLink: "Street View anzeigen", chooseFolderMessage: "Bitte wähle einen Ordner aus.", moveToTrainingBtn: "Ins Training verschieben", moveToFolder: "In Ordner verschieben", deletePositionBtn: "Löschen", deleteAllArchivedBtn: "Alle löschen", settingsTitle: "Einstellungen", quickSaveActive: "QuickSave aktiv", dragHint: "Zieh mich!", languageLabel: "Sprache", backupSave: "Backup speichern", backupRestore: "Backup wiederherstellen", noCurrentPosition: "Keine aktuelle Position gefunden.", invalidPosition: "Ungültige Position.", positionSaved: "Position gespeichert", quickSaveSaved: "QuickSave Position gespeichert: \nZeit: {time}", confirmDeletePosition: "Möchtest du diese Position wirklich löschen?", confirmDeleteFolder: "Möchtest du den Ordner \"{folder}\" wirklich löschen?", confirmDeleteAllArchived: "Möchtest du alle archivierten Positionen im Ordner \"{folder}\" wirklich löschen?", enterNewFolderName: "Bitte neuen Ordnernamen eingeben.", selectFolder: "Bitte wähle einen Ordner aus.", positionMoved: "Position verschoben in {folder}" }, ES: { displayName: "Español", title: "GeoSaver", save: "Guardar", saveBtn: "Guardar Ubicación", training: "Practicar", archive: "Archivo", archiveBtn: "Archivo", chooseFolder: "Elegir Carpeta", newFolder: "Crear Nueva Carpeta...", note: "Nota", tags: "Etiquetas", close: "Cerrar", streetViewLink: "Mostrar Street View", chooseFolderMessage: "Por favor, selecciona una carpeta.", moveToTrainingBtn: "Mover a Práctica", moveToFolder: "Mover a Carpeta", deletePositionBtn: "Eliminar", deleteAllArchivedBtn: "Eliminar Todo", settingsTitle: "Configuración", quickSaveActive: "QuickSave activo", dragHint: "¡Arrástrame!", languageLabel: "Idioma", backupSave: "Guardar Respaldo", backupRestore: "Restaurar Respaldo", noCurrentPosition: "No se encontró posición actual.", invalidPosition: "Posición inválida.", positionSaved: "Posición guardada", quickSaveSaved: "Posición guardada en QuickSave: \nZeit: {time}", confirmDeletePosition: "¿Estás seguro de que deseas eliminar esta posición?", confirmDeleteFolder: "¿Estás seguro de que deseas eliminar la carpeta \"{folder}\"?", confirmDeleteAllArchived: "¿Estás seguro de que deseas eliminar todas las posiciones archivadas en la carpeta \"{folder}\"?", enterNewFolderName: "Por favor, ingresa un nuevo nombre para la carpeta.", selectFolder: "Por favor, selecciona una carpeta.", positionMoved: "Posición movida a {folder}" } }; // ===================================================== // 4. SPA Navigation Override // ===================================================== (function() { const _oldPushState = history.pushState; history.pushState = function() { const ret = _oldPushState.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); return ret; }; const _oldReplaceState = history.replaceState; history.replaceState = function() { const ret = _oldReplaceState.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); return ret; }; })(); window.addEventListener('locationchange', () => { const menu = document.getElementById('geoSaverMenu'); if (menu) menu.remove(); const indicator = document.getElementById('quickSaveIndicator'); if (indicator) indicator.remove(); initScript(); }); // ===================================================== // 5. QuickSave Indicator (Multiplayer) // ===================================================== function isMultiplayer() { return window.location.href.toLowerCase().includes("/multiplayer"); } function showQuickSaveIndicator() { const existing = document.getElementById('geoSaverMenu'); if (existing) existing.remove(); let indicator = document.createElement('div'); indicator.id = 'quickSaveIndicator'; indicator.style.position = 'fixed'; indicator.style.bottom = '0'; indicator.style.left = '0'; indicator.style.width = '100%'; indicator.style.backgroundColor = '#FF8C00'; indicator.style.color = '#000'; indicator.style.textAlign = 'center'; indicator.style.padding = '2px'; indicator.style.fontSize = '0.6em'; indicator.style.zIndex = '10000'; indicator.textContent = langStrings[currentLanguage].quickSaveActive; document.body.appendChild(indicator); } window.quickSaveToggledOff = false; function initScript() { console.log("initScript fired, URL:", window.location.href); if (isMultiplayer()) { if (!window.quickSaveToggledOff) { showQuickSaveIndicator(); } } else { const indicator = document.getElementById('quickSaveIndicator'); if (indicator) indicator.remove(); if (!document.getElementById('geoSaverMenu')) { createMainMenu(); } } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initScript); } else { initScript(); } // ===================================================== // 6. Main Menu Creation with Drag & Drop // ===================================================== function createMainMenu() { if (document.getElementById('geoSaverMenu')) return; console.log("Creating main menu"); const menu = document.createElement('div'); menu.id = 'geoSaverMenu'; menu.style.position = 'fixed'; // Load saved menu position, if available const savedPos = loadMenuPosition(); if (savedPos && savedPos.top && savedPos.left) { menu.style.top = savedPos.top; menu.style.left = savedPos.left; menu.style.right = "auto"; } else { // Default position menu.style.top = '10%'; menu.style.right = '0'; } menu.style.minWidth = '300px'; menu.style.height = 'auto'; menu.style.boxSizing = 'border-box'; menu.style.backgroundColor = 'rgba(0,0,0,0.85)'; menu.style.color = '#fff'; menu.style.padding = '20px'; menu.style.border = '2px solid #fff'; menu.style.borderRadius = '10px'; menu.style.boxShadow = '0 4px 10px rgba(0,0,0,0.5)'; menu.style.zIndex = '10000'; // Header with Drag & Drop functionality const header = document.createElement('div'); header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.cursor = 'move'; header.style.userSelect = 'none'; header.innerHTML = `<h2 style="margin:0;">${langStrings[currentLanguage].title}</h2> <button id="settingsBtn" style="background:none; border:none; cursor:pointer; font-size:1.5rem; color:#FF8C00;">⚙</button>`; menu.appendChild(header); // Main content sections (Save, QuickSave, Training, Archive) const contentContainer = document.createElement('div'); const contentHTML = ` <div style="margin:20px 0;"></div> <div id="mainSections" style="display:flex; flex-direction:column; gap:20px;"> <!-- Save Section --> <div id="speichernSection" style="border:1px solid #ccc; padding:10px;"> <h3 style="cursor:pointer;">${langStrings[currentLanguage].save}</h3> <div id="speichernContent" style="display:none;"> <div style="display:flex; align-items:center; margin-bottom:5px;"> <div style="width:120px;">${langStrings[currentLanguage].chooseFolder}:</div> <div style="flex:1;"> <select id="gg-folder-save" onchange="handleFolderChange(this)"> <option value="">${langStrings[currentLanguage].chooseFolder}</option> <option value="new">${langStrings[currentLanguage].newFolder}</option> </select> <div id="gg-new-folder-container"></div> </div> </div> <div style="margin-bottom:5px;"> <strong>${langStrings[currentLanguage].note}:</strong><br> <span class="editable" data-id="0" data-field="note" style="color:brown; cursor:pointer;" id="displayNote">[empty]</span> <input type="text" id="gg-note-input" placeholder="${langStrings[currentLanguage].note} input" style="width:100%; display:none;"> </div> <div style="margin-bottom:5px;"> <strong>${langStrings[currentLanguage].tags}:</strong><br> <span class="editable" data-id="0" data-field="tags" style="color:purple; cursor:pointer;" id="displayTags">[empty]</span> <input type="text" id="gg-tags-input" placeholder="${langStrings[currentLanguage].tags} input" style="width:100%; display:none;"> </div> <div style="text-align:center; margin-top:10px;"> <button id="savePositionBtn" style="background-color: green; color: white; border: none; padding: 5px 10px; border-radius: 5px;">${langStrings[currentLanguage].saveBtn}</button> </div> </div> </div> <!-- QuickSave Section --> <div id="quickSaveSection" style="border:1px solid #ccc; padding:10px;"> <h3 style="cursor:pointer;">QuickSave</h3> <div id="quickSaveContent" style="display:none;"> <div id="quickSaveList" style="max-height:200px; overflow-y:auto;"></div> </div> </div> <!-- Training Section --> <div id="trainingSection" style="border:1px solid #ccc; padding:10px;"> <h3 style="cursor:pointer;">${langStrings[currentLanguage].training}</h3> <div id="trainingContent" style="display:none;"> <div> <label>${langStrings[currentLanguage].chooseFolder}: <select id="gg-folder-training" onchange="if(typeof renderTrainingPositions==='function') renderTrainingPositions();"> <option value="">${langStrings[currentLanguage].chooseFolder}</option> </select> </label> </div> <div id="trainingList" style="max-height:200px; overflow-y:auto;"></div> </div> </div> <!-- Archive Section --> <div id="archivSection" style="border:1px solid #ccc; padding:10px;"> <h3 style="cursor:pointer;">${langStrings[currentLanguage].archive}</h3> <div id="archivContent" style="display:none;"> <div> <label>${langStrings[currentLanguage].chooseFolder}: <select id="gg-folder-completed" onchange="if(typeof renderCompletedPositions==='function') renderCompletedPositions();"> <option value="">${langStrings[currentLanguage].chooseFolder}</option> </select> </label> </div> <div id="archivList" style="max-height:200px; overflow-y:auto;"></div> <div style="text-align:center; margin-top:10px;"> <button id="deleteAllArchivedBtn" style="background-color: red; color:white; border: none; padding: 5px 10px; border-radius: 5px;">${langStrings[currentLanguage].deleteAllArchivedBtn}</button> </div> </div> </div> </div> <div style="text-align:right; margin-top:20px;"> <button id="closeMenuBtn" style="background-color: #696969; color: white; border: none; padding: 5px 10px; border-radius: 5px;"> ${langStrings[currentLanguage].close} </button> </div> `; contentContainer.insertAdjacentHTML('beforeend', contentHTML); menu.appendChild(contentContainer); document.body.appendChild(menu); // Prevent keyboard events in the menu menu.addEventListener('keydown', (e) => { e.stopPropagation(); }); // Event listeners for main menu buttons document.getElementById('closeMenuBtn').addEventListener('click', () => menu.remove()); document.getElementById('settingsBtn').addEventListener('click', openSettingsMenu); document.getElementById('savePositionBtn').addEventListener('click', saveCurrentPosition); document.getElementById('deleteAllArchivedBtn').addEventListener('click', deleteAllArchivedPositions); attachInlineEditing(); updateAllFolderDropdowns(); renderTrainingPositions(); renderCompletedPositions(); renderQuickSavePositions(); // Toggle sections on header click const sectionHeaders = document.querySelectorAll("#mainSections > div > h3"); sectionHeaders.forEach(secHeader => { secHeader.addEventListener("click", () => { document.querySelectorAll("#mainSections > div > div").forEach(div => { if(div !== secHeader.nextElementSibling) { div.style.display = "none"; } }); secHeader.nextElementSibling.style.display = (secHeader.nextElementSibling.style.display === "none" || secHeader.nextElementSibling.style.display === "") ? "block" : "none"; }); }); // -------------------------------------------------- // Drag & Drop for the entire menu // -------------------------------------------------- (function() { let offsetX = 0, offsetY = 0, startX = 0, startY = 0; header.ondragstart = function() { return false; }; header.addEventListener('mousedown', dragMouseDown); function dragMouseDown(e) { e.preventDefault(); startX = e.clientX; startY = e.clientY; document.addEventListener('mousemove', elementDrag); document.addEventListener('mouseup', closeDragElement); } function elementDrag(e) { e.preventDefault(); offsetX = startX - e.clientX; offsetY = startY - e.clientY; startX = e.clientX; startY = e.clientY; const rect = menu.getBoundingClientRect(); menu.style.top = (rect.top - offsetY) + "px"; menu.style.left = (rect.left - offsetX) + "px"; menu.style.right = "auto"; } function closeDragElement() { document.removeEventListener('mousemove', elementDrag); document.removeEventListener('mouseup', closeDragElement); saveMenuPosition({ top: menu.style.top, left: menu.style.left }); } })(); } // ===================================================== // 7. Settings Menu (Language and Backup Options) // ===================================================== function openSettingsMenu() { const overlay = document.createElement('div'); overlay.id = 'settingsOverlay'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0,0,0,0.7)'; overlay.style.zIndex = '11000'; overlay.style.display = 'flex'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; const container = document.createElement('div'); container.style.backgroundColor = '#fff'; container.style.padding = '20px'; container.style.borderRadius = '8px'; container.style.minWidth = '300px'; container.innerHTML = ` <!-- Language Section --> <div id="languageSettings" style="text-align:center; margin-bottom:20px; border-bottom: 1px solid #ccc; padding-bottom: 10px;"> <label for="languageSelect">${langStrings[currentLanguage].languageLabel}:</label> <select id="languageSelect"></select> </div> <!-- Backup Section --> <div id="backupButtons" style="text-align:center; display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px;"> <button id="backupSaveBtn" style="background-color: green; color: white; border: none; padding: 5px 10px; border-radius: 5px;">${langStrings[currentLanguage].backupSave}</button> <button id="backupRestoreBtn" style="background-color: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">${langStrings[currentLanguage].backupRestore}</button> </div> <div style="text-align:center;"> <button id="closeSettingsBtn" style="background-color: #696969; color: white; border: none; padding: 5px 10px; border-radius: 5px;"> ${langStrings[currentLanguage].close} </button> </div> `; overlay.appendChild(container); document.body.appendChild(overlay); // Dynamically populate language select options based on keys in langStrings const languageSelect = document.getElementById('languageSelect'); languageSelect.innerHTML = ""; Object.keys(langStrings).forEach(key => { const option = document.createElement("option"); option.value = key; option.textContent = langStrings[key].displayName; if(key === currentLanguage) { option.selected = true; } languageSelect.appendChild(option); }); // Language change event: update currentLanguage and store selection languageSelect.addEventListener('change', function() { currentLanguage = this.value; localStorage.setItem("geosaver_language", currentLanguage); location.reload(); }); // Backup button events document.getElementById('backupSaveBtn').addEventListener('click', backupSave); document.getElementById('backupRestoreBtn').addEventListener('click', backupRestore); document.getElementById('closeSettingsBtn').addEventListener('click', () => overlay.remove()); } // ----- 7.1 Backup Functions ----- function backupSave() { const backupData = { folders: localStorage.getItem("geoguessr_saved_folders"), positions: localStorage.getItem("geoguessr_saved_positions"), menuPosition: localStorage.getItem("geosaver_menu_position"), language: localStorage.getItem("geosaver_language") }; const dataStr = JSON.stringify(backupData, null, 2); const blob = new Blob([dataStr], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "geosaver_backup.json"; a.click(); URL.revokeObjectURL(url); } function backupRestore() { const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.addEventListener("change", function(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const backupData = JSON.parse(e.target.result); if (backupData.folders !== undefined) localStorage.setItem("geoguessr_saved_folders", backupData.folders); if (backupData.positions !== undefined) localStorage.setItem("geoguessr_saved_positions", backupData.positions); if (backupData.menuPosition !== undefined) localStorage.setItem("geosaver_menu_position", backupData.menuPosition); if (backupData.language !== undefined) localStorage.setItem("geosaver_language", backupData.language); showPopup("Backup restored. Reloading..."); setTimeout(() => location.reload(), 1500); } catch (err) { showPopup("Invalid backup file."); } }; reader.readAsText(file); }); input.click(); } // ===================================================== // 8. Inline Editing for Note & Tags // ===================================================== function attachInlineEditing() { const editables = document.querySelectorAll('.editable'); editables.forEach(span => { span.style.cursor = 'pointer'; span.addEventListener('click', () => { const field = span.getAttribute('data-field'); const posId = span.getAttribute('data-id'); const currentText = span.textContent === "[empty]" ? "" : span.textContent; let input = document.createElement('input'); input.type = 'text'; input.value = currentText; input.style.width = "100%"; input.addEventListener('blur', () => { const newVal = input.value.trim(); span.textContent = newVal || "[empty]"; input.remove(); span.style.display = "inline"; let positions = getSavedPositions(); let updated = false; positions.forEach(pos => { if (pos.id == posId) { pos[field] = newVal; updated = true; } }); if (updated) { localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); } }); input.addEventListener('keydown', (e) => { if (e.key === "Enter") { input.blur(); } }); span.style.display = "none"; span.parentNode.insertBefore(input, span); input.focus(); }); }); } // ===================================================== // 9. Folder Handling and Dropdown Updates // ===================================================== window.handleFolderChange = function(selectElem) { const container = document.getElementById('gg-new-folder-container'); if (selectElem.value === 'new') { container.innerHTML = `<input type="text" id="gg-new-folder-input" placeholder="${langStrings[currentLanguage].newFolder}" style="margin-top:5px; width:100%;">`; } else { container.innerHTML = ''; } }; function getSavedPositions() { let positionsStr = localStorage.getItem("geoguessr_saved_positions"); try { return positionsStr ? JSON.parse(positionsStr) : []; } catch(e) { return []; } } function updateAllFolderDropdowns() { let savedFolders = getSavedFolders(); function getFolderList(filterFn) { let posFolders = Array.from(new Set(getSavedPositions().filter(filterFn).map(p => p.folder).filter(f => f))); let allFolders = Array.from(new Set([...savedFolders, ...posFolders])); allFolders.sort(); return allFolders; } const saveDropdown = document.getElementById("gg-folder-save"); if (saveDropdown) { const curVal = saveDropdown.value; saveDropdown.innerHTML = `<option value="">${langStrings[currentLanguage].chooseFolder}</option> <option value="new">${langStrings[currentLanguage].newFolder}</option>`; let folders = getFolderList(() => true); folders.forEach(folder => { let opt = document.createElement("option"); opt.value = folder; opt.textContent = folder; saveDropdown.appendChild(opt); }); if (folders.includes(curVal)) { saveDropdown.value = curVal; } } const trainingDropdown = document.getElementById("gg-folder-training"); if (trainingDropdown) { const currentTrainingValue = trainingDropdown.value; trainingDropdown.innerHTML = `<option value="">${langStrings[currentLanguage].chooseFolder}</option>`; let folders = getFolderList(() => true); folders.forEach(folder => { let opt = document.createElement("option"); opt.value = folder; opt.textContent = folder; trainingDropdown.appendChild(opt); }); if (folders.includes(currentTrainingValue)) { trainingDropdown.value = currentTrainingValue; } if (!document.getElementById("deleteTrainingFolderBtn")) { const btn = document.createElement("button"); btn.id = "deleteTrainingFolderBtn"; btn.textContent = "❌"; btn.style.marginLeft = "10px"; btn.onclick = () => { const sel = trainingDropdown.value.trim(); if (sel && confirm(langStrings[currentLanguage].confirmDeleteFolder.replace("{folder}", sel))) { deleteFolder(sel); } }; trainingDropdown.parentNode.appendChild(btn); } } const completedDropdown = document.getElementById("gg-folder-completed"); if (completedDropdown) { const currentCompletedValue = completedDropdown.value; completedDropdown.innerHTML = `<option value="">${langStrings[currentLanguage].chooseFolder}</option>`; let folders = getFolderList(() => true); folders.forEach(folder => { let opt = document.createElement("option"); opt.value = folder; opt.textContent = folder; completedDropdown.appendChild(opt); }); if (folders.includes(currentCompletedValue)) { completedDropdown.value = currentCompletedValue; } if (!document.getElementById("deleteCompletedFolderBtn")) { const btn = document.createElement("button"); btn.id = "deleteCompletedFolderBtn"; btn.textContent = "❌"; btn.style.marginLeft = "10px"; btn.onclick = () => { const sel = completedDropdown.value.trim(); if (sel && confirm(langStrings[currentLanguage].confirmDeleteFolder.replace("{folder}", sel))) { deleteFolder(sel); } }; completedDropdown.parentNode.appendChild(btn); } } } function deleteFolder(folderName) { if (confirm(langStrings[currentLanguage].confirmDeleteFolder.replace("{folder}", folderName))) { let positions = getSavedPositions(); positions = positions.filter(p => p.folder !== folderName); localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); removeFolder(folderName); updateAllFolderDropdowns(); renderTrainingPositions(); renderCompletedPositions(); } } // ===================================================== // 10. Position Saving Functions (using the fixed start position) // ===================================================== function saveCurrentPosition() { let pos; if (window.cachedStartPosition) { pos = window.cachedStartPosition; } else if (window.myGeoGuessrPosition) { pos = window.myGeoGuessrPosition; } else if (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getPosition === 'function') { pos = window.myGeoGuessrPanorama.getPosition(); } else { showPopup(langStrings[currentLanguage].noCurrentPosition); return; } let lat, lng, heading, pitch, panoId, captureYear; const pov = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getPov === 'function') ? window.myGeoGuessrPanorama.getPov() : { heading: 0, pitch: 0 }; if (window.cachedStartPosition) { lat = pos.lat; lng = pos.lng; panoId = pos.pano; captureYear = pos.year; } else { if (typeof pos.lat === "function") { lat = pos.lat(); lng = pos.lng(); } else if (typeof pos.lat === "number") { lat = pos.lat; lng = pos.lng; } else { showPopup(langStrings[currentLanguage].invalidPosition); return; } panoId = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getPano === 'function') ? window.myGeoGuessrPanorama.getPano() : null; const imageDate = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.get === 'function') ? window.myGeoGuessrPanorama.get('imageDate') : null; captureYear = imageDate ? new Date(imageDate).getFullYear() : null; } heading = pov.heading; pitch = pov.pitch; let zoom = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getZoom === 'function') ? window.myGeoGuessrPanorama.getZoom() : 0; let note = document.getElementById("displayNote").textContent; let tags = document.getElementById("displayTags").textContent; note = (note === "[empty]") ? "" : note; tags = (tags === "[empty]") ? "" : tags; const folderSelect = document.getElementById("gg-folder-save"); const newFolderInput = document.getElementById("gg-new-folder-input"); let folder = ""; if (folderSelect.value === "new") { folder = newFolderInput ? newFolderInput.value.trim() : ""; if (!folder) { showPopup(langStrings[currentLanguage].enterNewFolderName); return; } } else { folder = folderSelect.value.trim(); if (!folder) { showPopup(langStrings[currentLanguage].selectFolder); return; } } addFolder(folder); let positions = getSavedPositions(); const newEntry = { id: Date.now(), lat: lat, lng: lng, folder: folder, tags: tags, note: note, completed: false, timestamp: new Date().toISOString(), heading: heading, pitch: pitch, zoom: zoom, pano: panoId, year: captureYear }; positions.push(newEntry); localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); showPopup(langStrings[currentLanguage].positionSaved.replace("{lat}", lat.toFixed(6)).replace("{lng}", lng.toFixed(6))); updateAllFolderDropdowns(); renderTrainingPositions(); renderCompletedPositions(); } let lastQuickSaveTime = 0; function quickSave() { const now = Date.now(); if(now - lastQuickSaveTime < 500) return; lastQuickSaveTime = now; let pos; if (window.cachedStartPosition) { pos = window.cachedStartPosition; } else if (window.myGeoGuessrPosition) { pos = window.myGeoGuessrPosition; } else if (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getPosition === 'function') { pos = window.myGeoGuessrPanorama.getPosition(); } else { showPopup(langStrings[currentLanguage].noCurrentPosition); return; } let lat, lng, heading, pitch, panoId, captureYear; const pov = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getPov === 'function') ? window.myGeoGuessrPanorama.getPov() : { heading: 0, pitch: 0 }; if (window.cachedStartPosition) { lat = pos.lat; lng = pos.lng; panoId = pos.pano; captureYear = pos.year; } else { if (typeof pos.lat === "function") { lat = pos.lat(); lng = pos.lng(); } else if (typeof pos.lat === "number") { lat = pos.lat; lng = pos.lng; } else { showPopup(langStrings[currentLanguage].invalidPosition); return; } panoId = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getPano === 'function') ? window.myGeoGuessrPanorama.getPano() : null; const imageDate = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.get === 'function') ? window.myGeoGuessrPanorama.get('imageDate') : null; captureYear = imageDate ? new Date(imageDate).getFullYear() : null; } heading = pov.heading; pitch = pov.pitch; let zoom = (window.myGeoGuessrPanorama && typeof window.myGeoGuessrPanorama.getZoom === 'function') ? window.myGeoGuessrPanorama.getZoom() : 0; const folder = ""; const timestamp = new Date().toLocaleString(); const tags = "QuickSave " + timestamp; const note = ""; let positions = getSavedPositions(); const newEntry = { id: Date.now(), lat: lat, lng: lng, folder: folder, tags: tags, note: note, completed: false, timestamp: new Date().toISOString(), heading: heading, pitch: pitch, zoom: zoom, pano: panoId, year: captureYear, quickSave: true }; positions.push(newEntry); localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); showPopup(langStrings[currentLanguage].quickSaveSaved.replace("{lat}", lat.toFixed(6)).replace("{lng}", lng.toFixed(6)).replace("{time}", timestamp)); updateAllFolderDropdowns(); renderTrainingPositions(); renderCompletedPositions(); renderQuickSavePositions(); } // ===================================================== // 11. Rendering Functions for Saved Positions // ===================================================== function renderTrainingPositions() { const dropdown = document.getElementById("gg-folder-training"); const listContainer = document.getElementById("trainingList"); if (!dropdown || !listContainer) return; let selectedFolder = dropdown.value.trim(); if (!selectedFolder) { listContainer.innerHTML = langStrings[currentLanguage].chooseFolderMessage; return; } let positions = getSavedPositions().filter(p => p.folder.trim() === selectedFolder && !p.completed); listContainer.innerHTML = ""; positions.forEach(p => { let streetViewURL = `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${p.lat},${p.lng}`; if (p.heading !== undefined && p.pitch !== undefined) { streetViewURL += `&heading=${p.heading}&pitch=${p.pitch}`; } if (p.zoom !== undefined && p.zoom !== null) { streetViewURL += `&zoom=${p.zoom}`; } if (p.pano) { streetViewURL += `&pano=${p.pano}`; } if (p.year) { streetViewURL += `&year=${p.year}`; } let div = document.createElement("div"); div.style.borderBottom = "1px solid #ccc"; div.style.marginBottom = "5px"; div.style.paddingBottom = "5px"; div.innerHTML = ` <a href="${streetViewURL}" target="_blank" style="color:#ADD8E6;">${langStrings[currentLanguage].streetViewLink}</a><br> <strong>${langStrings[currentLanguage].note}:</strong><br> <span class="editable" data-id="${p.id}" data-field="note" style="color:brown; cursor:pointer;">${p.note || "[empty]"}</span><br> <strong>${langStrings[currentLanguage].tags}:</strong><br> <span class="editable" data-id="${p.id}" data-field="tags" style="color:purple; cursor:pointer;">${p.tags || "[empty]"}</span><br> <button onclick="moveToArchive(${p.id})" style="background-color:green; color:white; border:none; padding:5px 10px; border-radius:5px;">${langStrings[currentLanguage].archiveBtn}</button> `; listContainer.appendChild(div); }); attachInlineEditing(); } function renderCompletedPositions() { const dropdown = document.getElementById("gg-folder-completed"); const listContainer = document.getElementById("archivList"); if (!dropdown || !listContainer) return; let selectedFolder = dropdown.value.trim(); if (!selectedFolder) { listContainer.innerHTML = langStrings[currentLanguage].chooseFolderMessage; return; } let positions = getSavedPositions().filter(p => p.folder.trim() === selectedFolder && p.completed); listContainer.innerHTML = ""; positions.forEach(p => { let streetViewURL = `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${p.lat},${p.lng}`; if (p.heading !== undefined && p.pitch !== undefined) { streetViewURL += `&heading=${p.heading}&pitch=${p.pitch}`; } if (p.zoom !== undefined && p.zoom !== null) { streetViewURL += `&zoom=${p.zoom}`; } if (p.pano) { streetViewURL += `&pano=${p.pano}`; } if (p.year) { streetViewURL += `&year=${p.year}`; } let div = document.createElement("div"); div.style.borderBottom = "1px solid #ccc"; div.style.marginBottom = "5px"; div.style.paddingBottom = "5px"; div.innerHTML = ` <a href="${streetViewURL}" target="_blank" style="color:#ADD8E6;">${langStrings[currentLanguage].streetViewLink}</a><br> <strong>${langStrings[currentLanguage].note}:</strong><br> <span class="editable" data-id="${p.id}" data-field="note" style="color:brown; cursor:pointer;">${p.note || "[empty]"}</span><br> <strong>${langStrings[currentLanguage].tags}:</strong><br> <span class="editable" data-id="${p.id}" data-field="tags" style="color:purple; cursor:pointer;">${p.tags || "[empty]"}</span><br> <button onclick="moveToTraining(${p.id})" style="background-color:yellow; color:black; border:none; padding:5px 10px; border-radius:5px; margin-right:5px;">${langStrings[currentLanguage].moveToTrainingBtn}</button> <button onclick="deletePosition(${p.id})" style="background-color:red; color:white; border:none; padding:5px 10px; border-radius:5px;">${langStrings[currentLanguage].deletePositionBtn}</button> `; let folderDropdown = document.createElement("select"); folderDropdown.innerHTML = `<option value="">${langStrings[currentLanguage].chooseFolder}</option><option value="new">${langStrings[currentLanguage].newFolder}</option>`; let folders = getSavedFolders(); folders.sort(); folders.forEach(folder => { let opt = document.createElement("option"); opt.value = folder; opt.textContent = folder; folderDropdown.appendChild(opt); }); folderDropdown.addEventListener("change", function() { handleArchiveFolderChange(folderDropdown, p.id); }); div.appendChild(folderDropdown); listContainer.appendChild(div); }); attachInlineEditing(); } function renderQuickSavePositions() { const listContainer = document.getElementById("quickSaveList"); if (!listContainer) return; let positions = getSavedPositions().filter(p => p.quickSave === true); if (positions.length === 0) { listContainer.innerHTML = "No QuickSave entries."; return; } listContainer.innerHTML = ""; positions.forEach(p => { let streetViewURL = `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${p.lat},${p.lng}`; if (p.heading !== undefined && p.pitch !== undefined) { streetViewURL += `&heading=${p.heading}&pitch=${p.pitch}`; } if (p.zoom !== undefined && p.zoom !== null) { streetViewURL += `&zoom=${p.zoom}`; } if (p.pano) { streetViewURL += `&pano=${p.pano}`; } if (p.year) { streetViewURL += `&year=${p.year}`; } let folders = getSavedFolders(); folders.sort(); let dropdown = document.createElement("select"); let option = document.createElement("option"); option.value = ""; option.textContent = langStrings[currentLanguage].chooseFolder; dropdown.appendChild(option); option = document.createElement("option"); option.value = "new"; option.textContent = langStrings[currentLanguage].newFolder; dropdown.appendChild(option); folders.forEach(folder => { let opt = document.createElement("option"); opt.value = folder; opt.textContent = folder; dropdown.appendChild(opt); }); dropdown.addEventListener("change", function() { handleQuickSaveMoveDropdownChange(dropdown, p.id); }); let containerDiv = document.createElement("div"); containerDiv.style.borderBottom = "1px solid #ccc"; containerDiv.style.marginBottom = "5px"; containerDiv.style.paddingBottom = "5px"; containerDiv.innerHTML = ` <a href="${streetViewURL}" target="_blank" style="color:#ADD8E6;">${langStrings[currentLanguage].streetViewLink}</a><br> <strong>${langStrings[currentLanguage].note}:</strong><br> <span class="editable" data-id="${p.id}" data-field="note" style="color: brown; cursor:pointer;">${p.note || "[empty]"}</span><br> <strong>${langStrings[currentLanguage].tags}:</strong><br> <span class="editable" data-id="${p.id}" data-field="tags" style="color: purple; cursor:pointer;">${p.tags || "[empty]"}</span><br> <strong>${langStrings[currentLanguage].moveToFolder}:</strong><br> `; containerDiv.appendChild(dropdown); listContainer.appendChild(containerDiv); }); attachInlineEditing(); } // ===================================================== // 12. Position Management Functions // ===================================================== function moveToArchive(positionId) { let positions = getSavedPositions(); let position = positions.find(p => p.id === positionId); if (position) { position.completed = true; localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); updateAllFolderDropdowns(); renderTrainingPositions(); renderCompletedPositions(); } } function moveToTraining(positionId) { let positions = getSavedPositions(); let position = positions.find(p => p.id === positionId); if (position) { position.completed = false; localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); console.log(`Position ${positionId} moved back to training.`); updateAllFolderDropdowns(); renderTrainingPositions(); renderCompletedPositions(); } else { console.error(`Position ${positionId} not found.`); } } function deletePosition(positionId) { if (confirm(langStrings[currentLanguage].confirmDeletePosition)) { let positions = getSavedPositions(); positions = positions.filter(p => p.id !== positionId); localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); console.log(`Position ${positionId} deleted.`); renderTrainingPositions(); renderCompletedPositions(); } } function handleQuickSaveMoveDropdownChange(selectElem, quickSaveId) { if (selectElem.value === "new") { let input = document.createElement("input"); input.type = "text"; input.placeholder = langStrings[currentLanguage].newFolder; input.style.marginTop = "5px"; input.style.width = "100%"; input.addEventListener('blur', function(){ let folderName = input.value.trim(); if(folderName) { moveQuickSaveToTraining(quickSaveId, folderName); } else { renderQuickSavePositions(); } }); selectElem.parentNode.replaceChild(input, selectElem); input.focus(); } else if (selectElem.value !== "") { moveQuickSaveToTraining(quickSaveId, selectElem.value); } } window.moveQuickSaveToTraining = function(quickSaveId, folderName) { let positions = getSavedPositions(); let pos = positions.find(p => p.id === quickSaveId); if (pos) { pos.folder = folderName; pos.quickSave = false; pos.completed = false; localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); addFolder(folderName); showPopup(langStrings[currentLanguage].positionMoved.replace("{folder}", folderName)); updateAllFolderDropdowns(); renderTrainingPositions(); renderCompletedPositions(); renderQuickSavePositions(); } }; function handleArchiveFolderChange(selectElem, positionId) { if (selectElem.value === "new") { let input = document.createElement("input"); input.type = "text"; input.placeholder = langStrings[currentLanguage].newFolder; input.style.marginTop = "5px"; input.style.width = "100%"; input.addEventListener('blur', function() { let folderName = input.value.trim(); if (folderName) { moveArchiveToFolder(positionId, folderName); } else { renderCompletedPositions(); } }); selectElem.parentNode.replaceChild(input, selectElem); input.focus(); } else if (selectElem.value !== "") { moveArchiveToFolder(positionId, selectElem.value); } } window.moveArchiveToFolder = function(positionId, folderName) { let positions = getSavedPositions(); let pos = positions.find(p => p.id === positionId); if (pos) { pos.folder = folderName; localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); addFolder(folderName); showPopup(langStrings[currentLanguage].positionMoved.replace("{folder}", folderName)); updateAllFolderDropdowns(); renderCompletedPositions(); } }; function deleteAllArchivedPositions() { const dropdown = document.getElementById("gg-folder-completed"); let selectedFolder = dropdown ? dropdown.value.trim() : ""; if (!selectedFolder) { showPopup(langStrings[currentLanguage].selectFolder); return; } if (confirm(langStrings[currentLanguage].confirmDeleteAllArchived.replace("{folder}", selectedFolder))) { let positions = getSavedPositions(); positions = positions.filter(p => !(p.completed && p.folder.trim() === selectedFolder)); localStorage.setItem("geoguessr_saved_positions", JSON.stringify(positions)); renderCompletedPositions(); updateAllFolderDropdowns(); } } // ===================================================== // 13. Hotkeys and Menu Toggle // ===================================================== function toggleMenu() { if (isMultiplayer()) { const indicator = document.getElementById('quickSaveIndicator'); if (indicator) { indicator.remove(); window.quickSaveToggledOff = true; } else { showQuickSaveIndicator(); window.quickSaveToggledOff = false; } } else { const menu = document.getElementById('geoSaverMenu'); if (menu) { menu.remove(); } else { createMainMenu(); } } } document.addEventListener('keydown', (e) => { if (e.altKey && e.code === "KeyS") { e.preventDefault(); quickSave(); } }, true); document.addEventListener('keydown', (e) => { if (e.altKey && e.code === "KeyG") { e.preventDefault(); toggleMenu(); } }); // ===================================================== // 14. Auto Initialization on Page Load and SPA Changes // ===================================================== function initMenu() { console.log("initMenu fired, URL:", window.location.href); if (isMultiplayer()) { const existing = document.getElementById('geoSaverMenu'); if (existing) existing.remove(); showQuickSaveIndicator(); } else { if (!document.getElementById('geoSaverMenu')) { createMainMenu(); } } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initMenu); } else { initMenu(); } window.addEventListener('locationchange', () => { const existing = document.getElementById('geoSaverMenu'); if (existing) existing.remove(); initMenu(); }); // ===================================================== // 15. Expose Global Functions for Inline Handlers // ===================================================== window.renderTrainingPositions = renderTrainingPositions; window.renderCompletedPositions = renderCompletedPositions; window.moveToArchive = moveToArchive; window.moveToTraining = moveToTraining; window.deletePosition = deletePosition; window.moveQuickSaveToTraining = window.moveQuickSaveToTraining; window.moveArchiveToFolder = moveArchiveToFolder; console.log("GeoSaver Complete Script Loaded"); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址