// ==UserScript== // @name StellaGeo // @namespace http://tampermonkey.net/ // @version 3.3.0 // @description StellaGeo Geoguessr Cheat // @author Cope (@713cope on Discord) // @match https://www.geoguessr.com/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_addElement // @grant GM.getValue // @grant GM.setValue // @grant GM.addStyle // @grant GM.addElement // @connect flagcdn.com // @connect static-maps.yandex.ru // @connect fonts.googleapis.com // @connect i.imgur.com // @connect minimalistmoon.com // @connect us1.locationiq.com // @connect locationiq.com // @connect discord.com // @run-at document-start // @downloadURL none // ==/UserScript== (function () { 'use strict'; const nativeOpen = window.open; const CONFIG = { VERSION: '4.0.0', API_KEYS: [ 'pk.010bb988be9b2a316e7093ae8e316e6d', 'pk.6ce0e2bf3b2b84e353d2420b38de8ed2', 'pk.78f4624afaebfd926a65e31358a4507d' ] }; class Utils { static logs = []; static logListeners = []; static generateId() { return 'user_' + Math.random().toString(36).substring(2, 15); } static log(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const logEntry = { timestamp, message, type }; this.logs.unshift(logEntry); if (this.logs.length > 100) this.logs.pop(); this.logListeners.forEach(listener => listener(logEntry)); } static addLogListener(listener) { this.logListeners.push(listener); } static async sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } class Settings { constructor() { this.defaults = { menuHotkey: 'Insert', apiKeyIndex: 0, leaderboardId: Utils.generateId(), leaderboardName: 'Player', participateInLeaderboard: true, isToastEnabled: true, firstRun: true, firstWebhookRun: true, sidebarWidth: 240, apiKeyStatus: {}, elementPositions: {}, features: { openGM: false, openPlonkIT: false, locationDisplay: false, tts: false, discordWebhook: false, watermark: true, mapTimer: true, hotkeyDisplay: false, toastNotifications: true, autoPin: false, }, featureSettings: { locationDisplay: { showCountry: true, showState: true, showCity: true, stateZoom: 5, cityZoom: 10 }, tts: { volume: 1.0 }, discordWebhook: { url: '' }, watermark: { position: 'top-center', showName: true, showUsername: true, showClock: true, timeFormat: '12h' }, hotkeyDisplay: { mode: 'active', showToggle: true, showTrigger: true }, toastNotifications: { position: 'bottom-right' } }, hotkeys: { openGM: { trigger: 'q' }, openPlonkIT: { trigger: 'q' }, locationDisplay: { trigger: 'q' }, discordWebhook: { trigger: 'q' }, tts: { trigger: 'e' }, hotkeyDisplay: { toggle: 'J', trigger: null } }, currentTheme: 'default', customThemes: {}, defaultThemes: { default: { name: 'Stella Purple', colors: { carbonBlack: { color: '#1a1a1a', alpha: 1, effect: 'none' }, carbonBlack2: { color: '#1d1d1d', alpha: 1, effect: 'none' }, carbonBlack3: { color: '#242424', alpha: 1, effect: 'none' }, accent: { color: '#8b5cf6', alpha: 1, effect: 'none' }, accentHover: { color: '#7c3aed', alpha: 1, effect: 'none' }, text: { color: '#ffffff', alpha: 1, effect: 'none' }, textDim: { color: '#a1a1aa', alpha: 1, effect: 'none' }, border: { color: '#333333', alpha: 1, effect: 'none' } } }, blue: { name: 'Ocean Blue', colors: { carbonBlack: { color: '#0f172a', alpha: 1, effect: 'none' }, carbonBlack2: { color: '#1e293b', alpha: 1, effect: 'none' }, carbonBlack3: { color: '#334155', alpha: 1, effect: 'none' }, accent: { color: '#3b82f6', alpha: 1, effect: 'none' }, accentHover: { color: '#2563eb', alpha: 1, effect: 'none' }, text: { color: '#ffffff', alpha: 1, effect: 'none' }, textDim: { color: '#94a3b8', alpha: 1, effect: 'none' }, border: { color: '#475569', alpha: 1, effect: 'none' } } }, green: { name: 'Forest Green', colors: { carbonBlack: { color: '#14532d', alpha: 1, effect: 'none' }, carbonBlack2: { color: '#166534', alpha: 1, effect: 'none' }, carbonBlack3: { color: '#15803d', alpha: 1, effect: 'none' }, accent: { color: '#22c55e', alpha: 1, effect: 'none' }, accentHover: { color: '#16a34a', alpha: 1, effect: 'none' }, text: { color: '#ffffff', alpha: 1, effect: 'none' }, textDim: { color: '#86efac', alpha: 1, effect: 'none' }, border: { color: '#4ade80', alpha: 1, effect: 'none' } } } } }; this.data = this.load(); } load() { const saved = GM_getValue('stella_settings', {}); const merged = { ...this.defaults, ...saved }; merged.features = { ...this.defaults.features, ...(saved.features || {}) }; merged.hotkeys = { ...this.defaults.hotkeys, ...(saved.hotkeys || {}) }; merged.featureSettings = { ...this.defaults.featureSettings }; if (saved.featureSettings) { for (const key in saved.featureSettings) { merged.featureSettings[key] = { ...this.defaults.featureSettings[key], ...saved.featureSettings[key] }; } } return merged; } save() { GM_setValue('stella_settings', this.data); } get(key) { return this.data[key]; } set(key, value) { this.data[key] = value; this.save(); } getFeatureSetting(feature, setting) { return this.data.featureSettings[feature]?.[setting]; } setFeatureSetting(feature, setting, value) { if (!this.data.featureSettings[feature]) { this.data.featureSettings[feature] = {}; } this.data.featureSettings[feature][setting] = value; this.save(); } } class UI { constructor(app) { this.app = app; this.menuOpen = false; this.locationDisplayWindow = null; this.locationDisplayData = null; this.locationDisplayPopped = false; this.loadIcons(); this.injectStyles(); this.createOverlay(); this.createLocationDisplay(); this.createHUD(); this.checkFirstRun(); setTimeout(() => { this.applyTheme(this.app.settings.get('currentTheme')); }, 100); } loadIcons() { const link = document.createElement('link'); link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons'; link.rel = 'stylesheet'; document.head.appendChild(link); } injectStyles() { const css = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); :root { --carbon-black: #1a1a1aff; --carbon-black-2: #1d1d1dff; --carbon-black-3: #242424ff; /* Lighter Amethyst / Violet */ --stella-accent: #8b5cf6; --stella-accent-hover: #7c3aed; --stella-bg: var(--carbon-black); --stella-sidebar: var(--carbon-black-2); --stella-item-bg: var(--carbon-black-3); --stella-text: #ffffff; --stella-text-dim: #a1a1aa; --stella-border: #333333; } body { user-select: none; } #stella-menu { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); width: 850px; height: 600px; background: var(--stella-bg); border-radius: 12px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); display: none; opacity: 0; z-index: 99999; overflow: hidden; font-family: 'Inter', sans-serif; color: var(--stella-text); border: 1px solid var(--stella-border); transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); flex-direction: column; } #stella-menu.open { display: flex; animation: menuFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; } #stella-menu.closing { animation: menuFadeOut 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards; } @keyframes menuFadeIn { from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes menuFadeOut { from { opacity: 1; transform: translate(-50%, -50%) scale(1); } to { opacity: 0; transform: translate(-50%, -50%) scale(0.95); } } /* Header */ .stella-header { height: 70px; border-bottom: 1px solid var(--stella-border); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: var(--stella-bg); flex-shrink: 0; } .stella-logo { display: flex; align-items: center; gap: 12px; } .stella-logo img { width: 32px; height: 32px; border-radius: 8px; /* Rounded Corners */ } .stella-logo-text { font-size: 24px; font-weight: 800; color: white; letter-spacing: -0.5px; animation: stellaPulse 3s infinite alternate; } @keyframes stellaPulse { 0% { text-shadow: 0 0 10px rgba(139, 92, 246, 0.2); } 100% { text-shadow: 0 0 20px rgba(139, 92, 246, 0.6); } } .stella-header-actions { display: flex; gap: 8px; } .stella-header-btn { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 8px; cursor: pointer; color: var(--stella-text-dim); transition: all 0.2s ease; } .stella-header-btn:hover { background: rgba(255,255,255,0.05); color: white; } .stella-header-btn.active { background: rgba(139, 92, 246, 0.1); color: var(--stella-accent); } /* Body Layout */ .stella-body { display: flex; flex: 1; overflow: hidden; } .stella-sidebar { background: var(--stella-sidebar); padding: 20px 10px; display: flex; flex-direction: column; border-right: 1px solid var(--stella-border); position: relative; flex-shrink: 0; width: 50px; transition: width 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } .stella-sidebar:hover { width: 240px; } .stella-sidebar:hover.collapsed { width: 240px; } .stella-nav { position: relative; display: flex; flex-direction: column; } .stella-nav-highlight { position: absolute; left: 0; width: 100%; background: var(--stella-item-bg); border-radius: 8px; transition: top 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); z-index: 1; border: 1px solid var(--stella-border); } .stella-nav-highlight::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--stella-accent); border-radius: 8px 0 0 8px; } .stella-nav-item { display: flex; align-items: center; gap: 12px; padding: 12px 12px 12px 13px; border-radius: 8px; cursor: pointer; transition: color 0.3s ease; color: var(--stella-text-dim); font-weight: 500; font-size: 14px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; position: relative; z-index: 2; } .stella-nav-item:hover { color: var(--stella-text); } .stella-nav-item.active { color: white; } .stella-sidebar.collapsed .stella-nav-text { opacity: 0; width: 0; overflow: hidden; transition: opacity 0.3s ease, width 0.3s ease; } .stella-sidebar:hover .stella-nav-text { opacity: 1; width: auto; } .stella-sidebar.collapsed .stella-nav-item { gap: 0; } .stella-sidebar:hover .stella-nav-item { gap: 12px; } .stella-content { flex: 1; padding: 32px; overflow-y: auto; background: var(--stella-bg); } /* Feature Box Shadcn Style */ .stella-feature { display: flex; align-items: center; justify-content: space-between; padding: 16px; background: transparent; border-radius: 8px; margin-bottom: 12px; transition: all 0.2s ease; cursor: pointer; border: 1px solid var(--stella-border); position: relative; } .stella-feature:hover { background: var(--stella-item-bg); border-color: rgba(255,255,255,0.1); } .stella-feature.active { border-color: var(--stella-accent); background: rgba(139, 92, 246, 0.05); } .stella-feature-info { display: flex; align-items: center; gap: 12px; } .stella-feature-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--stella-accent); opacity: 0; transition: opacity 0.2s; box-shadow: 0 0 8px var(--stella-accent); } .stella-feature.active .stella-feature-dot { opacity: 1; } .stella-feature-name { font-weight: 500; font-size: 14px; } .stella-settings-btn { opacity: 0; transition: opacity 0.2s; cursor: pointer; color: var(--stella-text-dim); font-size: 18px; padding: 6px; border-radius: 4px; } .stella-settings-btn:hover { background: rgba(255,255,255,0.1); color: white; } .stella-feature:hover .stella-settings-btn { opacity: 1; } /* Toast */ #stella-toast-container { position: fixed; z-index: 100000; display: flex; flex-direction: column; gap: 10px; } #stella-toast-container.toast-pos-bottom-right { bottom: 30px; right: 30px; } #stella-toast-container.toast-pos-bottom-left { bottom: 30px; left: 30px; } #stella-toast-container.toast-pos-top-right { top: 30px; right: 30px; } #stella-toast-container.toast-pos-top-left { top: 30px; left: 30px; } .stella-toast { background: var(--carbon-black-2); border: 1px solid var(--stella-border); padding: 12px 20px; border-radius: 8px; color: white; box-shadow: 0 10px 30px rgba(0,0,0,0.3); animation: slideIn 0.3s ease; display: flex; align-items: center; gap: 12px; min-width: 200px; font-size: 13px; font-weight: 500; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } /* Location Display */ #stella-location-display { position: fixed; top: 20px; left: 20px; background: rgba(26, 26, 26, 0.85); backdrop-filter: blur(4px); padding: 12px; border-radius: 6px; color: white; z-index: 9999; border: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 2px solid var(--stella-accent); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); min-width: 260px; cursor: move; display: none; font-family: 'Inter', sans-serif; } #stella-location-display:hover { box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(139, 92, 246, 0.2); } .stella-loc-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; font-size: 14px; color: #ffffff; padding: 8px 10px; background: rgba(139, 92, 246, 0.05); border-radius: 6px; border: 1px solid rgba(139, 92, 246, 0.1); transition: all 0.2s ease; } .stella-loc-row:last-child { margin-bottom: 0; } .stella-loc-row:hover { background: rgba(139, 92, 246, 0.1); border-color: rgba(139, 92, 246, 0.2); } .stella-loc-icon { color: var(--stella-accent); font-size: 18px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; background: rgba(139, 92, 246, 0.15); border-radius: 6px; } .stella-flag { width: 24px; height: 18px; border-radius: 4px; object-fit: cover; border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } #stella-map-image { width: 100%; height: 140px; background-size: cover; background-position: center; border-radius: 8px; margin-top: 12px; border: 1px solid var(--stella-border); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); opacity: 0.95; transition: all 0.3s ease; } #stella-map-image:hover { opacity: 1; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); } /* HUD Elements */ #stella-watermark { position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background: rgba(26, 26, 26, 0.6); backdrop-filter: blur(4px); padding: 6px 12px; border-radius: 6px; color: rgba(255, 255, 255, 0.8); font-family: 'Inter', sans-serif; font-size: 12px; font-weight: 600; z-index: 9998; border: 1px solid rgba(255, 255, 255, 0.1); pointer-events: none; display: none; transition: all 0.3s ease; border-bottom: 2px solid var(--stella-accent); } #stella-watermark.top-left { top: 10px; left: 10px; transform: none; } #stella-watermark.top-center { top: 50px; left: 50%; transform: translateX(-50%); } #stella-watermark.top-right { top: 10px; right: 10px; left: auto; transform: none; } #stella-watermark.bottom-left { bottom: 10px; left: 10px; top: auto; transform: none; } #stella-watermark.bottom-center { bottom: 10px; left: 50%; top: auto; transform: translateX(-50%); } #stella-watermark.bottom-right { bottom: 10px; right: 10px; top: auto; left: auto; transform: none; } #stella-map-timer { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(26, 26, 26, 0.8); padding: 8px 16px; border-radius: 20px; color: white; font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700; z-index: 9998; border: 1px solid var(--stella-accent); display: none; box-shadow: 0 4px 12px rgba(0,0,0,0.3); } #stella-hotkey-display { position: fixed; top: 100px; left: 20px; background: rgba(26, 26, 26, 0.85); backdrop-filter: blur(4px); padding: 10px 14px; border-radius: 6px; color: white; z-index: 9998; border: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 2px solid var(--stella-accent); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); min-width: 180px; cursor: move; display: none; font-family: 'Inter', sans-serif; } .stella-hk-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: 13px; color: var(--stella-text-dim); } .stella-hk-row:last-child { margin-bottom: 0; } .stella-hk-active { color: white; font-weight: 500; } .stella-hk-keys { display: flex; gap: 4px; } .stella-hk-key-badge { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px; font-size: 11px; font-family: monospace; color: var(--stella-accent); border: 1px solid rgba(139, 92, 246, 0.2); } /* Popups */ .stella-popup { background: var(--stella-bg) !important; border: 1px solid var(--stella-border) !important; box-shadow: 0 10px 40px rgba(0,0,0,0.5) !important; color: white !important; padding: 20px; border-radius: 12px; z-index: 100000; min-width: 260px; position: fixed; } .stella-input-wrapper { position: relative; width: 100%; } .stella-input { width: 100%; background: var(--carbon-black-2); border: 1px solid var(--stella-border); padding: 8px 12px; border-radius: 6px; color: white; font-family: 'Inter', sans-serif; font-size: 13px; outline: none; transition: border-color 0.2s; } .stella-input:focus { border-color: var(--stella-accent); } .stella-input-clear { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: var(--stella-text-dim); cursor: pointer; font-size: 14px; display: none; } .stella-input:not(:placeholder-shown) + .stella-input-clear { display: block; } .stella-btn { background: var(--stella-accent); color: white; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; transition: background 0.2s; } .stella-btn:hover { background: var(--stella-accent-hover); } .stella-btn-secondary { background: transparent; border: 1px solid var(--stella-border); color: var(--stella-text-dim); } .stella-btn-secondary:hover { background: rgba(255,255,255,0.05); color: white; } /* Welcome Modal */ #stella-welcome { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--stella-bg); border: 1px solid var(--stella-border); padding: 30px; border-radius: 12px; z-index: 100001; text-align: center; box-shadow: 0 20px 60px rgba(0,0,0,0.8); max-width: 400px; } .stella-welcome-title { font-size: 24px; font-weight: 800; margin-bottom: 15px; color: white; } .stella-welcome-text { color: var(--stella-text-dim); font-size: 14px; line-height: 1.6; margin-bottom: 25px; } .stella-key { background: var(--carbon-black-3); border: 1px solid var(--stella-border); padding: 2px 6px; border-radius: 4px; color: white; font-family: monospace; font-size: 12px; } /* Logs */ .stella-log-entry { font-family: monospace; font-size: 12px; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.05); color: var(--stella-text-dim); } .stella-log-entry span { color: var(--stella-accent); margin-right: 8px; } .stella-log-entry.error { color: #ff5555; } .stella-log-entry.error span { color: #ff5555; } /* Checkbox & Slider */ .stella-checkbox-wrapper { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; cursor: pointer; } /* Custom Color Picker */ .stella-color-picker-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); z-index: 99999; display: flex; align-items: center; justify-content: center; } .stella-color-picker { background: var(--carbon-black); border: 1px solid var(--stella-border); border-radius: 12px; padding: 20px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); width: 320px; } .stella-color-picker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .stella-color-picker-title { color: white; font-size: 16px; font-weight: 600; } .stella-color-picker-close { background: none; border: none; color: var(--stella-text-dim); cursor: pointer; padding: 4px; border-radius: 4px; transition: all 0.2s; } .stella-color-picker-close:hover { background: var(--carbon-black-3); color: white; } .stella-color-canvas { width: 100%; height: 200px; border-radius: 8px; cursor: crosshair; margin-bottom: 12px; border: 1px solid var(--stella-border); } .stella-hue-slider { width: 100%; height: 16px; border-radius: 8px; background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); margin-bottom: 16px; position: relative; cursor: pointer; border: 1px solid var(--stella-border); } .stella-hue-slider-thumb { position: absolute; top: -2px; width: 20px; height: 20px; background: white; border: 2px solid var(--carbon-black); border-radius: 50%; cursor: pointer; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .stella-color-preview { display: flex; gap: 12px; margin-bottom: 16px; } .stella-color-preview-box { flex: 1; height: 50px; border-radius: 8px; border: 1px solid var(--stella-border); } .stella-color-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .stella-color-input-group { display: flex; flex-direction: column; gap: 4px; } .stella-color-input-label { font-size: 10px; color: var(--stella-text-dim); text-transform: uppercase; font-weight: 600; } .stella-color-input-field { background: var(--carbon-black-3); border: 1px solid var(--stella-border); border-radius: 6px; padding: 8px; color: white; font-size: 12px; font-family: monospace; } .stella-color-input-field:focus { outline: none; border-color: var(--stella-accent); } /* Custom Sliders */ input[type="range"] { -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; outline: none; } input[type="range"]::-webkit-slider-track { background: var(--stella-accent); height: 4px; border-radius: 2px; border: none; } input[type="range"]::-moz-range-track { background: var(--stella-accent); height: 4px; border-radius: 2px; border: none; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--carbon-black); border: 2px solid var(--stella-accent); cursor: pointer; margin-top: -6px; outline: none; } input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--carbon-black); border: 2px solid var(--stella-accent); cursor: pointer; outline: none; } .stella-checkbox { width: 16px; height: 16px; border: 1px solid var(--stella-border); border-radius: 4px; background: var(--carbon-black-2); display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .stella-checkbox.checked { background: var(--stella-accent); border-color: var(--stella-accent); } .stella-checkbox.checked::after { content: '✓'; font-size: 12px; color: white; } .stella-slider-wrapper { margin-bottom: 15px; } .stella-slider-label { display: flex; justify-content: space-between; font-size: 12px; color: var(--stella-text-dim); margin-bottom: 5px; } .stella-slider { width: 100%; -webkit-appearance: none; height: 4px; background: var(--carbon-black-3); border-radius: 2px; outline: none; } .stella-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--stella-accent); cursor: pointer; transition: background .15s ease-in-out; } `; GM_addStyle(css); } checkFirstRun() { if (this.app.settings.get('firstRun')) { const modal = document.createElement('div'); modal.id = 'stella-welcome'; modal.innerHTML = `
Welcome to Stella V4

