Greasy Fork is available in English.
StellaGeo Geoguessr Cheat
// ==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
// ==/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 = `
<div class="stella-welcome-title">Welcome to Stella V4</div>
<div class="stella-welcome-text">
<p>Here's how to use the menu:</p>
<ul style="text-align:left; margin: 15px 0; padding-left: 20px;">
<li>Press <span class="stella-key">Insert</span> to toggle the menu.</li>
<li><span class="stella-key">Left Click</span> a feature to toggle it.</li>
<li><span class="stella-key">Right Click</span> a feature to set hotkeys.</li>
<li>Click the <span class="material-icons" style="font-size:14px; vertical-align:middle;">settings</span> icon for advanced settings.</li>
</ul>
</div>
<button class="stella-btn" id="stella-welcome-close">Get Started</button>
`;
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 = `
<div class="stella-welcome-title">Location API Setup</div>
<div class="stella-welcome-text">
<p>The first API call will trigger a Tampermonkey permission popup if you are not on the latest Tamper Monkey Version.</p>
<p style="margin-top:10px;">Click <strong>"Always allow"</strong> to enable location lookups.</p>
<img src="https://i.imgur.com/yIVeDlq.png" style="max-width:100%; margin:15px 0; border-radius:8px; border:1px solid var(--stella-border);" />
<div style="margin-top:15px; padding:10px; background:rgba(139, 92, 246, 0.1); border:1px solid rgba(139, 92, 246, 0.2); border-radius:6px;">
<div style="display:flex; align-items:start; gap:8px;">
<span class="material-icons" style="font-size:16px; color:var(--stella-accent);">info</span>
<div style="font-size:11px; color:var(--stella-text-dim); line-height:1.5;">
This permission allows the script to use the LocationIQ API for reverse geocoding.
</div>
</div>
</div>
</div>
<button class="stella-btn" id="stella-location-api-okay">Okay</button>
`;
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 = `
<div class="stella-welcome-title">Discord Webhook Setup</div>
<div class="stella-welcome-text">
<p>When you press "Okay", a Tampermonkey permission popup will appear (if you are not on the latest Tampermonkey version).</p>
<p style="margin-top:10px;">Click <strong>"Always allow"</strong> to enable Discord webhook functionality.</p>
<div style="margin-top:15px; padding:10px; background:rgba(139, 92, 246, 0.1); border:1px solid rgba(139, 92, 246, 0.2); border-radius:6px;">
<div style="display:flex; align-items:start; gap:8px;">
<span class="material-icons" style="font-size:16px; color:var(--stella-accent);">info</span>
<div style="font-size:11px; color:var(--stella-text-dim); line-height:1.5;">
This permission allows the script to send location data to your Discord webhook URL.
</div>
</div>
</div>
</div>
<button class="stella-btn" id="stella-webhook-okay">Okay</button>
`;
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 = `
<div class="stella-header">
<div class="stella-logo">
<img src="https://minimalistmoon.com/favlogo.png" alt="Logo">
<span class="stella-logo-text">Stella</span>
</div>
<div class="stella-header-actions">
<div class="stella-header-btn" data-tab="customize" title="Customize">
<span class="material-icons">edit</span>
</div>
<div class="stella-header-btn" data-tab="logs" title="Logs">
<span class="material-icons">list_alt</span>
</div>
<div class="stella-header-btn" data-tab="settings" title="Settings">
<span class="material-icons">settings</span>
</div>
</div>
</div>
<div class="stella-body">
<div class="stella-sidebar collapsed">
<div class="stella-nav">
<div class="stella-nav-highlight"></div>
<div class="stella-nav-item active" data-tab="location">
<span class="material-icons">place</span> <span class="stella-nav-text">Location</span>
</div>
<div class="stella-nav-item" data-tab="hud">
<span class="material-icons">dashboard</span> <span class="stella-nav-text">HUD</span>
</div>
<div class="stella-nav-item" data-tab="autoplay">
<span class="material-icons">play_arrow</span> <span class="stella-nav-text">Autoplay</span>
</div>
</div>
</div>
<div class="stella-content" id="stella-tab-content"></div>
</div>
`;
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 = `
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:6px;">
<span style="font-weight:600; color:var(--stella-text);">Location</span>
<button id="stella-loc-popout" title="Pop out" style="display:inline-flex; align-items:center; gap:4px; background:transparent; border:1px solid var(--stella-border); color:var(--stella-text-dim); border-radius:4px; padding:2px 6px; cursor:pointer; font-size:11px;">
<span class="material-icons" style="font-size:14px;">open_in_new</span>
</button>
</div>
<div class="stella-loc-row" id="stella-row-country">
<span class="material-icons stella-loc-icon">public</span>
<img id="stella-loc-flag" class="stella-flag" style="display:none;" />
<span id="stella-loc-country">Country: N/A</span>
</div>
<div class="stella-loc-row" id="stella-row-state">
<span class="material-icons stella-loc-icon">map</span>
<span id="stella-loc-state">State: N/A</span>
</div>
<div class="stella-loc-row" id="stella-row-city">
<span class="material-icons stella-loc-icon">location_city</span>
<span id="stella-loc-city">City: N/A</span>
</div>
<div id="stella-map-image"></div>
`;
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 = '<span class="material-icons" style="font-size:14px;">call_received</span>';
popButton.title = 'Pop back in';
} else {
this.popInLocationDisplay();
popButton.innerHTML = '<span class="material-icons" style="font-size:14px;">open_in_new</span>';
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 = `
<div style="display:flex; align-items:center; gap:12px;">
<div id="stella-watermark-logo-group" style="display:flex; align-items:center; gap:6px;">
<img src="https://minimalistmoon.com/favlogo.png" style="width:16px; height:16px; border-radius:4px;">
<span id="stella-watermark-name">Stella</span>
</div>
<div id="stella-watermark-user-group" style="display:flex; align-items:center; gap:4px; color:var(--stella-accent);">
<span class="material-icons" style="font-size:14px;">person</span>
<span id="stella-watermark-user"></span>
</div>
<div id="stella-watermark-clock-group" style="display:flex; align-items:center; gap:4px; opacity:0.8;">
<span class="material-icons" style="font-size:14px;">schedule</span>
<span id="stella-clock" style="font-weight:400;"></span>
</div>
</div>
`;
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 += `<div style="font-weight:700; margin-bottom:8px; color:white; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:6px; font-size:12px; letter-spacing:0.5px; text-transform:uppercase;">Hotkeys</div>`;
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 += `<div class="stella-hk-row ${isActive ? 'stella-hk-active' : ''}">
<span>${name}</span>
<div class="stella-hk-keys">`;
if (hk) {
if (settings.showToggle && hk.toggle) {
html += `<span class="stella-hk-key-badge" title="Toggle">${hk.toggle}</span>`;
}
if (settings.showTrigger && hk.trigger) {
html += `<span class="stella-hk-key-badge" title="Trigger">${hk.trigger}</span>`;
}
}
html += `</div></div>`;
}
if (!hasContent) html = '<div class="stella-hk-row">No active features</div>';
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(`<!DOCTYPE html><html><head><title>Location</title><style>${css}</style><link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"></head><body><div class="card">
<div class="header">
<span>Location</span>
<button id="pop-in" style="display:inline-flex; align-items:center; gap:4px; background:transparent; border:1px solid rgba(255,255,255,0.12); color:#fff; border-radius:4px; padding:2px 6px; cursor:pointer; font-size:11px;"><span class="material-icons" style="font-size:14px;">call_received</span></button>
</div>
<div class="row" id="pop-country-row"><span class="material-icons" style="font-size:16px;">public</span><img id="pop-flag" class="flag" style="display:none;"><span id="pop-country">Country: N/A</span></div>
<div class="row" id="pop-state-row"><span class="material-icons" style="font-size:16px;">map</span><span id="pop-state">State: N/A</span></div>
<div class="row" id="pop-city-row"><span class="material-icons" style="font-size:16px;">location_city</span><span id="pop-city">City: N/A</span></div>
<div class="map" id="pop-map"></div>
</div></body></html>`);
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 = '<span class="material-icons" style="font-size:14px;">open_in_new</span>';
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 = `
<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--stella-text-dim); gap:15px;">
<span class="material-icons" style="font-size:48px; color:#f97316;">warning</span>
<div style="text-align:center;">
<p style="font-size:16px; font-weight:600; color:white; margin-bottom:4px;">Maintenance Mode</p>
<p style="font-size:13px;">This feature is currently being updated.</p>
</div>
</div>
`;
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 = '<h3 style="color:white; font-size:16px; font-weight:600; margin-bottom:12px;">Default Themes</h3>';
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 = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<h3 style="color:white; font-size:16px; font-weight:600;">Custom Themes</h3>
<button class="stella-btn" id="create-theme-btn" style="padding:8px 16px;">
<span class="material-icons" style="font-size:16px; vertical-align:middle;">add</span>
Create Theme
</button>
</div>
`;
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 = `
<div class="stella-feature-info">
<div class="stella-feature-dot"></div>
<div class="stella-feature-name">${name}</div>
</div>
<div class="stella-feature-actions">
${isCustom ? '<span class="material-icons stella-settings-btn" style="opacity:1; color:#ef4444;" data-action="delete">delete</span>' : ''}
</div>
`;
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 = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px;">
<button class="stella-btn stella-btn-secondary" id="theme-back-btn" style="padding:8px 16px;">
<span class="material-icons" style="font-size:16px; vertical-align:middle;">arrow_back</span>
Back
</button>
<h2 style="color:white; font-size:18px; font-weight:700; margin:0;">Customize: ${theme.name}</h2>
<button class="stella-btn" id="theme-save-btn" style="padding:8px 16px;">
<span class="material-icons" style="font-size:16px; vertical-align:middle;">save</span>
Save
</button>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; max-height:450px; overflow-y:auto; padding-right:8px;">
${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)}
</div>
`;
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 `
<div style="margin-bottom:16px; padding:12px; background:var(--carbon-black-2); border-radius:8px; border:1px solid var(--stella-border);">
<label style="display:block; margin-bottom:8px; color:white; font-size:13px; font-weight:600;">${label}</label>
<!-- Color Picker Button -->
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
<div class="stella-custom-color-btn" data-color-key="${key}"
style="width:50px; height:40px; border:1px solid var(--stella-border); border-radius:6px; background:${color}; cursor:pointer; position:relative; overflow:hidden;">
<div style="position:absolute; inset:0; background:linear-gradient(45deg, #808080 25%, transparent 25%, transparent 75%, #808080 75%, #808080); background-size:10px 10px; background-position:0 0, 5px 5px; opacity:0.1;"></div>
</div>
<input type="text" value="${color}" class="stella-input stella-color-hex-input" data-text-key="${key}" data-color-key="${key}"
style="flex:1; font-family:monospace; text-transform:uppercase; font-size:11px;">
</div>
<!-- Alpha Slider -->
<div style="margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
<span style="font-size:11px; color:var(--stella-text-dim);">Opacity</span>
<span style="font-size:11px; color:var(--stella-accent);" data-alpha-display="${key}">${Math.round(alpha * 100)}%</span>
</div>
<input type="range" min="0" max="100" value="${alpha * 100}" data-alpha-key="${key}"
style="width:100%; height:4px; background:var(--carbon-black-3); border-radius:2px; cursor:pointer;">
</div>
</div>
`;
}
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 = `
<div class="stella-color-picker-header">
<div class="stella-color-picker-title">Choose Color</div>
<button class="stella-color-picker-close">
<span class="material-icons" style="font-size:20px;">close</span>
</button>
</div>
<canvas class="stella-color-canvas" width="280" height="200"></canvas>
<div class="stella-hue-slider">
<div class="stella-hue-slider-thumb" style="left:${(h / 360) * 100}%;"></div>
</div>
<div class="stella-color-preview">
<div class="stella-color-preview-box" id="color-preview"></div>
</div>
<div class="stella-color-inputs">
<div class="stella-color-input-group">
<label class="stella-color-input-label">Hex</label>
<input type="text" class="stella-color-input-field" id="hex-input" maxlength="7" value="${currentColor || '#ffffff'}">
</div>
<div class="stella-color-input-group">
<label class="stella-color-input-label">RGB</label>
<input type="text" class="stella-color-input-field" id="rgb-input" readonly>
</div>
</div>
`;
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 = `<button class="stella-btn stella-btn-secondary" id="clear-logs">Clear Logs</button>`;
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 => `
<div class="stella-log-entry ${log.type}">
<span>[${log.timestamp}]</span> ${log.message}
</div>
`).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 = `<label style="display:block; margin-bottom:8px; color:var(--stella-text-dim); font-size:12px; font-weight:500;">${label}</label>`;
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 = `<label style="display:block; margin-bottom:8px; color:var(--stella-text-dim); font-size:12px; font-weight:500;">LocationIQ API Key</label>`;
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 = `
<label style="display:block; margin-bottom:8px; color:var(--stella-text-dim); font-size:12px; font-weight:500;">API Key Status</label>
<div id="api-status-container" style="display:flex; flex-direction:column; gap:6px;">
${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 `
<div style="display:flex; align-items:center; gap:8px; padding:6px 10px; background:var(--carbon-black-3); border-radius:4px; border:1px solid var(--stella-border);">
<div style="width:8px; height:8px; border-radius:50%; background:${dotColor}; box-shadow:0 0 8px ${dotColor};"></div>
<span style="font-size:12px; color:var(--stella-text-dim);">Key ${index + 1}</span>
</div>
`;
}).join('')}
</div>
`;
container.appendChild(statusWrapper);
const noteWrapper = document.createElement('div');
noteWrapper.style.marginBottom = '15px';
noteWrapper.innerHTML = `
<div style="padding:10px; background:rgba(139, 92, 246, 0.1); border:1px solid rgba(139, 92, 246, 0.2); border-radius:6px;">
<div style="display:flex; align-items:start; gap:8px;">
<span class="material-icons" style="font-size:16px; color:var(--stella-accent);">info</span>
<div style="font-size:11px; color:var(--stella-text-dim); line-height:1.5;">
The first API call will trigger a Tampermonkey permission popup. Click "Always allow" to enable location lookups.
</div>
</div>
</div>
`;
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 = `
<div style="display:flex; gap:12px; margin-bottom:12px;">
<div style="flex:1;">
<label style="display:block; margin-bottom:6px; color:var(--stella-text-dim); font-size:12px; font-weight:500;">Onboarding</label>
<button class="stella-btn stella-btn-secondary" id="reset-onboarding-btn" style="width:100%;">
<span class="material-icons" style="font-size:16px; vertical-align:middle; margin-right:8px;">refresh</span>
Reset Onboarding
</button>
</div>
<div style="flex:1;">
<label style="display:block; margin-bottom:6px; color:var(--stella-text-dim); font-size:12px; font-weight:500;">Leaderboard</label>
<button class="stella-btn stella-btn-secondary" id="leaderboard-toggle-btn" style="width:100%; background:${participateState ? 'var(--stella-accent)' : 'transparent'}; color:${participateState ? 'white' : 'var(--stella-text-dim)'}; border:1px solid var(--stella-border);">
${participateState ? 'Enabled' : 'Disabled'}
</button>
</div>
</div>
`;
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 = `
<div class="stella-feature-info">
<div class="stella-feature-dot"></div>
<div class="stella-feature-name">${name}</div>
</div>
<div class="stella-feature-actions">
${hasSubSettings ? '<span class="material-icons stella-settings-btn">settings</span>' : ''}
</div>
`;
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 = '<span class="material-icons" style="font-size:14px;">open_in_new</span>';
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 = `<div style="font-weight:600; margin-bottom:15px; border-bottom:1px solid var(--stella-border); padding-bottom:10px; font-size:14px;">${name} Settings</div>`;
const settings = this.app.settings.get('featureSettings')[key];
if (key === 'locationDisplay') {
const createCheckbox = (label, prop) => `
<div class="stella-checkbox-wrapper" data-prop="${prop}">
<div class="stella-checkbox ${settings[prop] ? 'checked' : ''}"></div>
<span style="font-size:13px; color:var(--stella-text-dim);">${label}</span>
</div>
`;
const createSlider = (label, prop, min, max) => `
<div class="stella-slider-wrapper">
<div class="stella-slider-label">
<span>${label}</span>
<span id="val-${prop}">${settings[prop]}</span>
</div>
<input type="range" min="${min}" max="${max}" value="${settings[prop]}" class="stella-slider" data-prop="${prop}">
</div>
`;
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 += `
<div style="margin-top:10px; padding-top:10px; border-top:1px solid var(--stella-border);">
<button id="reset-loc-pos" class="stella-btn stella-btn-secondary" style="width:100%; font-size:12px;">Reset Position</button>
</div>
`;
} else if (key === 'tts') {
content += `
<div class="stella-slider-wrapper">
<div class="stella-slider-label">
<span>Volume</span>
<span id="val-volume">${Math.round(settings.volume * 100)}%</span>
</div>
<input type="range" min="0" max="1" step="0.1" value="${settings.volume}" class="stella-slider" data-prop="volume">
</div>
`;
} else if (key === 'discordWebhook') {
content += `
<div style="margin-bottom:10px;">
<label style="display:block; font-size:12px; color:var(--stella-text-dim); margin-bottom:5px;">Webhook URL</label>
<input type="text" value="${settings.url || ''}" class="stella-input" data-prop="url" placeholder="https://discord.com/api/webhooks/...">
</div>
`;
} else if (key === 'watermark') {
const createCheckbox = (label, prop) => `
<div class="stella-checkbox-wrapper" data-prop="${prop}">
<div class="stella-checkbox ${settings[prop] !== false ? 'checked' : ''}"></div>
<span style="font-size:13px; color:var(--stella-text-dim);">${label}</span>
</div>
`;
content += createCheckbox('Show Name', 'showName');
content += createCheckbox('Show Username', 'showUsername');
content += createCheckbox('Show Clock', 'showClock');
content += `
<div style="margin-bottom:10px; margin-top:10px;">
<label style="display:block; font-size:12px; color:var(--stella-text-dim); margin-bottom:5px;">Time Format</label>
<select class="stella-input" data-prop="timeFormat">
<option value="12h">12 Hour</option>
<option value="24h">24 Hour</option>
</select>
</div>
`;
content += `
<div style="margin-bottom:10px; margin-top:10px;">
<label style="display:block; font-size:12px; color:var(--stella-text-dim); margin-bottom:5px;">Position</label>
<select class="stella-input" data-prop="position">
<option value="top-left">Top Left</option>
<option value="top-center">Top Center</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-center">Bottom Center</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
`;
} else if (key === 'hotkeyDisplay') {
content += `
<div style="margin-bottom:10px;">
<label style="display:block; font-size:12px; color:var(--stella-text-dim); margin-bottom:5px;">Display Mode</label>
<select class="stella-input" data-prop="mode">
<option value="active">Active Features Only</option>
<option value="bound">All Bound Features</option>
</select>
</div>
`;
const createCheckbox = (label, prop) => `
<div class="stella-checkbox-wrapper" data-prop="${prop}">
<div class="stella-checkbox ${settings[prop] !== false ? 'checked' : ''}"></div>
<span style="font-size:13px; color:var(--stella-text-dim);">${label}</span>
</div>
`;
content += createCheckbox('Show Toggle Key', 'showToggle');
content += createCheckbox('Show Trigger Key', 'showTrigger');
} else if (key === 'toastNotifications') {
content += `
<div style="margin-bottom:10px;">
<label style="display:block; font-size:12px; color:var(--stella-text-dim); margin-bottom:5px;">Position</label>
<select class="stella-input" data-prop="position">
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
`;
}
content += `
<div style="display:flex; justify-content:flex-end; gap:8px; margin-top:15px;">
<button id="popup-close" class="stella-btn stella-btn-secondary">Close</button>
<button id="popup-save" class="stella-btn">Save</button>
</div>
`;
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) => `
<div class="stella-input-wrapper">
<input type="text" id="${id}" value="${val || ''}" class="stella-input" placeholder="Press any key...">
<span class="material-icons stella-input-clear" data-target="${id}">close</span>
</div>
`;
popup.innerHTML = `
<div style="font-weight:600; margin-bottom:15px; border-bottom:1px solid var(--stella-border); padding-bottom:10px; font-size:14px;">${name} Hotkeys</div>
<div style="margin-bottom:12px;">
<label style="display:block; font-size:12px; color:var(--stella-text-dim); margin-bottom:5px;">Toggle Key</label>
${createInput('hk-toggle', hotkeys.toggle)}
</div>
${['openGM', 'openPlonkIT', 'locationDisplay', 'tts', 'discordWebhook'].includes(key) ? `
<div style="margin-bottom:20px;">
<label style="display:block; font-size:12px; color:var(--stella-text-dim); margin-bottom:5px;">Trigger Key</label>
${createInput('hk-trigger', hotkeys.trigger)}
</div>` : ''}
<div style="display:flex; justify-content:flex-end; gap:8px;">
<button id="hk-close" class="stella-btn stella-btn-secondary">Close</button>
<button id="hk-save" class="stella-btn">Save</button>
</div>
`;
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 = `<span class="material-icons" style="font-size:18px;">${type === 'error' ? 'error' : 'info'}</span> ${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 `
<div style="display:flex; align-items:center; gap:8px; padding:6px 10px; background:var(--carbon-black-3); border-radius:4px; border:1px solid var(--stella-border);">
<div style="width:8px; height:8px; border-radius:50%; background:${dotColor}; box-shadow:0 0 8px ${dotColor};"></div>
<span style="font-size:12px; color:var(--stella-text-dim);">Key ${index + 1}</span>
</div>
`;
}).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);
}
}
})();