// ==UserScript== // @name GeoGuessr Wrapped // @namespace https://github.com/lonanche/geoguessr-wrapped // @version 1.1.2 // @description Fetch all 2025 games and show top 20 most played maps with image generation // @author trausi // @match https://www.geoguessr.com/me/activities // @icon https://www.geoguessr.com/_next/static/media/favicon.bffdd9d3.png // @updateURL https://raw.githubusercontent.com/lonanche/geoguessr-wrapped/main/geoguessr-wrapped.user.js // @downloadURL https://raw.githubusercontent.com/lonanche/geoguessr-wrapped/main/geoguessr-wrapped.user.js // @supportURL https://github.com/lonanche/geoguessr-wrapped/issues // @grant none // ==/UserScript== (function() { 'use strict'; async function fetchWithCredentials(url, options = {}) { const defaultOptions = { credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...options.headers } }; const response = await fetch(url, { ...defaultOptions, ...options }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } async function fetchAll2025Games(progressCallback) { const mapCounts = {}; let totalGames = 0; let paginationToken = null; let foundPre2025 = false; let pageCount = 0; try { while (!foundPre2025) { const url = new URL('https://www.geoguessr.com/api/v4/feed/private'); if (paginationToken) { url.searchParams.append('paginationToken', paginationToken); } const data = await fetchWithCredentials(url.toString()); paginationToken = data.paginationToken; pageCount++; data.entries.forEach(entry => { try { if (entry.type !== 7) return; const payloadJson = JSON.parse(entry.payload); const payloadArray = Array.isArray(payloadJson) ? payloadJson : [payloadJson]; payloadArray.forEach(payload => { if (payload.type === 1 && payload.payload) { const gameData = payload.payload; const gameDate = new Date(payload.time || entry.time || ''); if (gameDate.getFullYear() < 2025) { foundPre2025 = true; return; } if (gameDate.getFullYear() === 2025 && gameData.gameMode === 'Standard' && gameData.mapSlug && gameData.mapName) { // Use mapSlug as the unique identifier to handle renamed maps const mapId = gameData.mapSlug; if (!mapCounts[mapId]) { // First time seeing this map (from newest to oldest) // So this is the most recent name mapCounts[mapId] = { mapSlug: gameData.mapSlug, mapName: gameData.mapName, count: 0 }; } // Don't update mapName if we already have it (keep the newest name) mapCounts[mapId].count++; totalGames++; } } }); } catch (error) { // Skip malformed entries } }); if (progressCallback) { progressCallback({ totalGames, pageCount, uniqueMaps: Object.keys(mapCounts).length }); } if (!paginationToken || foundPre2025) { break; } await new Promise(resolve => setTimeout(resolve, 100)); } const sortedMaps = Object.values(mapCounts) .sort((a, b) => b.count - a.count) .slice(0, 30); return { totalGames, top30Maps: sortedMaps, uniqueMaps: Object.keys(mapCounts).length, allMaps: Object.values(mapCounts).sort((a, b) => b.count - a.count) }; } catch (error) { throw error; } } function add2025MapAnalysisUI() { const activitiesHeading = document.querySelector('h1.headline_heading__2lf9L'); if (!activitiesHeading || !activitiesHeading.textContent.includes('Activities')) { return; } if (document.querySelector('.map-2025-container')) { return; } const container = document.createElement('div'); container.className = 'map-2025-container'; container.style.cssText = ` margin: 32px 0; padding: 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%); border-radius: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08); `; const header = document.createElement('div'); header.style.cssText = ` display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px solid #e5e7eb; gap: 24px; `; const title = document.createElement('h2'); title.textContent = 'GeoGuessr Wrapped 2025'; title.style.cssText = ` margin: 0; font-size: 24px; font-weight: 700; color: #111827; letter-spacing: -0.025em; `; const loadButton = document.createElement('button'); loadButton.className = 'load-2025-button'; loadButton.textContent = 'Start'; loadButton.style.cssText = ` padding: 12px 24px; background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: #fff; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s ease; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); letter-spacing: 0.015em; `; const progressContainer = document.createElement('div'); progressContainer.className = 'progress-container'; progressContainer.style.cssText = ` display: none; margin: 32px 0; padding: 24px; background: #f9fafb; border-radius: 12px; `; const progressText = document.createElement('div'); progressText.className = 'progress-text'; progressText.style.cssText = ` font-size: 13px; color: #6b7280; margin-bottom: 12px; font-weight: 500; `; const progressBar = document.createElement('div'); progressBar.style.cssText = ` width: 100%; height: 6px; background: #e5e7eb; border-radius: 6px; overflow: hidden; `; const progressFill = document.createElement('div'); progressFill.style.cssText = ` height: 100%; background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%); border-radius: 6px; width: 0%; transition: width 0.3s ease; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.2); `; progressBar.appendChild(progressFill); progressContainer.appendChild(progressText); progressContainer.appendChild(progressBar); const resultsContainer = document.createElement('div'); resultsContainer.className = 'results-2025-container'; resultsContainer.style.cssText = ` display: none; `; const statsContainer = document.createElement('div'); statsContainer.style.cssText = ` display: flex; gap: 24px; margin-bottom: 40px; `; const tableControls = document.createElement('div'); tableControls.style.cssText = ` display: none; margin-bottom: 16px; align-items: center; gap: 12px; `; const showAllLabel = document.createElement('label'); showAllLabel.style.cssText = ` font-size: 13px; color: #6b7280; font-weight: 500; `; showAllLabel.textContent = 'Show all maps:'; const showAllCheckbox = document.createElement('input'); showAllCheckbox.type = 'checkbox'; showAllCheckbox.className = 'show-all-checkbox'; showAllCheckbox.style.cssText = ` width: 16px; height: 16px; accent-color: #3b82f6; `; const mapCountLabel = document.createElement('label'); mapCountLabel.style.cssText = ` font-size: 13px; color: #6b7280; font-weight: 500; margin-left: 24px; `; mapCountLabel.textContent = 'Maps in image:'; const mapCountInput = document.createElement('input'); mapCountInput.type = 'number'; mapCountInput.className = 'map-count-input'; mapCountInput.value = '15'; mapCountInput.min = '1'; mapCountInput.max = '20'; mapCountInput.style.cssText = ` width: 60px; padding: 4px 8px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px; text-align: center; `; const generateImageButton = document.createElement('button'); generateImageButton.className = 'generate-image-button'; generateImageButton.textContent = 'Generate Image'; generateImageButton.style.cssText = ` padding: 8px 16px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2); margin-left: auto; `; tableControls.appendChild(showAllLabel); tableControls.appendChild(showAllCheckbox); tableControls.appendChild(mapCountLabel); tableControls.appendChild(mapCountInput); tableControls.appendChild(generateImageButton); const mapsTable = document.createElement('div'); mapsTable.className = 'maps-table'; mapsTable.style.cssText = ` background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; max-height: 600px; overflow-y: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.05); `; header.appendChild(title); header.appendChild(loadButton); container.appendChild(header); container.appendChild(progressContainer); container.appendChild(resultsContainer); resultsContainer.appendChild(statsContainer); resultsContainer.appendChild(tableControls); resultsContainer.appendChild(mapsTable); let allMapsData = null; loadButton.addEventListener('mouseenter', () => { if (!loadButton.disabled) { loadButton.style.transform = 'translateY(-1px)'; loadButton.style.boxShadow = '0 6px 20px rgba(37, 99, 235, 0.3)'; } }); loadButton.addEventListener('mouseleave', () => { if (!loadButton.disabled) { loadButton.style.transform = 'translateY(0)'; loadButton.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.2)'; } }); generateImageButton.addEventListener('mouseenter', () => { if (!generateImageButton.disabled) { generateImageButton.style.transform = 'translateY(-1px)'; generateImageButton.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.3)'; } }); generateImageButton.addEventListener('mouseleave', () => { if (!generateImageButton.disabled) { generateImageButton.style.transform = 'translateY(0)'; generateImageButton.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.2)'; } }); loadButton.addEventListener('click', async () => { loadButton.disabled = true; loadButton.textContent = 'Analyzing...'; loadButton.style.opacity = '0.7'; loadButton.style.transform = 'scale(0.98)'; progressContainer.style.display = 'block'; resultsContainer.style.display = 'none'; try { const result = await fetchAll2025Games((progress) => { progressText.textContent = `Fetching games • ${progress.totalGames} found • ${progress.uniqueMaps} maps`; progressFill.style.width = `${Math.min((progress.pageCount / 50) * 100, 95)}%`; }); allMapsData = result; progressFill.style.width = '100%'; setTimeout(() => { progressContainer.style.display = 'none'; statsContainer.innerHTML = `
| Rank | Map | Plays | Share |
|---|---|---|---|
| ${index + 1} | ${map.mapName} | ${map.count} | ${percentage}% |