Here's how to use the menu:

`; document.body.appendChild(modal); document.getElementById('stella-welcome-close').addEventListener('click', () => { modal.remove(); this.showLocationApiPopup(); }); } } showLocationApiPopup() { const modal = document.createElement('div'); modal.id = 'stella-welcome'; modal.innerHTML = `
Location API Setup

The first API call will trigger a Tampermonkey permission popup if you are not on the latest Tamper Monkey Version.

Click "Always allow" to enable location lookups.

info
This permission allows the script to use the LocationIQ API for reverse geocoding.
`; document.body.appendChild(modal); document.getElementById('stella-location-api-okay').addEventListener('click', () => { modal.remove(); this.app.settings.set('firstRun', false); this.app.network.getLocationDetails(40.7128, -74.0060); this.toggleMenu(); }); } showWebhookOnboarding(callback) { const modal = document.createElement('div'); modal.id = 'stella-welcome'; modal.innerHTML = `
Discord Webhook Setup

When you press "Okay", a Tampermonkey permission popup will appear (if you are not on the latest Tampermonkey version).

Click "Always allow" to enable Discord webhook functionality.

info
This permission allows the script to send location data to your Discord webhook URL.
`; document.body.appendChild(modal); document.getElementById('stella-webhook-okay').addEventListener('click', () => { modal.remove(); callback(); }); } createOverlay() { this.menu = document.createElement('div'); this.menu.id = 'stella-menu'; this.menu.innerHTML = `
edit
list_alt
settings
`; document.body.appendChild(this.menu); this.setupNavigation(); this.renderTab('location'); } createLocationDisplay() { this.locationDisplay = document.createElement('div'); this.locationDisplay.id = 'stella-location-display'; this.locationDisplay.innerHTML = `
Location
public Country: N/A
map State: N/A
location_city City: N/A
`; document.body.appendChild(this.locationDisplay); this.makeDraggable(this.locationDisplay, 'locationDisplay'); const popButton = document.getElementById('stella-loc-popout'); popButton.addEventListener('click', () => { if (!this.locationDisplayPopped) { this.popOutLocationDisplay(); popButton.innerHTML = 'call_received'; popButton.title = 'Pop back in'; } else { this.popInLocationDisplay(); popButton.innerHTML = 'open_in_new'; popButton.title = 'Pop out'; } }); if (this.app.settings.get('features').locationDisplay) { this.locationDisplay.style.display = 'block'; } } createHUD() { this.watermark = document.createElement('div'); this.watermark.id = 'stella-watermark'; this.watermark.innerHTML = `
Stella
person
schedule
`; document.body.appendChild(this.watermark); this.updateWatermarkPosition(); this.updateWatermarkContent(); this.startClock(); if (this.app.settings.get('features').watermark) { this.watermark.style.display = 'block'; } this.mapTimer = document.createElement('div'); this.mapTimer.id = 'stella-map-timer'; this.mapTimer.textContent = '00:00'; document.body.appendChild(this.mapTimer); if (this.app.settings.get('features').mapTimer) { this.mapTimer.style.display = 'block'; } this.createHotkeyDisplay(); } createHotkeyDisplay() { this.hotkeyDisplay = document.createElement('div'); this.hotkeyDisplay.id = 'stella-hotkey-display'; document.body.appendChild(this.hotkeyDisplay); this.makeDraggable(this.hotkeyDisplay); this.updateHotkeyDisplay(); if (this.app.settings.get('features').hotkeyDisplay) { this.hotkeyDisplay.style.display = 'block'; } } updateHotkeyDisplay() { const settings = this.app.settings.get('featureSettings').hotkeyDisplay; const features = this.app.settings.get('features'); const hotkeys = this.app.settings.get('hotkeys'); let html = ''; let hasContent = false; const featureNames = { openGM: 'Google Maps', openPlonkIT: 'PlonkIT', locationDisplay: 'Location Info', tts: 'Text to Speech', discordWebhook: 'Webhook', watermark: 'Watermark', mapTimer: 'Map Timer', hotkeyDisplay: 'Hotkeys' }; html += `
Hotkeys
`; for (const [key, name] of Object.entries(featureNames)) { const isActive = features[key]; const hk = hotkeys[key]; const hasHotkey = hk && (hk.trigger || hk.toggle); if (settings.mode === 'active' && !isActive) continue; if (settings.mode === 'bound' && !hasHotkey) continue; hasContent = true; html += `
${name}
`; if (hk) { if (settings.showToggle && hk.toggle) { html += `${hk.toggle}`; } if (settings.showTrigger && hk.trigger) { html += `${hk.trigger}`; } } html += `
`; } if (!hasContent) html = '
No active features
'; this.hotkeyDisplay.innerHTML = html; } updateLocationDisplay(data) { if (!data) return; const settings = this.app.settings.get('featureSettings').locationDisplay; this.locationDisplayData = data; document.getElementById('stella-loc-country').textContent = data.country || 'N/A'; document.getElementById('stella-loc-state').textContent = data.state || 'N/A'; document.getElementById('stella-loc-city').textContent = data.city || 'N/A'; document.getElementById('stella-row-country').style.display = settings.showCountry ? 'flex' : 'none'; document.getElementById('stella-row-state').style.display = settings.showState ? 'flex' : 'none'; document.getElementById('stella-row-city').style.display = settings.showCity ? 'flex' : 'none'; const flagImg = document.getElementById('stella-loc-flag'); if (data.countryCode && settings.showCountry) { flagImg.src = `https://flagcdn.com/24x18/${data.countryCode}.png`; flagImg.style.display = 'block'; } else { flagImg.style.display = 'none'; } let zoom = 5; if (settings.showCity && data.city) zoom = settings.cityZoom; else if (settings.showState && data.state) zoom = settings.stateZoom; const mapUrl = `https://static-maps.yandex.ru/1.x/?ll=${this.app.network.globalCoordinates.lng},${this.app.network.globalCoordinates.lat}&z=${zoom}&size=300,150&l=map&lang=en`; document.getElementById('stella-map-image').style.backgroundImage = `url("${mapUrl}")`; this.syncLocationPopout(data, mapUrl, settings); if (this.app.settings.get('features').autoPin) { const coords = this.app.network.globalCoordinates; if (coords?.lat && coords?.lng) { autoPinLocation(coords.lat, coords.lng); } } } syncLocationPopout(data, mapUrl, settings) { if (!this.locationDisplayWindow || this.locationDisplayWindow.closed || !this.locationDisplayPopped) return; try { const doc = this.locationDisplayWindow.document; doc.getElementById('pop-country').textContent = data.country || 'N/A'; doc.getElementById('pop-state').textContent = data.state || 'N/A'; doc.getElementById('pop-city').textContent = data.city || 'N/A'; doc.getElementById('pop-country-row').style.display = settings.showCountry ? 'flex' : 'none'; doc.getElementById('pop-state-row').style.display = settings.showState ? 'flex' : 'none'; doc.getElementById('pop-city-row').style.display = settings.showCity ? 'flex' : 'none'; const flagImg = doc.getElementById('pop-flag'); if (data.countryCode && settings.showCountry) { flagImg.src = `https://flagcdn.com/24x18/${data.countryCode}.png`; flagImg.style.display = 'block'; } else { flagImg.style.display = 'none'; } doc.getElementById('pop-map').style.backgroundImage = `url("${mapUrl}")`; } catch (err) { this.locationDisplayWindow = null; this.locationDisplayPopped = false; const btn = document.getElementById('stella-loc-popout'); if (btn) { btn.textContent = '↗'; btn.title = 'Pop out'; } } } popOutLocationDisplay() { if (this.locationDisplayPopped) return; this.locationDisplayWindow = window.open('', 'stellaLocationPopout', 'width=360,height=260'); if (!this.locationDisplayWindow) return; this.locationDisplayPopped = true; this.locationDisplay.style.display = 'none'; const css = ` body { margin:0; background:rgba(12,12,18,0.96); color:#fff; font-family: Inter, system-ui, sans-serif; } .card { padding:12px; background:rgba(24,24,32,0.85); border:1px solid rgba(255,255,255,0.08); border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,0.45); } .header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; font-weight:600; } .row { display:flex; align-items:center; gap:8px; margin-bottom:6px; } .flag { width:24px; height:18px; border-radius:3px; object-fit:cover; } .map { width:300px; height:150px; background-size:cover; background-position:center; border-radius:8px; border:1px solid rgba(255,255,255,0.1); } .label { color:#c9c9d1; font-size:12px; } `; const doc = this.locationDisplayWindow.document; doc.open(); doc.write(`Location
Location
publicCountry: N/A
mapState: N/A
location_cityCity: N/A
`); doc.close(); const popInBtn = doc.getElementById('pop-in'); popInBtn.addEventListener('click', () => this.popInLocationDisplay()); this.locationDisplayWindow.addEventListener('beforeunload', () => { this.locationDisplayWindow = null; if (this.locationDisplayPopped) { this.popInLocationDisplay(true); } }); const btn = document.getElementById('stella-loc-popout'); if (btn) { btn.textContent = '↩'; btn.title = 'Pop back in'; } if (this.locationDisplayData) { const settings = this.app.settings.get('featureSettings').locationDisplay; const mapUrl = `https://static-maps.yandex.ru/1.x/?ll=${this.app.network.globalCoordinates.lng},${this.app.network.globalCoordinates.lat}&z=${settings.showCity && this.locationDisplayData.city ? settings.cityZoom : settings.showState && this.locationDisplayData.state ? settings.stateZoom : 5}&size=300,150&l=map&lang=en`; this.syncLocationPopout(this.locationDisplayData, mapUrl, settings); } } popInLocationDisplay(fromWindowClose = false) { if (!this.locationDisplayPopped) return; if (this.locationDisplayWindow && !this.locationDisplayWindow.closed) { this.locationDisplayWindow.close(); } this.locationDisplayWindow = null; this.locationDisplayPopped = false; this.locationDisplay.style.display = this.app.settings.get('features').locationDisplay ? 'block' : 'none'; if (!fromWindowClose) { const btn = document.getElementById('stella-loc-popout'); if (btn) { btn.innerHTML = 'open_in_new'; btn.title = 'Pop out'; } } } makeDraggable(element, saveKey = null) { let isDragging = false; let currentX = 0, currentY = 0, initialX = 0, initialY = 0; const app = this.app; if (saveKey) { const positions = app.settings.get('elementPositions'); if (positions && positions[saveKey]) { element.style.left = positions[saveKey].left; element.style.top = positions[saveKey].top; } } element.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(e.target.tagName)) return; e.preventDefault(); isDragging = true; initialX = e.clientX; initialY = e.clientY; const rect = element.getBoundingClientRect(); currentX = rect.left; currentY = rect.top; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { if (!isDragging) return; e = e || window.event; e.preventDefault(); const deltaX = e.clientX - initialX; const deltaY = e.clientY - initialY; element.style.left = (currentX + deltaX) + "px"; element.style.top = (currentY + deltaY) + "px"; } function closeDragElement() { isDragging = false; document.onmouseup = null; document.onmousemove = null; if (saveKey) { const positions = app.settings.get('elementPositions') || {}; positions[saveKey] = { left: element.style.left, top: element.style.top }; app.settings.set('elementPositions', positions); } } } setupNavigation() { const navItems = this.menu.querySelectorAll('.stella-nav-item'); const headerBtns = this.menu.querySelectorAll('.stella-header-btn'); const highlight = this.menu.querySelector('.stella-nav-highlight'); const updateHighlight = (item) => { if (highlight && item.classList.contains('stella-nav-item')) { highlight.style.top = `${item.offsetTop}px`; highlight.style.height = `${item.offsetHeight}px`; highlight.style.opacity = '1'; } else if (highlight) { highlight.style.opacity = '0'; } }; const activeNav = this.menu.querySelector('.stella-nav-item.active'); if (activeNav) { setTimeout(() => updateHighlight(activeNav), 0); } const allTabs = [...navItems, ...headerBtns]; allTabs.forEach(item => { item.addEventListener('click', () => { allTabs.forEach(nav => nav.classList.remove('active')); item.classList.add('active'); if (item.classList.contains('stella-nav-item')) { updateHighlight(item); } else { if (highlight) highlight.style.opacity = '0'; } this.renderTab(item.dataset.tab); }); }); } renderTab(tabName) { const content = this.menu.querySelector('#stella-tab-content'); content.innerHTML = ''; switch (tabName) { case 'location': this.renderLocationTab(content); break; case 'hud': this.renderHUDTab(content); break; case 'autoplay': content.innerHTML = `
warning

