// ==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';
}