您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Find duels against banned cheaters with enhanced features
// ==UserScript== // @name Lunar Finder - Enhanced Duel Finder // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description Find duels against banned cheaters with enhanced features // @author Neo // @match https://www.geoguessr.com/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @run-at document-start // ==/UserScript== (function() { 'use strict'; let isMenuOpen = false; let menu = null; let myUserId = null; let storedMatches = []; let currentResults = []; let logs = []; let bannedPlayers = []; let lastScanResults = []; const styles = ` @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap'); #lunar-finder-menu { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 900px; height: 600px; background: rgba(15, 15, 15, 0.95); backdrop-filter: blur(20px); border-radius: 16px; border: 1px solid rgba(255, 20, 147, 0.2); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); display: flex; font-family: 'Geist', sans-serif; z-index: 10000; overflow: hidden; } #lunar-finder-sidebar { width: 240px; background: rgba(20, 20, 20, 0.8); border-right: 1px solid rgba(255, 20, 147, 0.1); padding: 24px 0; display: flex; flex-direction: column; } #lunar-finder-brand { padding: 0 24px 32px; margin-bottom: 24px; } #lunar-finder-brand h1 { color: #ffffff; font-size: 28px; font-weight: 700; margin: 0; letter-spacing: -0.5px; } #lunar-finder-brand .accent { color: #ff1493; } #lunar-finder-nav { flex: 1; padding: 0 16px; } .nav-item { display: flex; align-items: center; padding: 12px 16px; margin: 4px 0; border-radius: 12px; color: rgba(255, 255, 255, 0.7); text-decoration: none; font-weight: 500; font-size: 14px; transition: all 0.2s ease; cursor: pointer; border: none; background: transparent; width: 100%; text-align: left; } .nav-item:hover { background: rgba(255, 20, 147, 0.1); color: #ffffff; } .nav-item.active { background: rgba(255, 20, 147, 0.15); color: #ff1493; box-shadow: 0 0 0 1px rgba(255, 20, 147, 0.3); } .nav-item svg { width: 18px; height: 18px; margin-right: 12px; fill: currentColor; } #lunar-finder-sidebar .nav-item[data-tab="logs"] { margin-right: 0; margin-left: 0; } #lunar-finder-sidebar .nav-item[data-tab="logs"] svg { margin-right: 0; margin-left: 0; } #lunar-finder-content { flex: 1; padding: 24px; overflow-y: auto; } .content-tab { display: none; height: 100%; } .content-tab.active { display: block; } .tab-title { color: #ffffff; font-size: 24px; font-weight: 600; margin: 0 0 24px 0; letter-spacing: -0.5px; display: flex; align-items: center; justify-content: space-between; } .refresh-button { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; padding: 6px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .refresh-button:hover { background: rgba(255, 20, 147, 0.2); border-color: rgba(255, 20, 147, 0.4); } .refresh-button svg { width: 16px; height: 16px; fill: rgba(255, 255, 255, 0.7); } .refresh-button:hover svg { fill: #ff1493; } .input-group { margin-bottom: 20px; } .input-label { display: block; color: rgba(255, 255, 255, 0.8); font-size: 14px; font-weight: 500; margin-bottom: 8px; } .input-field { width: 100%; padding: 12px 16px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: #ffffff; font-family: 'Geist', sans-serif; font-size: 14px; outline: none; transition: all 0.2s ease; box-sizing: border-box; } .input-field:focus { border-color: #ff1493; box-shadow: 0 0 0 3px rgba(255, 20, 147, 0.1); } .input-field::placeholder { color: rgba(255, 255, 255, 0.4); } .button { background: #ff1493; color: #ffffff; border: none; border-radius: 8px; padding: 12px 24px; font-size: 14px; font-weight: 600; font-family: 'Geist', sans-serif; cursor: pointer; transition: all 0.2s ease; margin-right: 12px; margin-bottom: 12px; } .button:hover { background: rgba(255, 20, 147, 0.8); transform: translateY(-1px); } .button:disabled { background: rgba(255, 255, 255, 0.2); cursor: not-allowed; transform: none; } .button.secondary { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); } .button.secondary:hover { background: rgba(255, 255, 255, 0.15); } .stats-container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-bottom: 24px; } .stat-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px; text-align: center; } .stat-number { color: #ff1493; font-size: 28px; font-weight: 700; margin: 0 0 8px 0; } .stat-label { color: rgba(255, 255, 255, 0.7); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } .progress-container { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 16px; margin-bottom: 20px; display: none; } .progress-text { color: rgba(255, 255, 255, 0.8); font-size: 14px; margin-bottom: 8px; } .progress-bar { width: 100%; height: 4px; background: rgba(255, 255, 255, 0.1); border-radius: 2px; overflow: hidden; } .progress-fill { height: 100%; background: #ff1493; width: 0%; transition: width 0.3s ease; } .results-container { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px; max-height: 400px; overflow-y: auto; display: none; } .result-item { background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px; padding: 16px; margin-bottom: 12px; transition: all 0.2s ease; } .result-item:hover { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 20, 147, 0.3); } .result-date { color: #ff1493; font-size: 12px; font-weight: 600; margin-bottom: 4px; } .result-username { color: #ffffff; font-size: 16px; font-weight: 600; margin-bottom: 8px; } .result-link { color: rgba(255, 255, 255, 0.7); font-size: 14px; text-decoration: none; word-break: break-all; } .result-link:hover { color: #ff1493; } .file-upload { border: 2px dashed rgba(255, 255, 255, 0.2); border-radius: 8px; padding: 40px 20px; text-align: center; cursor: pointer; transition: all 0.2s ease; margin-bottom: 20px; } .file-upload:hover { border-color: #ff1493; background: rgba(255, 20, 147, 0.05); } .file-upload.dragover { border-color: #ff1493; background: rgba(255, 20, 147, 0.1); } .file-upload-text { color: rgba(255, 255, 255, 0.7); font-size: 14px; margin-bottom: 8px; } .file-upload-hint { color: rgba(255, 255, 255, 0.4); font-size: 12px; } .uuid-display { background: rgba(255, 20, 147, 0.1); border: 1px solid rgba(255, 20, 147, 0.3); border-radius: 8px; padding: 16px; margin-bottom: 20px; } .uuid-label { color: rgba(255, 255, 255, 0.7); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; } .uuid-value { color: #ff1493; font-size: 16px; font-weight: 600; font-family: 'Geist Mono', monospace; word-break: break-all; } #lunar-finder-content::-webkit-scrollbar { width: 10px; } #lunar-finder-content::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.03); border-radius: 5px; } #lunar-finder-content::-webkit-scrollbar-thumb { background: rgba(255, 20, 147, 0.4); border-radius: 5px; border: 1px solid rgba(255, 20, 147, 0.2); } #lunar-finder-content::-webkit-scrollbar-thumb:hover { background: rgba(255, 20, 147, 0.6); } `; const styleSheet = document.createElement('style'); styleSheet.textContent = styles; document.head.appendChild(styleSheet); function createSVGIcon(type) { const icons = { home: '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/>', search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>', users: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="m23 21-2-2"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>', settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1 1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>', refresh: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/>', logs: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10,9 9,9 8,9"/>', discord: '<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>' }; return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${icons[type] || icons.home}</svg>`; } function createMenu() { if (menu) { menu.remove(); } menu = document.createElement('div'); menu.id = 'lunar-finder-menu'; menu.style.display = 'none'; const sidebar = document.createElement('div'); sidebar.id = 'lunar-finder-sidebar'; const brand = document.createElement('div'); brand.id = 'lunar-finder-brand'; brand.innerHTML = '<h1>Lunar <span class="accent">Finder</span></h1>'; const nav = document.createElement('div'); nav.id = 'lunar-finder-nav'; const navItems = [ { id: 'home', label: 'Home', icon: 'home' }, { id: 'single', label: 'Single Search', icon: 'search' }, { id: 'multi', label: 'Multi Search', icon: 'users' } ]; const bottomNavItems = [ { id: 'logs', label: 'Logs', icon: 'logs' }, { id: 'discord', label: 'Discord', icon: 'discord', isExternal: true, url: 'https://discord.gg/TmxEA7RTuQ' } ]; navItems.forEach(item => { const navItem = document.createElement('button'); navItem.className = 'nav-item'; navItem.setAttribute('data-tab', item.id); navItem.innerHTML = createSVGIcon(item.icon) + item.label; navItem.addEventListener('click', () => switchTab(item.id)); nav.appendChild(navItem); }); const bottomNav = document.createElement('div'); bottomNav.style.display = 'flex'; bottomNav.style.justifyContent = 'space-between'; bottomNav.style.gap = '12px'; bottomNav.style.marginTop = 'auto'; bottomNav.style.paddingTop = '24px'; bottomNav.style.paddingLeft = '20px'; bottomNav.style.paddingRight = '20px'; bottomNav.style.paddingBottom = '8px'; bottomNavItems.forEach(item => { const navItem = document.createElement('button'); navItem.className = 'nav-item'; navItem.setAttribute('data-tab', item.id); navItem.style.justifyContent = 'center'; navItem.style.alignItems = 'center'; navItem.style.display = 'flex'; navItem.style.padding = '0'; navItem.style.width = '56px'; navItem.style.height = '56px'; navItem.style.borderRadius = '12px'; navItem.style.margin = '0'; navItem.style.border = 'none'; navItem.style.background = 'transparent'; navItem.innerHTML = createSVGIcon(item.icon); const svg = navItem.querySelector('svg'); if (svg) { svg.style.width = '24px'; svg.style.height = '24px'; svg.style.display = 'block'; svg.style.margin = 'auto'; } if (item.isExternal && item.url) { navItem.addEventListener('click', () => window.open(item.url, '_blank')); } else { navItem.addEventListener('click', () => switchTab(item.id)); } bottomNav.appendChild(navItem); }); const content = document.createElement('div'); content.id = 'lunar-finder-content'; const homeTab = createHomeTab(); const singleTab = createSingleTab(); const multiTab = createMultiTab(); const logsTab = createLogsTab(); content.appendChild(homeTab); content.appendChild(singleTab); content.appendChild(multiTab); content.appendChild(logsTab); sidebar.appendChild(brand); sidebar.appendChild(nav); sidebar.appendChild(bottomNav); menu.appendChild(sidebar); menu.appendChild(content); document.body.appendChild(menu); setTimeout(() => { fetchMyUUID(); }, 100); } function createHomeTab() { const tab = document.createElement('div'); tab.id = 'home-tab'; tab.className = 'content-tab'; const title = document.createElement('h2'); title.className = 'tab-title'; title.innerHTML = ` <span>Home</span> <button class="refresh-button" id="refresh-uuid-btn" title="Refresh UUID"> ${createSVGIcon('refresh')} </button> `; const uuidDisplay = document.createElement('div'); uuidDisplay.className = 'uuid-display'; uuidDisplay.innerHTML = ` <div class="uuid-label">Your UUID</div> <div class="uuid-value" id="my-uuid">Loading...</div> `; const statsContainer = document.createElement('div'); statsContainer.className = 'stats-container'; const storedMatchesCard = document.createElement('div'); storedMatchesCard.className = 'stat-card'; storedMatchesCard.id = 'stored-matches-card'; const totalDuelsCard = document.createElement('div'); totalDuelsCard.className = 'stat-card'; totalDuelsCard.id = 'total-duels-card'; const oldestMatchCard = document.createElement('div'); oldestMatchCard.className = 'stat-card'; oldestMatchCard.id = 'oldest-match-card'; const fetchButton = document.createElement('button'); fetchButton.className = 'button'; fetchButton.textContent = 'Fetch My Matches'; fetchButton.id = 'fetch-matches-btn'; const clearButton = document.createElement('button'); clearButton.className = 'button secondary'; clearButton.textContent = 'Clear Stored Data'; clearButton.id = 'clear-stored-btn'; clearButton.style.marginLeft = '12px'; const progressContainer = document.createElement('div'); progressContainer.className = 'progress-container'; progressContainer.id = 'progress-container'; progressContainer.innerHTML = ` <div class="progress-text" id="progress-text">Processing...</div> <div class="progress-bar"> <div class="progress-fill" id="progress-fill"></div> </div> `; statsContainer.appendChild(storedMatchesCard); statsContainer.appendChild(totalDuelsCard); statsContainer.appendChild(oldestMatchCard); const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.alignItems = 'center'; buttonContainer.appendChild(fetchButton); buttonContainer.appendChild(clearButton); tab.appendChild(title); tab.appendChild(uuidDisplay); tab.appendChild(statsContainer); tab.appendChild(buttonContainer); tab.appendChild(progressContainer); loadStoredData(); return tab; } function createSingleTab() { const tab = document.createElement('div'); tab.id = 'single-tab'; tab.className = 'content-tab'; const titleRow = document.createElement('div'); titleRow.style.display = 'flex'; titleRow.style.alignItems = 'center'; titleRow.style.justifyContent = 'space-between'; titleRow.style.marginBottom = '18px'; const title = document.createElement('h2'); title.className = 'tab-title'; title.textContent = 'Single Search'; const targetUuidGroup = document.createElement('div'); targetUuidGroup.className = 'input-group'; targetUuidGroup.innerHTML = ` <label class="input-label">Target UUID</label> <input type="text" class="input-field" id="target-uuid" placeholder="Enter UUID or profile URL"> `; const maxMatchesGroup = document.createElement('div'); maxMatchesGroup.className = 'input-group'; maxMatchesGroup.innerHTML = ` <label class="input-label">Max Matches to Search</label> <input type="number" class="input-field" id="max-matches" value="50" min="1" max="500"> `; const searchButton = document.createElement('button'); searchButton.className = 'button'; searchButton.textContent = 'Search Duels'; searchButton.id = 'single-search-btn'; searchButton.style.height = '44px'; searchButton.style.minHeight = '44px'; const actionButtons = document.createElement('div'); actionButtons.style.display = 'flex'; actionButtons.style.gap = '12px'; actionButtons.style.alignItems = 'center'; actionButtons.innerHTML = ` <button class="button secondary" id="single-copy-results-btn" style="display: none; height: 44px; min-height: 44px;">Copy Results</button> <button class="button secondary" id="single-download-csv-btn" style="display: none; height: 44px; min-height: 44px;">Download CSV</button> `; const progressContainer = document.createElement('div'); progressContainer.className = 'progress-container'; progressContainer.id = 'single-progress-container'; progressContainer.innerHTML = ` <div class="progress-text" id="single-progress-text">Processing...</div> <div class="progress-bar"> <div class="progress-fill" id="single-progress-fill"></div> </div> `; const resultsContainer = document.createElement('div'); resultsContainer.className = 'results-container'; resultsContainer.id = 'single-results-container'; const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '12px'; buttonContainer.style.alignItems = 'center'; buttonContainer.style.justifyContent = 'flex-start'; buttonContainer.appendChild(searchButton); buttonContainer.appendChild(actionButtons); tab.appendChild(title); tab.appendChild(targetUuidGroup); tab.appendChild(maxMatchesGroup); tab.appendChild(buttonContainer); tab.appendChild(progressContainer); tab.appendChild(resultsContainer); return tab; } function createMultiTab() { const tab = document.createElement('div'); tab.id = 'multi-tab'; tab.className = 'content-tab'; const title = document.createElement('h2'); title.className = 'tab-title'; title.textContent = 'Multi Search'; const fileUpload = document.createElement('div'); fileUpload.className = 'file-upload'; fileUpload.id = 'file-upload'; fileUpload.innerHTML = ` <div class="file-upload-text">Drop JSON/CSV file here or click to browse</div> <div class="file-upload-hint">Format: Date,Username,UserID,Profile_URL,CountryCode,ELO,Position,Action_Type,Suspended_Until</div> <input type="file" id="json-file" accept=".json,.csv" style="display: none;"> `; const resetBtn = document.createElement('button'); resetBtn.className = 'button secondary'; resetBtn.textContent = 'Reset'; resetBtn.style.marginLeft = '12px'; resetBtn.style.display = 'none'; fileUpload.appendChild(resetBtn); const playerListContainer = document.createElement('div'); playerListContainer.id = 'multi-player-list'; playerListContainer.style.marginTop = '20px'; playerListContainer.style.maxHeight = '400px'; playerListContainer.style.overflowY = 'auto'; playerListContainer.style.scrollbarWidth = 'none'; playerListContainer.style.msOverflowStyle = 'none'; const style = document.createElement('style'); style.textContent = `#multi-player-list::-webkit-scrollbar { display: none !important; }`; document.head.appendChild(style); tab.appendChild(title); tab.appendChild(fileUpload); tab.appendChild(playerListContainer); const scanButton = document.createElement('button'); scanButton.className = 'button'; scanButton.textContent = 'Scan All'; scanButton.style.margin = '16px 0 16px 0'; const exportAllButton = document.createElement('button'); exportAllButton.className = 'button secondary'; exportAllButton.textContent = 'Export All'; exportAllButton.style.margin = '16px 0 16px 12px'; exportAllButton.style.display = 'none'; const multiProgressContainer = document.createElement('div'); multiProgressContainer.className = 'progress-container'; multiProgressContainer.id = 'multi-progress-container'; multiProgressContainer.style.display = 'none'; multiProgressContainer.style.marginBottom = '12px'; multiProgressContainer.innerHTML = ` <div class="progress-text" id="multi-progress-text">Processing...</div> <div class="progress-bar"> <div class="progress-fill" id="multi-progress-fill"></div> </div> `; tab.appendChild(multiProgressContainer); function renderScanControls() { if (scanButton.parentElement) scanButton.parentElement.removeChild(scanButton); if (exportAllButton.parentElement) exportAllButton.parentElement.removeChild(exportAllButton); playerListContainer.parentElement.insertBefore(scanButton, playerListContainer); playerListContainer.parentElement.insertBefore(exportAllButton, playerListContainer); } function handleMultiFile(file) { setFileLoadedUI(file.name); if (file.name.endsWith('.csv')) { handleMultiCsv(file); } else { handleMultiJson(file); } } function handleMultiJson(file) { const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); bannedPlayers = data.filter(entry => entry.Action_Type && entry.Action_Type.toLowerCase().includes('banned') ); renderScanControls(); playerListContainer.innerHTML = '<div style="color: #ff1493; text-align:center; padding:40px;">Ready to scan. Click Scan All.</div>'; } catch (error) { showNotification('Invalid JSON file', 'error'); } }; reader.readAsText(file); } function handleMultiCsv(file) { const reader = new FileReader(); reader.onload = (e) => { try { const text = e.target.result; const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0); if (!lines.length) throw new Error('Empty CSV'); const headers = lines[0].split(',').map(h => h.trim()); const data = lines.slice(1).map(line => { const cols = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { if (inQuotes && line[i+1] === '"') { current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === ',' && !inQuotes) { cols.push(current.trim()); current = ''; } else { current += char; } } cols.push(current.trim()); while (cols.length < headers.length) cols.push(''); const obj = {}; headers.forEach((h, i) => { obj[h] = cols[i] || ''; }); return obj; }).filter(row => Object.values(row).some(v => v)); if (!data.length) throw new Error('No valid data rows'); bannedPlayers = data.filter(entry => entry.UserID && entry.Action_Type && entry.Action_Type.toLowerCase().includes('banned') ); renderScanControls(); playerListContainer.innerHTML = '<div style="color: #ff1493; text-align:center; padding:40px;">Ready to scan. Click Scan All.</div>'; } catch (error) { showNotification('Invalid CSV file: ' + error.message, 'error'); } }; reader.readAsText(file); } scanButton.onclick = async () => { scanButton.disabled = true; scanButton.textContent = 'Scanning...'; exportAllButton.style.display = 'none'; multiProgressContainer.style.display = 'block'; const progressText = multiProgressContainer.querySelector('#multi-progress-text'); const progressFill = multiProgressContainer.querySelector('#multi-progress-fill'); const userMap = {}; bannedPlayers.forEach(player => { userMap[player.UserID] = { player, matches: [] }; }); let scanned = 0; const total = storedMatches.length; const batchSize = 3; for (let i = 0; i < total; i += batchSize) { const batch = storedMatches.slice(i, i + batchSize); await Promise.all(batch.map(async (match) => { try { const duelData = await fetchDuelDetailsGM(match.gameId); if (duelData.teams) { for (const team of duelData.teams) { if (team.players) { for (const p of team.players) { if (userMap[p.playerId]) { userMap[p.playerId].matches.push({ gameId: match.gameId, time: match.time, gameLink: `https://www.geoguessr.com/duels/${match.gameId}/summary`, }); } } } } } } catch (e) {} })); scanned += batch.length; progressText.textContent = `Scanning: ${scanned} of ${total}`; progressFill.style.width = `${(scanned / total) * 100}%`; await new Promise(resolve => setTimeout(resolve, 200)); } const results = Object.values(userMap).filter(u => u.matches.length > 0); lastScanResults = results; renderScanResults(results); scanButton.textContent = 'Scan All'; scanButton.disabled = false; exportAllButton.style.display = results.length > 0 ? 'inline-block' : 'none'; multiProgressContainer.style.display = 'none'; }; exportAllButton.onclick = () => { if (!lastScanResults.length) return; const allRows = []; lastScanResults.forEach(u => { u.matches.forEach(m => { const dateObj = new Date(m.time); const day = dateObj.getDate().toString().padStart(2, '0'); const month = (dateObj.getMonth() + 1).toString().padStart(2, '0'); const date = `${day}/${month}`; allRows.push([ date, u.player.Username, m.gameLink ]); }); }); const headers = ['Date', 'Username', 'Game Link']; const csv = [headers, ...allRows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `duels_all_banned.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; function exportPlayerMatches(player, matches) { const allRows = []; matches.forEach(m => { const dateObj = new Date(m.time); const day = dateObj.getDate().toString().padStart(2, '0'); const month = (dateObj.getMonth() + 1).toString().padStart(2, '0'); const date = `${day}/${month}`; allRows.push([ date, player.Username, m.gameLink ]); }); const headers = ['Date', 'Username', 'Game Link']; const csv = [headers, ...allRows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `duels_${player.Username.replace(/[^a-zA-Z0-9]/g, '_')}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function renderScanResults(results) { playerListContainer.innerHTML = ''; const table = document.createElement('table'); table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.innerHTML = ` <thead><tr style="background:rgba(255,255,255,0.05);"> <th style="padding:8px; text-align:left; color:#ff1493;">Username</th> <th style="padding:8px; text-align:left; color:#ff1493; max-width:220px;">UserID</th> <th style="padding:8px; text-align:left; color:#ff1493; max-width:100px;">Matches</th> <th style="padding:8px; text-align:left; color:#ff1493;">Export</th> </tr></thead> <tbody></tbody> `; const tbody = table.querySelector('tbody'); results.forEach((u, idx) => { const tr = document.createElement('tr'); tr.style.background = 'rgba(255,255,255,0.02)'; tr.style.borderRadius = '12px'; tr.style.boxShadow = '0 2px 12px rgba(0,0,0,0.08)'; tr.style.border = '2px solid #ff1493'; tr.style.transition = 'box-shadow 0.3s, background 0.3s'; tr.innerHTML = ` <td style="padding:14px 8px; color:#fff; font-weight:500; border:none;">${u.player.Username}</td> <td style="padding:14px 8px; font-size:12px; color:#ff1493; text-decoration:underline; cursor:pointer; max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border:none;" id="userlink-${idx}">${u.player.UserID}</td> <td style="padding:14px 8px; color:#fff; font-weight:600; border:none;">${u.matches.length}</td> <td style="padding:8px; border:none;"><button class='button secondary' id='export-player-${idx}'>Export</button></td> `; tbody.appendChild(tr); setTimeout(() => { const link = document.getElementById(`userlink-${idx}`); if (link) { link.onclick = (e) => { window.open(`https://www.geoguessr.com/user/${u.player.UserID}`, '_blank'); }; link.onmouseover = () => link.style.textDecoration = 'underline'; link.onmouseout = () => link.style.textDecoration = 'underline'; } const exportBtn = document.getElementById(`export-player-${idx}`); if (exportBtn) { exportBtn.onclick = () => { exportPlayerMatches(u.player, u.matches); }; } }, 0); }); playerListContainer.appendChild(table); } const fileInput = fileUpload.querySelector('#json-file'); fileInput.setAttribute('accept', '.json,.csv'); fileUpload.addEventListener('click', () => fileInput.click()); fileUpload.addEventListener('dragover', (e) => { e.preventDefault(); fileUpload.classList.add('dragover'); }); fileUpload.addEventListener('dragleave', () => { fileUpload.classList.remove('dragover'); }); fileUpload.addEventListener('drop', (e) => { e.preventDefault(); fileUpload.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length > 0) { fileInput.files = files; handleMultiFile(files[0]); } }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleMultiFile(e.target.files[0]); } }); resetBtn.onclick = () => { const fileInput = fileUpload.querySelector('#json-file'); fileInput.value = ''; resetFileUI(); }; return tab; } function createLogsTab() { const tab = document.createElement('div'); tab.id = 'logs-tab'; tab.className = 'content-tab'; const title = document.createElement('h2'); title.className = 'tab-title'; title.innerHTML = ` <span>Logs</span> <button class="refresh-button" id="clear-logs-btn" title="Clear Logs" style="margin-left: 12px;"> ${createSVGIcon('refresh')} </button> `; const logsContainer = document.createElement('div'); logsContainer.className = 'results-container'; logsContainer.id = 'logs-container'; logsContainer.style.display = 'block'; logsContainer.style.maxHeight = '400px'; const actionButtons = document.createElement('div'); actionButtons.style.marginTop = '20px'; actionButtons.innerHTML = ` <button class="button secondary" id="export-logs-btn">Export Logs</button> `; tab.appendChild(title); tab.appendChild(logsContainer); tab.appendChild(actionButtons); return tab; } function switchTab(tabId) { const navItems = document.querySelectorAll('.nav-item'); const tabs = document.querySelectorAll('.content-tab'); navItems.forEach(item => { item.classList.remove('active'); if (item.getAttribute('data-tab') === tabId) { item.classList.add('active'); } }); tabs.forEach(tab => { tab.classList.remove('active'); if (tab.id === `${tabId}-tab`) { tab.classList.add('active'); } }); } function toggleMenu() { if (!menu) { createMenu(); addEventListeners(); } isMenuOpen = !isMenuOpen; menu.style.display = isMenuOpen ? 'flex' : 'none'; if (isMenuOpen) { loadStoredData(); updateStats(); updateLogsDisplay(); switchTab('home'); } } async function fetchMyUUID() { const uuidElement = document.getElementById('my-uuid'); if (uuidElement) { uuidElement.textContent = 'Fetching...'; } addLog('Fetching UUID...', 'info'); try { console.log('Fetching UUID...'); let response = await fetch('https://www.geoguessr.com/api/v3/profiles', { credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); console.log('Response status:', response.status); if (response.ok) { const data = await response.json(); console.log('Profile data:', data); if (data.user && data.user.id) { myUserId = data.user.id; } else if (data.id) { myUserId = data.id; } else if (data.userId) { myUserId = data.userId; } else { console.log('No user ID found in response structure:', data); throw new Error('No user ID found in response'); } if (uuidElement) { uuidElement.textContent = myUserId; console.log('UUID updated:', myUserId); addLog(`UUID fetched successfully: ${myUserId}`, 'info'); } else { console.log('UUID element not found'); addLog('UUID element not found', 'warning'); } localStorage.setItem('lunar_finder_my_uuid', myUserId); return; } console.log('First endpoint failed, trying alternative...'); response = await fetch('https://www.geoguessr.com/api/v4/stats/me', { credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); console.log('Stats data:', data); if (data.userId) { myUserId = data.userId; } else if (data.id) { myUserId = data.id; } else { throw new Error('No user ID found in response'); } if (uuidElement) { uuidElement.textContent = myUserId; console.log('UUID updated from stats:', myUserId); } localStorage.setItem('lunar_finder_my_uuid', myUserId); return; } console.log('Both endpoints failed'); addLog(`Failed to fetch UUID: HTTP ${response.status}`, 'error'); if (uuidElement) { uuidElement.textContent = `Error: ${response.status}`; } } catch (error) { console.error('Error fetching UUID:', error); addLog(`Error fetching UUID: ${error.message}`, 'error'); if (uuidElement) { uuidElement.textContent = 'Error fetching UUID'; } } } function loadStoredData() { try { const stored = localStorage.getItem('lunar_finder_matches'); if (stored) { storedMatches = JSON.parse(stored); console.log(`Loaded ${storedMatches.length} matches from storage`); } else { storedMatches = []; console.log('No stored matches found'); } updateStats(); } catch (error) { console.error('Error loading stored data:', error); storedMatches = []; try { localStorage.removeItem('lunar_finder_matches'); } catch (clearError) { console.error('Failed to clear corrupted data:', clearError); } } } function updateStats() { const storedMatchesCard = document.getElementById('stored-matches-card'); const totalDuelsCard = document.getElementById('total-duels-card'); const oldestMatchCard = document.getElementById('oldest-match-card'); if (storedMatchesCard && totalDuelsCard && oldestMatchCard) { storedMatchesCard.innerHTML = ` <div class="stat-number">${storedMatches.length.toLocaleString()}</div> <div class="stat-label">Unique Games</div> `; const lastFetchTime = localStorage.getItem('lunar_finder_last_fetch'); let timeSinceLastFetch = 'Never'; if (lastFetchTime) { const lastFetch = new Date(parseInt(lastFetchTime)); const now = new Date(); const diffMs = now - lastFetch; const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays > 0) { timeSinceLastFetch = `${diffDays}d ago`; } else if (diffHours > 0) { timeSinceLastFetch = `${diffHours}h ago`; } else if (diffMinutes > 0) { timeSinceLastFetch = `${diffMinutes}m ago`; } else { timeSinceLastFetch = 'Just now'; } } totalDuelsCard.innerHTML = ` <div class="stat-number">${timeSinceLastFetch}</div> <div class="stat-label">Last Fetch</div> `; let oldestDate = 'No matches'; if (storedMatches.length > 0) { const oldestMatch = storedMatches.reduce((oldest, current) => { return new Date(current.time) < new Date(oldest.time) ? current : oldest; }); const oldestDateObj = new Date(oldestMatch.time); const day = oldestDateObj.getDate().toString().padStart(2, '0'); const month = (oldestDateObj.getMonth() + 1).toString().padStart(2, '0'); oldestDate = `${day}/${month}`; } oldestMatchCard.innerHTML = ` <div class="stat-number">${oldestDate}</div> <div class="stat-label">Oldest Match</div> `; } } async function fetchMyMatches() { if (!myUserId) { alert('Please wait for UUID to load'); return; } const button = document.getElementById('fetch-matches-btn'); const progressContainer = document.getElementById('progress-container'); const progressText = document.getElementById('progress-text'); const progressFill = document.getElementById('progress-fill'); button.disabled = true; button.textContent = 'Fetching...'; progressContainer.style.display = 'block'; try { let allMatches = []; let paginationToken = null; let page = 0; let hasMore = true; let consecutiveEmptyPages = 0; const maxPages = 100; while (hasMore && page < maxPages && consecutiveEmptyPages < 3) { progressText.textContent = `Fetching page ${page + 1}...`; progressFill.style.width = `${((page + 1) / maxPages) * 100}%`; let url = `https://www.geoguessr.com/api/v4/feed/private?count=100`; if (paginationToken) url += `&paginationToken=${paginationToken}`; const response = await fetch(url, { credentials: 'include' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); const entries = data.entries || data || []; paginationToken = data.paginationToken || null; if (entries.length === 0) { consecutiveEmptyPages++; if (consecutiveEmptyPages >= 3) { hasMore = false; } } else { consecutiveEmptyPages = 0; const duelMatches = extractDuelMatches(entries); allMatches.push(...duelMatches); console.log(`Page ${page + 1}: Found ${duelMatches.length} duel matches (${entries.length} total activities)`); } if (!paginationToken) hasMore = false; page++; await new Promise(resolve => setTimeout(resolve, 100)); } console.log(`Total activities processed: ${page * 100}, Total duel matches found: ${allMatches.length}`); const uniqueMatches = []; const seenGameIds = new Set(); for (const match of allMatches) { if (!seenGameIds.has(match.gameId)) { seenGameIds.add(match.gameId); uniqueMatches.push(match); } } if (allMatches.length > 0 && uniqueMatches.length < 100) { console.log('Trying alternative approach to find more matches...'); try { const gamesResponse = await fetch('https://www.geoguessr.com/api/v3/games?count=100', { credentials: 'include' }); if (gamesResponse.ok) { const gamesData = await gamesResponse.json(); console.log('Games data:', gamesData); if (gamesData.games) { for (const game of gamesData.games) { if (game.gameMode === 'Duels' || game.gameMode === 'TeamDuels') { const existingMatch = allMatches.find(m => m.gameId === game.id); if (!existingMatch) { allMatches.push({ gameId: game.id, gameMode: game.gameMode, time: game.created, activity: null }); console.log(`Added game from games endpoint: ${game.id}`); } } } } } } catch (error) { console.error('Error fetching games:', error); } } for (const match of allMatches) { if (!seenGameIds.has(match.gameId)) { seenGameIds.add(match.gameId); uniqueMatches.push(match); } } console.log(`Found ${allMatches.length} total matches, ${uniqueMatches.length} unique games`); console.log('Sample of unique game IDs:', uniqueMatches.slice(0, 10).map(m => m.gameId)); addLog(`Found ${allMatches.length} total matches, ${uniqueMatches.length} unique games`, 'info'); if (uniqueMatches.length > 0) { addLog(`Sample game IDs: ${uniqueMatches.slice(0, 5).map(m => m.gameId).join(', ')}`, 'info'); } storedMatches = uniqueMatches; try { const matchesJson = JSON.stringify(storedMatches); localStorage.setItem('lunar_finder_matches', matchesJson); localStorage.setItem('lunar_finder_last_fetch', Date.now().toString()); console.log(`Saved ${storedMatches.length} unique matches to localStorage`); addLog(`Saved ${storedMatches.length} unique matches to localStorage`, 'info'); } catch (storageError) { console.error('Storage error:', storageError); addLog(`Storage error: ${storageError.message}`, 'error'); if (storageError.name === 'QuotaExceededError') { try { const limitedMatches = storedMatches.slice(0, 500); localStorage.setItem('lunar_finder_matches', JSON.stringify(limitedMatches)); localStorage.setItem('lunar_finder_last_fetch', Date.now().toString()); console.log(`Saved limited set of ${limitedMatches.length} unique matches due to storage quota`); addLog(`Saved limited set of ${limitedMatches.length} unique matches due to storage quota`, 'warning'); progressText.textContent = `Warning: Only saved ${limitedMatches.length} unique matches due to storage limit`; } catch (secondError) { console.error('Failed to save even limited matches:', secondError); addLog(`Failed to save even limited matches: ${secondError.message}`, 'error'); progressText.textContent = 'Warning: Could not save matches to storage'; } } } updateStats(); progressText.textContent = `Complete! Found ${allMatches.length} matches`; setTimeout(() => { progressContainer.style.display = 'none'; }, 3000); } catch (error) { console.error('Error fetching matches:', error); addLog(`Error fetching matches: ${error.message}`, 'error'); progressText.textContent = `Error: ${error.message}`; } finally { button.disabled = false; button.textContent = 'Fetch My Matches'; } } function extractDuelMatches(activities) { const matches = []; for (const activity of activities) { if (!activity.payload) continue; try { const payload = JSON.parse(activity.payload); if (Array.isArray(payload)) { for (const event of payload) { if (event.payload && event.payload.gameId) { const gameMode = event.payload.gameMode; if (gameMode === 'Duels' || gameMode === 'TeamDuels') { matches.push({ gameId: event.payload.gameId, gameMode: gameMode, time: event.time || activity.time, activity: activity }); } } } } else if (payload.gameId) { const gameMode = payload.gameMode; if (gameMode === 'Duels' || gameMode === 'TeamDuels') { matches.push({ gameId: payload.gameId, gameMode: gameMode, time: payload.time || activity.time, activity: activity }); } } if (payload.gameId && !payload.gameMode) { matches.push({ gameId: payload.gameId, gameMode: 'Unknown', time: payload.time || activity.time, activity: activity }); } } catch (error) { console.error('Error parsing payload:', error); } } return matches; } async function searchSingleDuels() { const targetUuid = document.getElementById('target-uuid').value.trim(); const maxMatches = parseInt(document.getElementById('max-matches').value); if (!targetUuid) { alert('Please enter a target UUID'); return; } if (!storedMatches.length) { alert('Please fetch your matches first from the Home tab'); return; } const button = document.getElementById('single-search-btn'); const progressContainer = document.getElementById('single-progress-container'); const progressText = document.getElementById('single-progress-text'); const progressFill = document.getElementById('single-progress-fill'); const resultsContainer = document.getElementById('single-results-container'); button.disabled = true; button.textContent = 'Searching...'; progressContainer.style.display = 'block'; resultsContainer.style.display = 'none'; document.getElementById('single-copy-results-btn').style.display = 'none'; document.getElementById('single-download-csv-btn').style.display = 'none'; try { const foundDuels = []; let processed = 0; const onlyMe = document.getElementById('compare-matches-switch')?.checked ?? true; for (const match of storedMatches.slice(0, maxMatches)) { progressText.textContent = `Checking duel ${processed + 1}/${Math.min(maxMatches, storedMatches.length)}...`; progressFill.style.width = `${((processed + 1) / Math.min(maxMatches, storedMatches.length)) * 100}%`; try { const duelData = await fetchDuelDetailsGM(match.gameId); let hasTarget = false; let hasMe = false; if (duelData.teams) { for (const team of duelData.teams) { if (team.players) { for (const player of team.players) { if (player.playerId === targetUuid) hasTarget = true; if (player.playerId === myUserId) hasMe = true; } } } } if (hasTarget && (onlyMe ? hasMe : true)) { foundDuels.push({ gameId: match.gameId, gameMode: match.gameMode, time: match.time, gameLink: `https://www.geoguessr.com/duels/${match.gameId}/summary`, duelData: duelData }); } } catch (error) { console.error(`Error checking duel ${match.gameId}:`, error); } processed++; await new Promise(resolve => setTimeout(resolve, 300)); } currentResults = foundDuels; displayResults(foundDuels, resultsContainer, 'single'); progressText.textContent = `Complete! Found ${foundDuels.length} duels`; document.getElementById('single-copy-results-btn').style.display = 'inline-block'; document.getElementById('single-download-csv-btn').style.display = 'inline-block'; } catch (error) { console.error('Error searching duels:', error); progressText.textContent = `Error: ${error.message}`; } finally { button.disabled = false; button.textContent = 'Search Duels'; } } async function searchMultiDuels() { if (!window.selectedCheatersData) { alert('Please select a JSON file first'); return; } if (!storedMatches.length) { alert('Please fetch your matches first from the Home tab'); return; } const maxMatches = parseInt(document.getElementById('max-matches').value); const button = document.getElementById('single-search-btn'); const progressContainer = document.getElementById('single-progress-container'); const progressText = document.getElementById('single-progress-text'); const progressFill = document.getElementById('single-progress-fill'); const resultsContainer = document.getElementById('single-results-container'); button.disabled = true; button.textContent = 'Searching...'; progressContainer.style.display = 'block'; resultsContainer.style.display = 'none'; try { const foundDuels = []; const cheaterIds = window.selectedCheatersData.map(cheater => cheater.UserID); let processed = 0; const totalToProcess = Math.min(maxMatches * cheaterIds.length, storedMatches.length); for (const match of storedMatches.slice(0, maxMatches * cheaterIds.length)) { progressText.textContent = `Checking duel ${processed + 1}/${totalToProcess}...`; progressFill.style.width = `${((processed + 1) / totalToProcess) * 100}%`; try { const duelData = await fetch(`https://game-server.geoguessr.com/api/duels/${match.gameId}`, { credentials: 'include' }).then(r => r.json()); if (duelData.teams) { for (const team of duelData.teams) { if (team.players) { for (const player of team.players) { if (cheaterIds.includes(player.playerId)) { const cheater = window.selectedCheatersData.find(c => c.UserID === player.playerId); foundDuels.push({ gameId: match.gameId, gameMode: match.gameMode, time: match.time, gameLink: `https://www.geoguessr.com/duels/${match.gameId}/summary`, username: cheater.Username, date: cheater.Date, actionType: cheater.Action_Type }); break; } } } } } } catch (error) { console.error(`Error checking duel ${match.gameId}:`, error); } processed++; await new Promise(resolve => setTimeout(resolve, 50)); } currentResults = foundDuels; displayResults(foundDuels, resultsContainer, 'multi'); progressText.textContent = `Complete! Found ${foundDuels.length} duels`; document.getElementById('single-copy-results-btn').style.display = 'inline-block'; document.getElementById('single-download-csv-btn').style.display = 'inline-block'; } catch (error) { console.error('Error searching duels:', error); progressText.textContent = `Error: ${error.message}`; } finally { button.disabled = false; button.textContent = 'Search All Duels'; } } function displayResults(duels, container, type) { container.innerHTML = ''; if (duels.length === 0) { container.innerHTML = '<div style="color: rgba(255, 255, 255, 0.7); text-align: center; padding: 40px;">No duels found</div>'; container.style.display = 'block'; return; } duels.forEach(duel => { const item = document.createElement('div'); item.className = 'result-item'; const dateObj = new Date(duel.time); const day = dateObj.getDate().toString().padStart(2, '0'); const month = (dateObj.getMonth() + 1).toString().padStart(2, '0'); const date = `${day}/${month}`; if (type === 'multi') { item.innerHTML = ` <div class="result-date">${date} - ${duel.actionType}</div> <div class="result-username">${duel.username}</div> <a href="${duel.gameLink}" target="_blank" class="result-link">${duel.gameLink}</a> `; } else { item.innerHTML = ` <div class="result-date">${date}</div> <div class="result-username">Duel Match</div> <a href="${duel.gameLink}" target="_blank" class="result-link">${duel.gameLink}</a> `; } container.appendChild(item); }); container.style.display = 'block'; } function copyResults() { if (!currentResults.length) return; const csvContent = generateCSV(currentResults); navigator.clipboard.writeText(csvContent).then(() => { alert('Results copied to clipboard!'); }).catch(() => { alert('Failed to copy results'); }); } function downloadCSV() { if (!currentResults.length) return; const csvContent = generateCSV(currentResults); const blob = new Blob([csvContent], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `duel_results_${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function clearStoredData() { if (confirm('Are you sure you want to clear all stored match data? This cannot be undone.')) { try { localStorage.removeItem('lunar_finder_matches'); storedMatches = []; updateStats(); alert('Stored data cleared successfully!'); } catch (error) { console.error('Error clearing stored data:', error); alert('Error clearing stored data'); } } } function generateCSV(results) { const headers = ['Date', 'Username', 'Game Link', 'Action Type']; const rows = results.map(duel => { const dateObj = new Date(duel.time); const day = dateObj.getDate().toString().padStart(2, '0'); const month = (dateObj.getMonth() + 1).toString().padStart(2, '0'); const date = `${day}/${month}`; return [ date, duel.username || 'N/A', duel.gameLink, duel.actionType || 'N/A' ]; }); return [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); } function addLog(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const logEntry = { timestamp, message, type }; logs.unshift(logEntry); if (logs.length > 100) { logs = logs.slice(0, 100); } updateLogsDisplay(); } function updateLogsDisplay() { const logsContainer = document.getElementById('logs-container'); if (!logsContainer) return; logsContainer.innerHTML = ''; if (logs.length === 0) { logsContainer.innerHTML = '<div style="color: rgba(255, 255, 255, 0.7); text-align: center; padding: 40px;">No logs yet</div>'; return; } logs.forEach(log => { const item = document.createElement('div'); item.className = 'result-item'; const typeColor = log.type === 'error' ? '#e74c3c' : log.type === 'warning' ? '#f39c12' : '#27ae60'; item.innerHTML = ` <div class="result-date" style="color: ${typeColor};">${log.timestamp}</div> <div class="result-username">${log.message}</div> `; logsContainer.appendChild(item); }); } function clearLogs() { if (confirm('Are you sure you want to clear all logs?')) { logs = []; updateLogsDisplay(); } } function exportLogs() { if (logs.length === 0) { alert('No logs to export'); return; } const csvContent = logs.map(log => [ log.timestamp, log.type, log.message ]).map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); const headers = ['Timestamp', 'Type', 'Message']; const fullCsv = [headers, ...logs.map(log => [log.timestamp, log.type, log.message])] .map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); const blob = new Blob([fullCsv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `lunar_finder_logs_${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function addEventListeners() { const fetchBtn = document.getElementById('fetch-matches-btn'); const singleSearchBtn = document.getElementById('single-search-btn'); const multiSearchBtn = document.getElementById('multi-search-btn'); const copyResultsBtn = document.getElementById('single-copy-results-btn'); const downloadCsvBtn = document.getElementById('single-download-csv-btn'); const refreshUuidBtn = document.getElementById('refresh-uuid-btn'); const clearStoredBtn = document.getElementById('clear-stored-btn'); const clearLogsBtn = document.getElementById('clear-logs-btn'); const exportLogsBtn = document.getElementById('export-logs-btn'); const singleCopyResultsBtn = document.getElementById('single-copy-results-btn'); const singleDownloadCsvBtn = document.getElementById('single-download-csv-btn'); if (fetchBtn) fetchBtn.addEventListener('click', fetchMyMatches); if (singleSearchBtn) singleSearchBtn.addEventListener('click', searchSingleDuels); if (multiSearchBtn) multiSearchBtn.addEventListener('click', searchMultiDuels); if (copyResultsBtn) copyResultsBtn.addEventListener('click', copyResults); if (downloadCsvBtn) downloadCsvBtn.addEventListener('click', downloadCSV); if (refreshUuidBtn) refreshUuidBtn.addEventListener('click', fetchMyUUID); if (clearStoredBtn) clearStoredBtn.addEventListener('click', clearStoredData); if (clearLogsBtn) clearLogsBtn.addEventListener('click', clearLogs); if (exportLogsBtn) exportLogsBtn.addEventListener('click', exportLogs); if (singleCopyResultsBtn) singleCopyResultsBtn.addEventListener('click', copyResults); if (singleDownloadCsvBtn) singleDownloadCsvBtn.addEventListener('click', downloadCSV); } function addTriggerButton() { const button = document.createElement('button'); button.textContent = 'Lunar Finder'; button.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #ff1493; color: white; border: none; padding: 12px 20px; border-radius: 25px; cursor: pointer; z-index: 9999; font-weight: bold; font-family: 'Geist', sans-serif; box-shadow: 0 2px 10px rgba(0,0,0,0.2); transition: all 0.2s ease; `; button.onmouseover = () => { button.style.transform = 'translateY(-2px)'; button.style.boxShadow = '0 4px 15px rgba(255, 20, 147, 0.3)'; }; button.onmouseout = () => { button.style.transform = 'translateY(0)'; button.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; }; button.onclick = () => { toggleMenu(); }; document.body.appendChild(button); } function handleKeyPress(event) { if (event.key === 'Insert' || event.code === 'Insert') { event.preventDefault(); toggleMenu(); } } function initializeApp() { loadStoredData(); } document.addEventListener('keydown', handleKeyPress); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { addTriggerButton(); initializeApp(); }); } else { addTriggerButton(); initializeApp(); } })(); function showNotification(message, type = 'info') { let container = document.getElementById('lunar-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'lunar-toast-container'; container.style.position = 'fixed'; container.style.top = '24px'; container.style.right = '24px'; container.style.zIndex = '99999'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.gap = '12px'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.style.background = type === 'error' ? 'rgba(255,59,59,0.95)' : 'rgba(30,30,30,0.97)'; toast.style.color = '#fff'; toast.style.border = type === 'error' ? '1.5px solid #ff3b3b' : '1.5px solid #ff1493'; toast.style.borderRadius = '10px'; toast.style.padding = '16px 28px'; toast.style.fontSize = '15px'; toast.style.fontWeight = '500'; toast.style.boxShadow = '0 4px 24px rgba(0,0,0,0.18)'; toast.style.opacity = '0'; toast.style.transform = 'translateY(-10px)'; toast.style.transition = 'opacity 0.2s, transform 0.2s'; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; }, 10); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(-10px)'; setTimeout(() => { toast.remove(); if (container.childElementCount === 0) container.remove(); }, 200); }, 3000); } function fetchDuelDetailsGM(gameId) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest is not available')); return; } GM_xmlhttpRequest({ method: 'GET', url: `https://game-server.geoguessr.com/api/duels/${gameId}`, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); resolve(data); } catch (e) { reject(e); } } else { reject(new Error('Status ' + response.status)); } }, onerror: function(e) { reject(new Error('Request failed')); } }); }); } function setFileLoadedUI(filename) { const fileUpload = document.getElementById('file-upload'); const resetBtn = fileUpload.querySelector('button'); const textDiv = fileUpload.querySelector('.file-upload-text'); textDiv.innerHTML = `<span style='color:#ff1493;font-weight:600;'>${filename}</span> <span style='color:#27ae60;font-size:18px;'>✓</span>`; fileUpload.style.background = 'rgba(39, 174, 96, 0.08)'; resetBtn.style.display = 'inline-block'; } function resetFileUI() { const fileUpload = document.getElementById('file-upload'); const resetBtn = fileUpload.querySelector('button'); const playerListContainer = document.getElementById('multi-player-list'); const exportAllButton = document.querySelector('#multi-tab button[style*="margin: 16px 0 16px 12px"]'); const scanButton = document.querySelector('#multi-tab button[style*="margin: 16px 0 16px 0"]'); const multiProgressContainer = document.getElementById('multi-progress-container'); const textDiv = fileUpload.querySelector('.file-upload-text'); textDiv.textContent = 'Drop JSON/CSV file here or click to browse'; fileUpload.style.background = ''; resetBtn.style.display = 'none'; playerListContainer.innerHTML = ''; bannedPlayers = []; lastScanResults = []; if (exportAllButton) exportAllButton.style.display = 'none'; if (scanButton) { scanButton.disabled = false; scanButton.textContent = 'Scan All'; } if (multiProgressContainer) multiProgressContainer.style.display = 'none'; }