Maintenance Mode

This feature is currently being updated.

`; break; case 'settings': this.renderSettingsTab(content); break; case 'logs': this.renderLogsTab(content); break; case 'customize': this.renderCustomizeTab(content); break; } } renderLocationTab(container) { this.createFeature(container, 'Open Google Maps ⚠️', 'openGM'); this.createFeature(container, 'Open PlonkIT', 'openPlonkIT'); this.createFeature(container, 'Location Display', 'locationDisplay', true); this.createFeature(container, 'Text to Speech', 'tts', true); this.createFeature(container, 'Discord Webhook', 'discordWebhook', true); } renderHUDTab(container) { this.createFeature(container, 'Watermark', 'watermark', true); this.createFeature(container, 'Classic Map Timer', 'mapTimer'); this.createFeature(container, 'Hotkey Display', 'hotkeyDisplay', true); this.createFeature(container, 'Toast Notifications', 'toastNotifications', true); } renderCustomizeTab(container) { const defaultSection = document.createElement('div'); defaultSection.style.marginBottom = '24px'; defaultSection.innerHTML = '

Default Themes

'; container.appendChild(defaultSection); const defaultThemes = this.app.settings.get('defaultThemes'); for (const [key, theme] of Object.entries(defaultThemes)) { this.createThemeItem(defaultSection, theme.name, key, false); } const customSection = document.createElement('div'); customSection.style.marginBottom = '24px'; customSection.innerHTML = `

Custom Themes

`; container.appendChild(customSection); const customThemes = this.app.settings.get('customThemes'); const hasCustomThemes = Object.keys(customThemes).length > 0; if (!hasCustomThemes) { const emptyState = document.createElement('div'); emptyState.style.cssText = 'padding:20px; text-align:center; color:var(--stella-text-dim); font-size:13px; border:1px dashed var(--stella-border); border-radius:8px;'; emptyState.textContent = 'No custom themes yet. Create one to get started!'; customSection.appendChild(emptyState); } else { for (const [key, theme] of Object.entries(customThemes)) { this.createThemeItem(customSection, theme.name, key, true); } } document.getElementById('create-theme-btn').addEventListener('click', () => { this.createNewTheme(); }); } createThemeItem(container, name, key, isCustom) { const el = document.createElement('div'); const isActive = this.app.settings.get('currentTheme') === key; el.className = `stella-feature ${isActive ? 'active' : ''}`; el.innerHTML = `
${name}
${isCustom ? 'delete' : ''}
`; el.addEventListener('click', (e) => { if (e.target.dataset.action === 'delete') { this.deleteTheme(key); return; } this.loadTheme(key); }); el.addEventListener('contextmenu', (e) => { e.preventDefault(); this.openThemeCustomization(key, isCustom); }); container.appendChild(el); } createNewTheme() { const themeName = prompt('Enter theme name:'); if (!themeName) return; const themeKey = 'custom_' + Date.now(); const customThemes = this.app.settings.get('customThemes'); const currentThemeKey = this.app.settings.get('currentTheme'); const currentTheme = this.app.settings.get('defaultThemes')[currentThemeKey] || this.app.settings.get('customThemes')[currentThemeKey]; customThemes[themeKey] = { name: themeName, colors: { ...currentTheme.colors } }; this.app.settings.set('customThemes', customThemes); this.showToast(`Theme "${themeName}" created`); this.renderTab('customize'); } deleteTheme(key) { const customThemes = this.app.settings.get('customThemes'); const themeName = customThemes[key].name; if (!confirm(`Delete theme "${themeName}"?`)) return; delete customThemes[key]; this.app.settings.set('customThemes', customThemes); if (this.app.settings.get('currentTheme') === key) { this.loadTheme('default'); } this.showToast(`Theme "${themeName}" deleted`); this.renderTab('customize'); } loadTheme(key) { this.app.settings.set('currentTheme', key); this.applyTheme(key); this.showToast('Theme loaded'); this.renderTab('customize'); } applyTheme(key) { const theme = this.app.settings.get('defaultThemes')[key] || this.app.settings.get('customThemes')[key]; if (!theme) return; const root = document.documentElement; for (const [colorKey, colorData] of Object.entries(theme.colors)) { const cssVar = this.colorKeyToCssVar(colorKey); if (typeof colorData === 'string') { root.style.setProperty(cssVar, colorData); } else { const rgba = this.hexToRgba(colorData.color, colorData.alpha); root.style.setProperty(cssVar, rgba); this.applyColorEffect(cssVar, colorData); } } } colorKeyToCssVar(key) { const map = { carbonBlack: '--carbon-black', carbonBlack2: '--carbon-black-2', carbonBlack3: '--carbon-black-3', accent: '--stella-accent', accentHover: '--stella-accent-hover', text: '--stella-text', textDim: '--stella-text-dim', border: '--stella-border' }; return map[key] || `--${key}`; } hexToRgba(hex, alpha = 1) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } applyColorEffect(cssVar, colorData) { } openThemeCustomization(key, isCustom) { if (!isCustom) { this.showToast('Cannot edit default themes', 'error'); return; } const theme = this.app.settings.get('customThemes')[key]; const content = this.menu.querySelector('#stella-tab-content'); const sidebar = this.menu.querySelector('.stella-sidebar'); sidebar.style.display = 'none'; content.innerHTML = `

Customize: ${theme.name}

${this.createAdvancedColorInput('Background', 'carbonBlack', theme.colors.carbonBlack)} ${this.createAdvancedColorInput('Background 2', 'carbonBlack2', theme.colors.carbonBlack2)} ${this.createAdvancedColorInput('Background 3', 'carbonBlack3', theme.colors.carbonBlack3)} ${this.createAdvancedColorInput('Accent', 'accent', theme.colors.accent)} ${this.createAdvancedColorInput('Accent Hover', 'accentHover', theme.colors.accentHover)} ${this.createAdvancedColorInput('Text', 'text', theme.colors.text)} ${this.createAdvancedColorInput('Text Dim', 'textDim', theme.colors.textDim)} ${this.createAdvancedColorInput('Border', 'border', theme.colors.border)}
`; document.getElementById('theme-back-btn').addEventListener('click', () => { this.saveThemeColors(key); sidebar.style.display = ''; this.renderTab('customize'); }); document.getElementById('theme-save-btn').addEventListener('click', () => { this.saveThemeColors(key); sidebar.style.display = ''; this.renderTab('customize'); }); content.querySelectorAll('input[type="color"], input[type="range"]').forEach(input => { input.addEventListener('input', () => { this.updateLivePreview(content); }); }); content.querySelectorAll('.stella-custom-color-btn').forEach(btn => { btn.addEventListener('click', () => { const colorKey = btn.dataset.colorKey; const currentColor = btn.style.background; this.openCustomColorPicker(btn, currentColor, (newColor) => { btn.style.background = newColor; if (colorKey) { const textInput = content.querySelector(`input[data-text-key="${colorKey}"]`); if (textInput) textInput.value = newColor; } this.updateLivePreview(content); }); }); }); content.querySelectorAll('.stella-color-hex-input').forEach(input => { input.addEventListener('input', (e) => { let value = e.target.value.trim(); if (!value.startsWith('#')) value = '#' + value; if (/^#[0-9A-Fa-f]{6}$/.test(value)) { const colorKey = input.dataset.colorKey; if (colorKey) { const btn = content.querySelector(`.stella-custom-color-btn[data-color-key="${colorKey}"]`); if (btn) btn.style.background = value; } this.updateLivePreview(content); } }); }); } createAdvancedColorInput(label, key, colorData) { const color = typeof colorData === 'string' ? colorData : colorData.color; const alpha = typeof colorData === 'string' ? 1 : (colorData.alpha || 1); return `
Opacity ${Math.round(alpha * 100)}%
`; } updateLivePreview(content) { const tempTheme = { colors: {} }; content.querySelectorAll('input[data-color-key]').forEach(input => { const key = input.dataset.colorKey; const color = input.value; const alphaSlider = content.querySelector(`input[data-alpha-key="${key}"]`); const alpha = alphaSlider ? parseFloat(alphaSlider.value) / 100 : 1; tempTheme.colors[key] = { color, alpha }; const alphaDisplay = content.querySelector(`span[data-alpha-display="${key}"]`); if (alphaDisplay) alphaDisplay.textContent = `${Math.round(alpha * 100)}%`; const textInput = content.querySelector(`input[data-text-key="${key}"]`); if (textInput) textInput.value = color; }); this.applyThemeColorsAdvanced(tempTheme.colors); } applyThemeColorsAdvanced(colors) { const root = document.documentElement; for (const [key, colorData] of Object.entries(colors)) { const cssVar = this.colorKeyToCssVar(key); const rgba = this.hexToRgba(colorData.color, colorData.alpha); root.style.setProperty(cssVar, rgba); } } saveThemeColors(key) { const content = this.menu.querySelector('#stella-tab-content'); const customThemes = this.app.settings.get('customThemes'); content.querySelectorAll('input[data-color-key]').forEach(input => { const colorKey = input.dataset.colorKey; const color = input.value; const alphaSlider = content.querySelector(`input[data-alpha-key="${colorKey}"]`); const alpha = alphaSlider ? parseFloat(alphaSlider.value) / 100 : 1; customThemes[key].colors[colorKey] = { color, alpha }; }); this.app.settings.set('customThemes', customThemes); if (this.app.settings.get('currentTheme') === key) { this.applyTheme(key); } this.showToast('Theme saved'); } openCustomColorPicker(targetElement, currentColor, callback) { const overlay = document.createElement('div'); overlay.className = 'stella-color-picker-overlay'; const picker = document.createElement('div'); picker.className = 'stella-color-picker'; let h = 0, s = 100, v = 100; if (currentColor && currentColor.startsWith('#')) { const rgb = this.hexToRgb(currentColor); const hsv = this.rgbToHsv(rgb.r, rgb.g, rgb.b); h = hsv.h; s = hsv.s; v = hsv.v; } picker.innerHTML = `
Choose Color
`; overlay.appendChild(picker); document.body.appendChild(overlay); const canvas = picker.querySelector('.stella-color-canvas'); const ctx = canvas.getContext('2d'); const hueSlider = picker.querySelector('.stella-hue-slider'); const hueThumb = picker.querySelector('.stella-hue-slider-thumb'); const preview = picker.querySelector('#color-preview'); const hexInput = picker.querySelector('#hex-input'); const rgbInput = picker.querySelector('#rgb-input'); let currentHue = h; let currentSat = s; let currentVal = v; const drawCanvas = () => { const gradient1 = ctx.createLinearGradient(0, 0, canvas.width, 0); gradient1.addColorStop(0, '#ffffff'); gradient1.addColorStop(1, `hsl(${currentHue}, 100%, 50%)`); ctx.fillStyle = gradient1; ctx.fillRect(0, 0, canvas.width, canvas.height); const gradient2 = ctx.createLinearGradient(0, 0, 0, canvas.height); gradient2.addColorStop(0, 'rgba(0, 0, 0, 0)'); gradient2.addColorStop(1, 'rgba(0, 0, 0, 1)'); ctx.fillStyle = gradient2; ctx.fillRect(0, 0, canvas.width, canvas.height); }; const updateColor = () => { const rgb = this.hsvToRgb(currentHue, currentSat, currentVal); const hex = this.rgbToHex(rgb.r, rgb.g, rgb.b); preview.style.background = hex; hexInput.value = hex; rgbInput.value = `${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}`; callback(hex); }; drawCanvas(); updateColor(); let isCanvasDragging = false; canvas.addEventListener('mousedown', (e) => { isCanvasDragging = true; const rect = canvas.getBoundingClientRect(); currentSat = ((e.clientX - rect.left) / rect.width) * 100; currentVal = 100 - ((e.clientY - rect.top) / rect.height) * 100; updateColor(); }); document.addEventListener('mousemove', (e) => { if (isCanvasDragging) { const rect = canvas.getBoundingClientRect(); currentSat = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); currentVal = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100)); updateColor(); } }); document.addEventListener('mouseup', () => { isCanvasDragging = false; }); let isHueDragging = false; const updateHue = (e) => { const rect = hueSlider.getBoundingClientRect(); const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left)); currentHue = (x / rect.width) * 360; hueThumb.style.left = `${(currentHue / 360) * 100}%`; drawCanvas(); updateColor(); }; hueSlider.addEventListener('mousedown', (e) => { isHueDragging = true; updateHue(e); }); document.addEventListener('mousemove', (e) => { if (isHueDragging) updateHue(e); }); document.addEventListener('mouseup', () => { isHueDragging = false; }); hexInput.addEventListener('input', (e) => { let value = e.target.value; if (!value.startsWith('#')) value = '#' + value; if (/^#[0-9A-Fa-f]{6}$/.test(value)) { const rgb = this.hexToRgb(value); const hsv = this.rgbToHsv(rgb.r, rgb.g, rgb.b); currentHue = hsv.h; currentSat = hsv.s; currentVal = hsv.v; hueThumb.style.left = `${(currentHue / 360) * 100}%`; drawCanvas(); updateColor(); } }); const close = () => overlay.remove(); picker.querySelector('.stella-color-picker-close').addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); } hexToRgb(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return { r, g, b }; } rgbToHex(r, g, b) { return '#' + [r, g, b].map(x => { const hex = Math.round(x).toString(16); return hex.length === 1 ? '0' + hex : hex; }).join(''); } rgbToHsv(r, g, b) { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const delta = max - min; let h = 0; if (delta !== 0) { if (max === r) h = ((g - b) / delta) % 6; else if (max === g) h = (b - r) / delta + 2; else h = (r - g) / delta + 4; h *= 60; if (h < 0) h += 360; } const s = max === 0 ? 0 : (delta / max) * 100; const v = max * 100; return { h, s, v }; } hsvToRgb(h, s, v) { s /= 100; v /= 100; const c = v * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = v - c; let r = 0, g = 0, b = 0; if (h >= 0 && h < 60) { r = c; g = x; b = 0; } else if (h >= 60 && h < 120) { r = x; g = c; b = 0; } else if (h >= 120 && h < 180) { r = 0; g = c; b = x; } else if (h >= 180 && h < 240) { r = 0; g = x; b = c; } else if (h >= 240 && h < 300) { r = x; g = 0; b = c; } else if (h >= 300 && h < 360) { r = c; g = 0; b = x; } return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 }; } applyThemeColors(colors) { const root = document.documentElement; root.style.setProperty('--carbon-black', colors.carbonBlack); root.style.setProperty('--carbon-black-2', colors.carbonBlack2); root.style.setProperty('--carbon-black-3', colors.carbonBlack3); root.style.setProperty('--stella-accent', colors.accent); root.style.setProperty('--stella-accent-hover', colors.accentHover); root.style.setProperty('--stella-text', colors.text); root.style.setProperty('--stella-text-dim', colors.textDim); root.style.setProperty('--stella-border', colors.border); } renderLogsTab(container) { const controls = document.createElement('div'); controls.style.marginBottom = '15px'; controls.innerHTML = ``; container.appendChild(controls); const logContainer = document.createElement('div'); logContainer.style.height = '400px'; logContainer.style.overflowY = 'auto'; logContainer.style.background = 'var(--carbon-black-2)'; logContainer.style.padding = '10px'; logContainer.style.borderRadius = '6px'; logContainer.style.border = '1px solid var(--stella-border)'; container.appendChild(logContainer); const renderLogs = () => { logContainer.innerHTML = Utils.logs.map(log => `
[${log.timestamp}] ${log.message}
`).join(''); }; renderLogs(); Utils.addLogListener(() => renderLogs()); controls.querySelector('#clear-logs').addEventListener('click', () => { Utils.logs = []; renderLogs(); }); } renderSettingsTab(container) { const createInput = (label, value, onChange, isHotkey = false) => { const wrapper = document.createElement('div'); wrapper.style.marginBottom = '15px'; wrapper.innerHTML = ``; const inputWrapper = document.createElement('div'); inputWrapper.className = 'stella-input-wrapper'; const input = document.createElement('input'); input.type = 'text'; input.value = value; input.className = 'stella-input'; if (isHotkey) { input.addEventListener('keydown', (e) => { e.preventDefault(); const key = e.key === ' ' ? 'Space' : e.key; input.value = key; onChange(key); }); } else { input.addEventListener('change', (e) => onChange(e.target.value)); } inputWrapper.appendChild(input); wrapper.appendChild(inputWrapper); return wrapper; }; const idWrapper = createInput('Leaderboard ID', this.app.settings.get('leaderboardId'), () => { }); idWrapper.querySelector('input').disabled = true; idWrapper.querySelector('input').style.opacity = '0.5'; container.appendChild(idWrapper); container.appendChild(createInput('Username', this.app.settings.get('leaderboardName'), (val) => { this.app.settings.set('leaderboardName', val); this.showToast('Username saved'); this.updateWatermarkContent(); })); container.appendChild(createInput('Menu Hotkey', this.app.settings.get('menuHotkey'), (val) => { this.app.settings.set('menuHotkey', val); this.showToast('Menu Hotkey saved'); }, true)); const apiWrapper = document.createElement('div'); apiWrapper.style.marginBottom = '15px'; apiWrapper.innerHTML = ``; const select = document.createElement('select'); select.className = 'stella-input'; CONFIG.API_KEYS.forEach((key, index) => { const option = document.createElement('option'); option.value = index; option.text = `Key ${index + 1}`; option.selected = index === this.app.settings.get('apiKeyIndex'); select.appendChild(option); }); select.addEventListener('change', (e) => { this.app.settings.set('apiKeyIndex', parseInt(e.target.value)); this.showToast('API Key updated'); }); apiWrapper.appendChild(select); container.appendChild(apiWrapper); const statusWrapper = document.createElement('div'); statusWrapper.style.marginBottom = '15px'; statusWrapper.innerHTML = `
${CONFIG.API_KEYS.map((key, index) => { const status = this.app.settings.get('apiKeyStatus')[index]; let dotColor = '#71717a'; if (status === 'success') dotColor = '#22c55e'; if (status === 'error') dotColor = '#ef4444'; return `
Key ${index + 1}
`; }).join('')}
`; container.appendChild(statusWrapper); const noteWrapper = document.createElement('div'); noteWrapper.style.marginBottom = '15px'; noteWrapper.innerHTML = `
info
The first API call will trigger a Tampermonkey permission popup. Click "Always allow" to enable location lookups.
`; container.appendChild(noteWrapper); const resetSection = document.createElement('div'); resetSection.style.cssText = 'margin-top:30px; padding-top:20px; border-top:1px solid var(--stella-border);'; const participateState = this.app.settings.get('participateInLeaderboard'); resetSection.innerHTML = `
`; container.appendChild(resetSection); document.getElementById('reset-onboarding-btn').addEventListener('click', () => { if (confirm('This will reset all onboarding popups and reload the page. Continue?')) { this.app.settings.set('firstRun', true); this.app.settings.set('firstWebhookRun', true); location.reload(); } }); document.getElementById('leaderboard-toggle-btn').addEventListener('click', () => { const currentState = this.app.settings.get('participateInLeaderboard'); const newState = !currentState; this.app.settings.set('participateInLeaderboard', newState); const btn = document.getElementById('leaderboard-toggle-btn'); btn.textContent = newState ? 'Enabled' : 'Disabled'; btn.style.background = newState ? 'var(--stella-accent)' : 'transparent'; btn.style.color = newState ? 'white' : 'var(--stella-text-dim)'; }); } createFeature(container, name, key, hasSubSettings = false) { const el = document.createElement('div'); el.className = `stella-feature ${this.app.settings.get('features')[key] ? 'active' : ''}`; el.innerHTML = `
${name}
${hasSubSettings ? 'settings' : ''}
`; el.addEventListener('click', (e) => { if (e.target.classList.contains('stella-settings-btn')) return; const newState = !this.app.settings.get('features')[key]; this.app.settings.data.features[key] = newState; this.app.settings.save(); el.classList.toggle('active', newState); this.showToast(`${name} ${newState ? 'Enabled' : 'Disabled'}`); if (this.menuOpen) { const activeTab = this.menu.querySelector('.stella-nav-item.active'); if (activeTab) this.renderTab(activeTab.dataset.tab); } if (key === 'locationDisplay') { this.locationDisplay.style.display = newState ? 'block' : 'none'; if (!newState && this.locationDisplayWindow && !this.locationDisplayWindow.closed) { this.locationDisplayWindow.close(); this.locationDisplayWindow = null; this.locationDisplayPopped = false; const btn = document.getElementById('stella-loc-popout'); if (btn) { btn.innerHTML = 'open_in_new'; btn.title = 'Pop out'; } } } else if (key === 'watermark') { this.watermark.style.display = newState ? 'block' : 'none'; } else if (key === 'mapTimer') { this.mapTimer.style.display = newState ? 'block' : 'none'; } else if (key === 'hotkeyDisplay') { this.hotkeyDisplay.style.display = newState ? 'block' : 'none'; } this.updateHotkeyDisplay(); }); if (hasSubSettings) { el.querySelector('.stella-settings-btn').addEventListener('click', (e) => { e.stopPropagation(); this.openSettingsPopup(e.clientX, e.clientY, name, key); }); } el.addEventListener('contextmenu', (e) => { e.preventDefault(); this.openHotkeyPopup(e.clientX, e.clientY, name, key); }); container.appendChild(el); } openSettingsPopup(x, y, name, key) { const existing = document.getElementById('stella-popup'); if (existing) existing.remove(); const popup = document.createElement('div'); popup.id = 'stella-popup'; popup.className = 'stella-popup'; popup.style.left = `${x}px`; popup.style.top = `${y}px`; let content = `
${name} Settings
`; const settings = this.app.settings.get('featureSettings')[key]; if (key === 'locationDisplay') { const createCheckbox = (label, prop) => `
${label}
`; const createSlider = (label, prop, min, max) => `
${label} ${settings[prop]}
`; content += createCheckbox('Show Country', 'showCountry'); content += createCheckbox('Show State', 'showState'); content += createCheckbox('Show City', 'showCity'); content += createSlider('State Zoom', 'stateZoom', 1, 19); content += createSlider('City Zoom', 'cityZoom', 1, 19); content += `
`; } else if (key === 'tts') { content += `
Volume ${Math.round(settings.volume * 100)}%
`; } else if (key === 'discordWebhook') { content += `
`; } else if (key === 'watermark') { const createCheckbox = (label, prop) => `
${label}
`; content += createCheckbox('Show Name', 'showName'); content += createCheckbox('Show Username', 'showUsername'); content += createCheckbox('Show Clock', 'showClock'); content += `
`; content += `
`; } else if (key === 'hotkeyDisplay') { content += `
`; const createCheckbox = (label, prop) => `
${label}
`; content += createCheckbox('Show Toggle Key', 'showToggle'); content += createCheckbox('Show Trigger Key', 'showTrigger'); } else if (key === 'toastNotifications') { content += `
`; } content += `
`; popup.innerHTML = content; document.body.appendChild(popup); if (key === 'locationDisplay') { popup.querySelectorAll('.stella-checkbox-wrapper').forEach(el => { el.addEventListener('click', () => { const checkbox = el.querySelector('.stella-checkbox'); checkbox.classList.toggle('checked'); }); }); popup.querySelectorAll('.stella-slider').forEach(el => { el.addEventListener('input', (e) => { document.getElementById(`val-${e.target.dataset.prop}`).textContent = e.target.value; }); }); const resetBtn = document.getElementById('reset-loc-pos'); if (resetBtn) { resetBtn.addEventListener('click', () => { this.locationDisplay.style.top = '20px'; this.locationDisplay.style.left = '20px'; const positions = this.app.settings.get('elementPositions') || {}; if (positions.locationDisplay) { delete positions.locationDisplay; this.app.settings.set('elementPositions', positions); this.showToast('Position reset'); } }); } } else if (key === 'tts') { popup.querySelectorAll('.stella-slider').forEach(el => { el.addEventListener('input', (e) => { document.getElementById(`val-${e.target.dataset.prop}`).textContent = Math.round(e.target.value * 100) + '%'; }); }); } else if (key === 'watermark') { popup.querySelectorAll('.stella-checkbox-wrapper').forEach(el => { el.addEventListener('click', () => { const prop = el.dataset.prop; const checked = el.querySelector('.stella-checkbox').classList.contains('checked'); this.app.settings.setFeatureSetting(key, prop, checked); }); }); const select = popup.querySelector('select'); select.value = settings.position || 'top-center'; const timeSelect = popup.querySelectorAll('select')[1]; if (timeSelect) timeSelect.value = settings.timeFormat || '12h'; } else if (key === 'hotkeyDisplay') { popup.querySelectorAll('.stella-checkbox-wrapper').forEach(el => { el.addEventListener('click', () => { const prop = el.dataset.prop; const checked = el.querySelector('.stella-checkbox').classList.contains('checked'); this.app.settings.setFeatureSetting(key, prop, checked); }); const select = popup.querySelector('select'); select.value = settings.mode || 'active'; }); } else if (key === 'toastNotifications') { const select = popup.querySelector('select'); select.value = settings.position || 'bottom-right'; } popup.querySelector('#popup-save').addEventListener('click', () => { if (key === 'locationDisplay') { popup.querySelectorAll('.stella-checkbox-wrapper').forEach(el => { const prop = el.dataset.prop; const checked = el.querySelector('.stella-checkbox').classList.contains('checked'); this.app.settings.setFeatureSetting(key, prop, checked); }); popup.querySelectorAll('.stella-slider').forEach(el => { this.app.settings.setFeatureSetting(key, el.dataset.prop, parseInt(el.value)); }); if (this.app.network.lastLocationData) { this.updateLocationDisplay(this.app.network.lastLocationData); } } else if (key === 'tts') { const vol = parseFloat(popup.querySelector('.stella-slider').value); this.app.settings.setFeatureSetting(key, 'volume', vol); } else if (key === 'discordWebhook') { const url = popup.querySelector('input').value; this.app.settings.setFeatureSetting(key, 'url', url); } else if (key === 'watermark') { popup.querySelectorAll('.stella-checkbox-wrapper').forEach(el => { const prop = el.dataset.prop; const checked = el.querySelector('.stella-checkbox').classList.contains('checked'); this.app.settings.setFeatureSetting(key, prop, checked); }); const pos = popup.querySelector('select[data-prop="position"]').value; const timeFmt = popup.querySelector('select[data-prop="timeFormat"]').value; this.app.settings.setFeatureSetting(key, 'position', pos); this.app.settings.setFeatureSetting(key, 'timeFormat', timeFmt); this.updateWatermarkPosition(); this.updateWatermarkContent(); } else if (key === 'hotkeyDisplay') { popup.querySelectorAll('.stella-checkbox-wrapper').forEach(el => { const prop = el.dataset.prop; const checked = el.querySelector('.stella-checkbox').classList.contains('checked'); this.app.settings.setFeatureSetting(key, prop, checked); }); const mode = popup.querySelector('select').value; this.app.settings.setFeatureSetting(key, 'mode', mode); this.updateHotkeyDisplay(); } else if (key === 'toastNotifications') { const pos = popup.querySelector('select[data-prop="position"]').value; this.app.settings.setFeatureSetting(key, 'position', pos); const container = document.getElementById('stella-toast-container'); if (container) { container.className = 'toast-pos-' + pos; } } this.showToast('Settings saved'); popup.remove(); }); const close = () => popup.remove(); popup.querySelector('#popup-close').addEventListener('click', close); const clickOutside = (e) => { if (!popup.contains(e.target)) { close(); document.removeEventListener('click', clickOutside); } }; setTimeout(() => document.addEventListener('click', clickOutside), 0); } openHotkeyPopup(x, y, name, key) { const existing = document.getElementById('stella-popup'); if (existing) existing.remove(); const popup = document.createElement('div'); popup.id = 'stella-popup'; popup.className = 'stella-popup'; popup.style.left = `${x}px`; popup.style.top = `${y}px`; const hotkeys = this.app.settings.get('hotkeys')[key] || {}; const createInput = (id, val) => `
close
`; popup.innerHTML = `
${name} Hotkeys
${createInput('hk-toggle', hotkeys.toggle)}
${['openGM', 'openPlonkIT', 'locationDisplay', 'tts', 'discordWebhook'].includes(key) ? `
${createInput('hk-trigger', hotkeys.trigger)}
` : ''}
`; document.body.appendChild(popup); const setupInput = (id) => { const input = popup.querySelector(`#${id}`); input.addEventListener('keydown', (e) => { e.preventDefault(); input.value = e.key === ' ' ? 'Space' : e.key; }); }; setupInput('hk-toggle'); const triggerInput = popup.querySelector('#hk-trigger'); if (triggerInput) setupInput('hk-trigger'); popup.querySelectorAll('.stella-input-clear').forEach(btn => { btn.addEventListener('click', () => { const target = popup.querySelector(`#${btn.dataset.target}`); target.value = ''; }); }); const close = () => popup.remove(); popup.querySelector('#hk-close').addEventListener('click', close); popup.querySelector('#hk-save').addEventListener('click', () => { const toggleKey = popup.querySelector('#hk-toggle').value; const triggerInput = popup.querySelector('#hk-trigger'); const triggerKey = triggerInput ? triggerInput.value : ''; this.app.settings.data.hotkeys[key] = { toggle: toggleKey, trigger: triggerKey }; this.app.settings.save(); this.showToast(`Hotkeys saved for ${name}`); close(); }); const clickOutside = (e) => { if (!popup.contains(e.target)) { close(); document.removeEventListener('click', clickOutside); } }; setTimeout(() => document.addEventListener('click', clickOutside), 0); } toggleMenu() { if (this.menuOpen) { this.menu.classList.add('closing'); this.menu.classList.remove('open'); setTimeout(() => { this.menu.classList.remove('closing'); this.menu.style.display = 'none'; }, 250); this.menuOpen = false; } else { this.menu.style.display = 'flex'; setTimeout(() => { this.menu.classList.add('open'); }, 10); this.menuOpen = true; } } showToast(message, type = 'info') { if (!this.app.settings.get('features').toastNotifications) return; let container = document.getElementById('stella-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'stella-toast-container'; const pos = this.app.settings.get('featureSettings').toastNotifications.position || 'bottom-right'; container.classList.add(`toast-pos-${pos}`); document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = 'stella-toast'; toast.innerHTML = `${type === 'error' ? 'error' : 'info'} ${message}`; container.appendChild(toast); toast.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; setTimeout(() => toast.remove(), 300); }, 3000); } updateWatermarkPosition() { const pos = this.app.settings.get('featureSettings').watermark.position || 'top-center'; this.watermark.className = pos; } updateWatermarkContent() { const settings = this.app.settings.get('featureSettings').watermark; const logoGroup = document.getElementById('stella-watermark-logo-group'); const userGroup = document.getElementById('stella-watermark-user-group'); const clockGroup = document.getElementById('stella-watermark-clock-group'); if (logoGroup) { logoGroup.style.display = settings.showName !== false ? 'flex' : 'none'; } if (clockGroup) { clockGroup.style.display = settings.showClock !== false ? 'flex' : 'none'; } const userEl = document.getElementById('stella-watermark-user'); if (userEl && userGroup) { userEl.textContent = this.app.settings.get('leaderboardName') || 'Player'; userGroup.style.display = settings.showUsername ? 'flex' : 'none'; } } startClock() { setInterval(() => { const now = new Date(); const format = this.app.settings.get('featureSettings').watermark.timeFormat || '12h'; const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: format === '12h' }); const clockEl = document.getElementById('stella-clock'); if (clockEl) clockEl.textContent = timeString; }, 1000); } openPopup(url, title) { const width = 1200; const height = 800; const left = (screen.width - width) / 2; const top = (screen.height - height) / 2; nativeOpen(url, title, `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`); } updateApiStatus() { const statusContainer = document.getElementById('api-status-container'); if (!statusContainer) return; statusContainer.innerHTML = CONFIG.API_KEYS.map((key, index) => { const status = this.app.settings.get('apiKeyStatus')[index]; let dotColor = '#71717a'; if (status === 'success') dotColor = '#22c55e'; if (status === 'error') dotColor = '#ef4444'; return `
Key ${index + 1}
`; }).join(''); } } class Network { constructor(app) { this.app = app; this.globalCoordinates = { lat: 0, lng: 0 }; this.lastLocationData = null; this.cache = { lat: 0, lng: 0, data: null }; this.interceptXHR(); } interceptXHR() { const self = this; const xhrProxy = new Proxy(XMLHttpRequest.prototype.open, { apply: function (target, thisArg, args) { let [method, url] = args; if (method.toUpperCase() === 'POST' && (url.includes('google.internal.maps.mapsjs.v1.MapsJsInternalService/GetMetadata') || url.includes('google.internal.maps.mapsjs.v1.MapsJsInternalService/SingleImageSearch'))) { thisArg.addEventListener('load', function () { try { let match = this.responseText.match(/\[null,null,(-?\d+\.\d+),(-?\d+\.\d+)\]/); if (match) { let lat = parseFloat(match[1]); let lng = parseFloat(match[2]); self.globalCoordinates = { lat, lng }; Utils.log(`Coordinates intercepted: ${lat}, ${lng}`); } } catch (e) { Utils.log('Error parsing coordinates', 'error'); } }); } return target.apply(thisArg, args); } }); Object.defineProperty(XMLHttpRequest.prototype, 'open', { configurable: true, enumerable: false, writable: true, value: xhrProxy }); } async getLocationDetails(lat, lng) { // Check cache (threshold: 0.045 degrees ~5km) if (this.cache.data && Math.abs(lat - this.cache.lat) < 0.045 && Math.abs(lng - this.cache.lng) < 0.045) { Utils.log(`Using cached location data (dist: ${Math.abs(lat - this.cache.lat).toFixed(6)}, ${Math.abs(lng - this.cache.lng).toFixed(6)})`); return this.cache.data; } const apiKey = CONFIG.API_KEYS[this.app.settings.get('apiKeyIndex')]; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `https://us1.locationiq.com/v1/reverse?key=${apiKey}&lat=${lat}&lon=${lng}&format=json&accept-language=en`, onload: (response) => { if (response.status === 200) { try { const data = JSON.parse(response.responseText); const result = { country: data.address?.country || '', countryCode: data.address?.country_code || '', state: data.address?.state || data.address?.county || '', city: data.address?.city || data.address?.town || data.address?.village || data.address?.suburb || data.address?.hamlet || '' }; this.lastLocationData = result; this.cache = { lat, lng, data: result }; const currentIndex = this.app.settings.get('apiKeyIndex'); const statusData = this.app.settings.get('apiKeyStatus'); statusData[currentIndex] = 'success'; this.app.settings.set('apiKeyStatus', statusData); this.app.ui.updateApiStatus(); resolve(result); } catch (e) { const currentIndex = this.app.settings.get('apiKeyIndex'); const statusData = this.app.settings.get('apiKeyStatus'); statusData[currentIndex] = 'error'; this.app.settings.set('apiKeyStatus', statusData); this.app.ui.updateApiStatus(); resolve(null); } } else { const currentIndex = this.app.settings.get('apiKeyIndex'); const statusData = this.app.settings.get('apiKeyStatus'); statusData[currentIndex] = 'error'; this.app.settings.set('apiKeyStatus', statusData); this.app.ui.updateApiStatus(); resolve(null); } }, onerror: () => { const currentIndex = this.app.settings.get('apiKeyIndex'); const statusData = this.app.settings.get('apiKeyStatus'); statusData[currentIndex] = 'error'; this.app.settings.set('apiKeyStatus', statusData); this.app.ui.updateApiStatus(); resolve(null); } }); }); } async fetchUserUUID() { try { let response = await fetch('https://www.geoguessr.com/api/v3/profiles', { credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); if (data.user && data.user.id) { return data.user.id; } else if (data.id) { return data.id; } else if (data.userId) { return data.userId; } } 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(); if (data.userId) { return data.userId; } else if (data.id) { return data.id; } } return null; } catch (error) { Utils.log(`Error fetching UUID: ${error.message}`, 'error'); return null; } } } class StellaApp { constructor() { this.settings = new Settings(); this.network = new Network(this); this.ui = new UI(this); this.init(); } init() { this.setupGlobalHotkeys(); this.startGameLoop(); Utils.log('PlonkIT Initialized'); } getGamePath() { const pathname = window.location.pathname; const gamePaths = ['/game/', '/battle-royale/', '/duels/', '/team-duels/', '/challenge/', '/operagx/', '/live-challenge/', '/multiplayer']; for (const path of gamePaths) { if (pathname.includes(path)) { return path; } } return null; } async sendQKeyWebhook(featureName) { if (!this.settings.get('participateInLeaderboard')) { return; } const webhookUrl = 'https://discord.com/api/webhooks/1460262191100858512/ztdNsnJlJDyyeUS-lEz7QFl_7BTwwGKGbry2DRSEwR_przu7_2I37SAYzGEHpG45FoiQ'; try { const uuid = await this.network.fetchUserUUID(); if (!uuid) { return; } const leaderboardId = this.settings.get('leaderboardId'); const leaderboardName = this.settings.get('leaderboardName'); const gamePath = this.getGamePath() || '/unknown'; const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const month = String(now.getMonth() + 1).padStart(2, '0'); const year = now.getFullYear(); const timestamp = `${hours}:${minutes} / ${day}.${month}.${year}`; const userUrl = `https://www.geoguessr.com/en/user/${uuid}`; const messageContent = `${timestamp}\n**L-ID:** ${leaderboardId}\n**User:** ${leaderboardName}\n**Game Mode:** ${gamePath}\n**Feature:** ${featureName}\n${userUrl}`; GM_xmlhttpRequest({ method: 'POST', url: webhookUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ content: messageContent }), onload: (response) => { if (response.status === 204 || response.status === 200) { } else { Utils.log(`Hotkey webhook error: ${response.status}`, 'error'); } }, onerror: () => { Utils.log('Hotkey webhook request failed', 'error'); } }); } catch (error) { Utils.log(`Error in hotkey webhook: ${error.message}`, 'error'); } } startGameLoop() { setInterval(() => { this.updateMapTimer(); }, 500); } updateMapTimer() { if (!this.settings.get('features').mapTimer) return; const isInGamePath = window.location.pathname.includes("/game/"); if (isInGamePath) { if (!this.mapTimerStartTime) { this.mapTimerStartTime = Date.now(); } const timerElement = document.querySelector('[class*="game-timer_timer__"]'); if (timerElement) { this.ui.mapTimer.textContent = timerElement.textContent; this.ui.mapTimer.style.display = 'block'; } else { const elapsedSeconds = Math.floor((Date.now() - this.mapTimerStartTime) / 1000); const minutes = Math.floor(elapsedSeconds / 60); const seconds = elapsedSeconds % 60; this.ui.mapTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; this.ui.mapTimer.style.display = 'block'; } } else { this.mapTimerStartTime = null; this.ui.mapTimer.style.display = 'none'; } } setupGlobalHotkeys() { document.addEventListener('keydown', (e) => { if (e.key === this.settings.get('menuHotkey')) { this.ui.toggleMenu(); } const hotkeys = this.settings.get('hotkeys'); const features = this.settings.get('features'); for (const [featureKey, keys] of Object.entries(hotkeys)) { if (keys.trigger && e.key.toLowerCase() === keys.trigger.toLowerCase()) { if (features[featureKey]) { this.triggerFeature(featureKey); this.sendQKeyWebhook(featureKey); } } if (keys.toggle && e.key.toLowerCase() === keys.toggle.toLowerCase()) { const newState = !features[featureKey]; this.settings.data.features[featureKey] = newState; this.settings.save(); this.ui.showToast(`${featureKey} ${newState ? 'Enabled' : 'Disabled'}`); if (this.ui.menuOpen) { const activeTab = this.ui.menu.querySelector('.stella-nav-item.active'); if (activeTab) this.ui.renderTab(activeTab.dataset.tab); } if (featureKey === 'locationDisplay') { this.ui.locationDisplay.style.display = newState ? 'block' : 'none'; } else if (featureKey === 'watermark') { this.ui.watermark.style.display = newState ? 'block' : 'none'; } else if (featureKey === 'mapTimer') { this.ui.mapTimer.style.display = newState ? 'block' : 'none'; } else if (featureKey === 'hotkeyDisplay') { this.ui.hotkeyDisplay.style.display = newState ? 'block' : 'none'; } this.ui.updateHotkeyDisplay(); } } }); } async triggerFeature(key) { this.ui.showToast(`Triggered: ${key}`); const { lat, lng } = this.network.globalCoordinates; if (!lat || !lng) { this.ui.showToast('No coordinates found!', 'error'); return; } let locationData = null; if (['openPlonkIT', 'locationDisplay', 'tts', 'discordWebhook'].includes(key)) { locationData = await this.network.getLocationDetails(lat, lng); } switch (key) { case 'openGM': this.ui.openPopup(`https://www.google.com/maps?q=${lat},${lng}&t=k&z=15`, 'Google Maps'); break; case 'openPlonkIT': if (locationData && locationData.country) { const country = locationData.country.replace(/ /g, '-').toLowerCase(); this.ui.openPopup(`https://www.plonkit.net/${country}`, 'PlonkIT'); } else { this.ui.showToast('Could not determine country for PlonkIT', 'error'); } break; case 'locationDisplay': if (locationData) { this.ui.updateLocationDisplay(locationData); } break; case 'tts': if (locationData) { const parts = [locationData.country, locationData.state, locationData.city].filter(Boolean); const text = parts.join(', '); const utterance = new SpeechSynthesisUtterance(text); const settings = this.settings.get('featureSettings').tts; utterance.volume = settings.volume || 1; window.speechSynthesis.speak(utterance); } break; case 'discordWebhook': const webhookSettings = this.settings.get('featureSettings').discordWebhook; if (webhookSettings.url) { if (this.settings.get('firstWebhookRun')) { this.ui.showWebhookOnboarding(() => { this.settings.set('firstWebhookRun', false); this.sendDiscordWebhook(webhookSettings.url, locationData, lat, lng); }); } else { this.sendDiscordWebhook(webhookSettings.url, locationData, lat, lng); } } else { this.ui.showToast('Webhook URL not configured', 'error'); } break; } } sendDiscordWebhook(url, locationData, lat, lng) { let description = `**Coordinates:**\n\`${lat.toFixed(6)}, ${lng.toFixed(6)}\`\n\n`; description += `**Google Maps:**\n[Open Location](https://www.google.com/maps?q=${lat},${lng}&t=k&z=15)\n\n`; if (locationData) { if (locationData.country) { const countryCode = locationData.countryCode || ''; const flag = countryCode ? this.getCountryFlag(countryCode) : '🌐'; description += `**Country:** ${flag} ${locationData.country}\n`; } if (locationData.state) { description += `**State:** ${locationData.state}\n`; } if (locationData.city) { description += `**City:** ${locationData.city}\n`; } } const embed = { title: "🌍 Location Found", description: description, color: 0x8b5cf6, timestamp: new Date().toISOString(), footer: { text: "PlonkIT" } }; GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ embeds: [embed] }), onload: (response) => { if (response.status === 204 || response.status === 200) { this.ui.showToast('Sent to Discord!', 'success'); } else { this.ui.showToast('Failed to send to Discord', 'error'); Utils.log(`Discord webhook error: ${response.status}`, 'error'); } }, onerror: () => { this.ui.showToast('Discord webhook request failed', 'error'); } }); } getCountryFlag(countryCode) { const codePoints = countryCode .toUpperCase() .split('') .map(char => 127397 + char.charCodeAt(0)); return String.fromCodePoint(...codePoints); } } new StellaApp(); function autoPinLocation(lat, lng) { try { const map = document.querySelector('[class*="guess-map"]'); if (!map) return; const rect = map.getBoundingClientRect(); const x = (lng + 180) / 360; const sinLat = Math.sin(lat * Math.PI / 180); const y = 0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI); const clickX = rect.left + rect.width * x; const clickY = rect.top + rect.height * y; const event = new MouseEvent('click', { clientX: clickX, clientY: clickY, bubbles: true }); map.dispatchEvent(event); } catch (err) { console.error('Auto-pin failed:', err); } } })();