// ==UserScript==
// @name Lynn
// @namespace http://tampermonkey.net/
// @version 3.3.0
// @description Plonk
// @author luni_fw
// @match https://www.geoguessr.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_addElement
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.addStyle
// @grant GM.addElement
// @connect flagcdn.com
// @connect static-maps.yandex.ru
// @connect fonts.googleapis.com
// @connect i.imgur.com
// @connect minimalistmoon.com
// @connect us1.locationiq.com
// @connect locationiq.com
// @connect discord.com
// @run-at document-start
// @downloadURL https://update.greasyfork.icu/scripts/576586/Lynn.user.js
// @updateURL https://update.greasyfork.icu/scripts/576586/Lynn.meta.js
// ==/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,
},
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: 'Lunar 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('lunar_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('lunar_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 */
--lunar-accent: #8b5cf6;
--lunar-accent-hover: #7c3aed;
--lunar-bg: var(--carbon-black);
--lunar-sidebar: var(--carbon-black-2);
--lunar-item-bg: var(--carbon-black-3);
--lunar-text: #ffffff;
--lunar-text-dim: #a1a1aa;
--lunar-border: #333333;
}
body {
user-select: none;
}
#lunar-menu {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
width: 850px;
height: 600px;
background: var(--lunar-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(--lunar-text);
border: 1px solid var(--lunar-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;
}
#lunar-menu.open {
display: flex;
animation: menuFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
#lunar-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 */
.lunar-header {
height: 70px;
border-bottom: 1px solid var(--lunar-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: var(--lunar-bg);
flex-shrink: 0;
}
.lunar-logo {
display: flex;
align-items: center;
gap: 12px;
}
.lunar-logo img {
width: 32px;
height: 32px;
border-radius: 8px; /* Rounded Corners */
}
.lunar-logo-text {
font-size: 24px;
font-weight: 800;
color: white;
letter-spacing: -0.5px;
animation: lunarPulse 3s infinite alternate;
}
@keyframes lunarPulse {
0% { text-shadow: 0 0 10px rgba(139, 92, 246, 0.2); }
100% { text-shadow: 0 0 20px rgba(139, 92, 246, 0.6); }
}
.lunar-header-actions {
display: flex;
gap: 8px;
}
.lunar-header-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
color: var(--lunar-text-dim);
transition: all 0.2s ease;
}
.lunar-header-btn:hover {
background: rgba(255,255,255,0.05);
color: white;
}
.lunar-header-btn.active {
background: rgba(139, 92, 246, 0.1);
color: var(--lunar-accent);
}
/* Body Layout */
.lunar-body {
display: flex;
flex: 1;
overflow: hidden;
}
.lunar-sidebar {
background: var(--lunar-sidebar);
padding: 20px 10px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--lunar-border);
position: relative;
flex-shrink: 0;
width: 50px;
transition: width 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.lunar-sidebar:hover {
width: 240px;
}
.lunar-sidebar:hover.collapsed {
width: 240px;
}
.lunar-nav {
position: relative;
display: flex;
flex-direction: column;
}
.lunar-nav-highlight {
position: absolute;
left: 0;
width: 100%;
background: var(--lunar-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(--lunar-border);
}
.lunar-nav-highlight::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--lunar-accent);
border-radius: 8px 0 0 8px;
}
.lunar-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(--lunar-text-dim);
font-weight: 500;
font-size: 14px;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
position: relative;
z-index: 2;
}
.lunar-nav-item:hover {
color: var(--lunar-text);
}
.lunar-nav-item.active {
color: white;
}
.lunar-sidebar.collapsed .lunar-nav-text {
opacity: 0;
width: 0;
overflow: hidden;
transition: opacity 0.3s ease, width 0.3s ease;
}
.lunar-sidebar:hover .lunar-nav-text {
opacity: 1;
width: auto;
}
.lunar-sidebar.collapsed .lunar-nav-item {
gap: 0;
}
.lunar-sidebar:hover .lunar-nav-item {
gap: 12px;
}
.lunar-content {
flex: 1;
padding: 32px;
overflow-y: auto;
background: var(--lunar-bg);
}
/* Feature Box Shadcn Style */
.lunar-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(--lunar-border);
position: relative;
}
.lunar-feature:hover {
background: var(--lunar-item-bg);
border-color: rgba(255,255,255,0.1);
}
.lunar-feature.active {
border-color: var(--lunar-accent);
background: rgba(139, 92, 246, 0.05);
}
.lunar-feature-info {
display: flex;
align-items: center;
gap: 12px;
}
.lunar-feature-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--lunar-accent);
opacity: 0;
transition: opacity 0.2s;
box-shadow: 0 0 8px var(--lunar-accent);
}
.lunar-feature.active .lunar-feature-dot {
opacity: 1;
}
.lunar-feature-name {
font-weight: 500;
font-size: 14px;
}
.lunar-settings-btn {
opacity: 0;
transition: opacity 0.2s;
cursor: pointer;
color: var(--lunar-text-dim);
font-size: 18px;
padding: 6px;
border-radius: 4px;
}
.lunar-settings-btn:hover {
background: rgba(255,255,255,0.1);
color: white;
}
.lunar-feature:hover .lunar-settings-btn {
opacity: 1;
}
/* Toast */
#lunar-toast-container {
position: fixed;
z-index: 100000;
display: flex;
flex-direction: column;
gap: 10px;
}
#lunar-toast-container.toast-pos-bottom-right {
bottom: 30px;
right: 30px;
}
#lunar-toast-container.toast-pos-bottom-left {
bottom: 30px;
left: 30px;
}
#lunar-toast-container.toast-pos-top-right {
top: 30px;
right: 30px;
}
#lunar-toast-container.toast-pos-top-left {
top: 30px;
left: 30px;
}
.lunar-toast {
background: var(--carbon-black-2);
border: 1px solid var(--lunar-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 */
#lunar-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(--lunar-accent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 260px;
cursor: move;
display: none;
font-family: 'Inter', sans-serif;
}
#lunar-location-display:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(139, 92, 246, 0.2);
}
.lunar-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;
}
.lunar-loc-row:last-child {
margin-bottom: 0;
}
.lunar-loc-row:hover {
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.2);
}
.lunar-loc-icon {
color: var(--lunar-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;
}
.lunar-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);
}
#lunar-map-image {
width: 100%;
height: 140px;
background-size: cover;
background-position: center;
border-radius: 8px;
margin-top: 12px;
border: 1px solid var(--lunar-border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
opacity: 0.95;
transition: all 0.3s ease;
}
#lunar-map-image:hover {
opacity: 1;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}
/* HUD Elements */
#lunar-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(--lunar-accent);
}
#lunar-watermark.top-left { top: 10px; left: 10px; transform: none; }
#lunar-watermark.top-center { top: 50px; left: 50%; transform: translateX(-50%); }
#lunar-watermark.top-right { top: 10px; right: 10px; left: auto; transform: none; }
#lunar-watermark.bottom-left { bottom: 10px; left: 10px; top: auto; transform: none; }
#lunar-watermark.bottom-center { bottom: 10px; left: 50%; top: auto; transform: translateX(-50%); }
#lunar-watermark.bottom-right { bottom: 10px; right: 10px; top: auto; left: auto; transform: none; }
#lunar-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(--lunar-accent);
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
#lunar-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(--lunar-accent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 180px;
cursor: move;
display: none;
font-family: 'Inter', sans-serif;
}
.lunar-hk-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
font-size: 13px;
color: var(--lunar-text-dim);
}
.lunar-hk-row:last-child { margin-bottom: 0; }
.lunar-hk-active { color: white; font-weight: 500; }
.lunar-hk-keys {
display: flex;
gap: 4px;
}
.lunar-hk-key-badge {
background: rgba(255,255,255,0.1);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-family: monospace;
color: var(--lunar-accent);
border: 1px solid rgba(139, 92, 246, 0.2);
}
/* Popups */
.lunar-popup {
background: var(--lunar-bg) !important;
border: 1px solid var(--lunar-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;
}
.lunar-input-wrapper {
position: relative;
width: 100%;
}
.lunar-input {
width: 100%;
background: var(--carbon-black-2);
border: 1px solid var(--lunar-border);
padding: 8px 12px;
border-radius: 6px;
color: white;
font-family: 'Inter', sans-serif;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.lunar-input:focus {
border-color: var(--lunar-accent);
}
.lunar-input-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
color: var(--lunar-text-dim);
cursor: pointer;
font-size: 14px;
display: none;
}
.lunar-input:not(:placeholder-shown) + .lunar-input-clear {
display: block;
}
.lunar-btn {
background: var(--lunar-accent);
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background 0.2s;
}
.lunar-btn:hover {
background: var(--lunar-accent-hover);
}
.lunar-btn-secondary {
background: transparent;
border: 1px solid var(--lunar-border);
color: var(--lunar-text-dim);
}
.lunar-btn-secondary:hover {
background: rgba(255,255,255,0.05);
color: white;
}
/* Welcome Modal */
#lunar-welcome {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--lunar-bg);
border: 1px solid var(--lunar-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;
}
.lunar-welcome-title {
font-size: 24px;
font-weight: 800;
margin-bottom: 15px;
color: white;
}
.lunar-welcome-text {
color: var(--lunar-text-dim);
font-size: 14px;
line-height: 1.6;
margin-bottom: 25px;
}
.lunar-key {
background: var(--carbon-black-3);
border: 1px solid var(--lunar-border);
padding: 2px 6px;
border-radius: 4px;
color: white;
font-family: monospace;
font-size: 12px;
}
/* Logs */
.lunar-log-entry {
font-family: monospace;
font-size: 12px;
padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
color: var(--lunar-text-dim);
}
.lunar-log-entry span { color: var(--lunar-accent); margin-right: 8px; }
.lunar-log-entry.error { color: #ff5555; }
.lunar-log-entry.error span { color: #ff5555; }
/* Checkbox & Slider */
.lunar-checkbox-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
cursor: pointer;
}
/* Custom Color Picker */
.lunar-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;
}
.lunar-color-picker {
background: var(--carbon-black);
border: 1px solid var(--lunar-border);
border-radius: 12px;
padding: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
width: 320px;
}
.lunar-color-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.lunar-color-picker-title {
color: white;
font-size: 16px;
font-weight: 600;
}
.lunar-color-picker-close {
background: none;
border: none;
color: var(--lunar-text-dim);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.lunar-color-picker-close:hover {
background: var(--carbon-black-3);
color: white;
}
.lunar-color-canvas {
width: 100%;
height: 200px;
border-radius: 8px;
cursor: crosshair;
margin-bottom: 12px;
border: 1px solid var(--lunar-border);
}
.lunar-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(--lunar-border);
}
.lunar-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);
}
.lunar-color-preview {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.lunar-color-preview-box {
flex: 1;
height: 50px;
border-radius: 8px;
border: 1px solid var(--lunar-border);
}
.lunar-color-inputs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.lunar-color-input-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.lunar-color-input-label {
font-size: 10px;
color: var(--lunar-text-dim);
text-transform: uppercase;
font-weight: 600;
}
.lunar-color-input-field {
background: var(--carbon-black-3);
border: 1px solid var(--lunar-border);
border-radius: 6px;
padding: 8px;
color: white;
font-size: 12px;
font-family: monospace;
}
.lunar-color-input-field:focus {
outline: none;
border-color: var(--lunar-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(--lunar-accent);
height: 4px;
border-radius: 2px;
border: none;
}
input[type="range"]::-moz-range-track {
background: var(--lunar-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(--lunar-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(--lunar-accent);
cursor: pointer;
outline: none;
}
.lunar-checkbox {
width: 16px;
height: 16px;
border: 1px solid var(--lunar-border);
border-radius: 4px;
background: var(--carbon-black-2);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.lunar-checkbox.checked {
background: var(--lunar-accent);
border-color: var(--lunar-accent);
}
.lunar-checkbox.checked::after {
content: '✓';
font-size: 12px;
color: white;
}
.lunar-slider-wrapper {
margin-bottom: 15px;
}
.lunar-slider-label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--lunar-text-dim);
margin-bottom: 5px;
}
.lunar-slider {
width: 100%;
-webkit-appearance: none;
height: 4px;
background: var(--carbon-black-3);
border-radius: 2px;
outline: none;
}
.lunar-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--lunar-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 = 'lunar-welcome';
modal.innerHTML = `
Welcome to Lunar V4
Here's how to use the menu:
Press Insert to toggle the menu.
Left Click a feature to toggle it.
Right Click a feature to set hotkeys.
Click the settings icon for advanced settings.
Get Started
`;
document.body.appendChild(modal);
document.getElementById('lunar-welcome-close').addEventListener('click', () => {
modal.remove();
this.showLocationApiPopup();
});
}
}
showLocationApiPopup() {
const modal = document.createElement('div');
modal.id = 'lunar-welcome';
modal.innerHTML = `
Location API Setup
The first API call will trigger a Tampermonkey permission popup if you are not on the latest Tamper Monkey Version.
Click "Always allow" to enable location lookups.
info
This permission allows the script to use the LocationIQ API for reverse geocoding.
Okay
`;
document.body.appendChild(modal);
document.getElementById('lunar-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 = 'lunar-welcome';
modal.innerHTML = `
Discord Webhook Setup
When you press "Okay", a Tampermonkey permission popup will appear (if you are not on the latest Tampermonkey version).
Click "Always allow" to enable Discord webhook functionality.
info
This permission allows the script to send location data to your Discord webhook URL.
Okay
`;
document.body.appendChild(modal);
document.getElementById('lunar-webhook-okay').addEventListener('click', () => {
modal.remove();
callback();
});
}
createOverlay() {
this.menu = document.createElement('div');
this.menu.id = 'lunar-menu';
this.menu.innerHTML = `
`;
document.body.appendChild(this.menu);
this.setupNavigation();
this.renderTab('location');
}
createLocationDisplay() {
this.locationDisplay = document.createElement('div');
this.locationDisplay.id = 'lunar-location-display';
this.locationDisplay.innerHTML = `
Location
open_in_new
public
Country: N/A
map
State: N/A
location_city
City: N/A
`;
document.body.appendChild(this.locationDisplay);
this.makeDraggable(this.locationDisplay, 'locationDisplay');
const popButton = document.getElementById('lunar-loc-popout');
popButton.addEventListener('click', () => {
if (!this.locationDisplayPopped) {
this.popOutLocationDisplay();
popButton.innerHTML = 'call_received ';
popButton.title = 'Pop back in';
} else {
this.popInLocationDisplay();
popButton.innerHTML = 'open_in_new ';
popButton.title = 'Pop out';
}
});
if (this.app.settings.get('features').locationDisplay) {
this.locationDisplay.style.display = 'block';
}
}
createHUD() {
this.watermark = document.createElement('div');
this.watermark.id = 'lunar-watermark';
this.watermark.innerHTML = `
Lunar
person
schedule
`;
document.body.appendChild(this.watermark);
this.updateWatermarkPosition();
this.updateWatermarkContent();
this.startClock();
if (this.app.settings.get('features').watermark) {
this.watermark.style.display = 'block';
}
this.mapTimer = document.createElement('div');
this.mapTimer.id = 'lunar-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 = 'lunar-hotkey-display';
document.body.appendChild(this.hotkeyDisplay);
this.makeDraggable(this.hotkeyDisplay);
this.updateHotkeyDisplay();
if (this.app.settings.get('features').hotkeyDisplay) {
this.hotkeyDisplay.style.display = 'block';
}
}
updateHotkeyDisplay() {
const settings = this.app.settings.get('featureSettings').hotkeyDisplay;
const features = this.app.settings.get('features');
const hotkeys = this.app.settings.get('hotkeys');
let html = '';
let hasContent = false;
const featureNames = {
openGM: 'Google Maps',
openPlonkIT: 'PlonkIT',
locationDisplay: 'Location Info',
tts: 'Text to Speech',
discordWebhook: 'Webhook',
watermark: 'Watermark',
mapTimer: 'Map Timer',
hotkeyDisplay: 'Hotkeys'
};
html += `Hotkeys
`;
for (const [key, name] of Object.entries(featureNames)) {
const isActive = features[key];
const hk = hotkeys[key];
const hasHotkey = hk && (hk.trigger || hk.toggle);
if (settings.mode === 'active' && !isActive) continue;
if (settings.mode === 'bound' && !hasHotkey) continue;
hasContent = true;
html += `
${name}
`;
if (hk) {
if (settings.showToggle && hk.toggle) {
html += `${hk.toggle} `;
}
if (settings.showTrigger && hk.trigger) {
html += `${hk.trigger} `;
}
}
html += `
`;
}
if (!hasContent) html = 'No active features
';
this.hotkeyDisplay.innerHTML = html;
}
updateLocationDisplay(data) {
if (!data) return;
const settings = this.app.settings.get('featureSettings').locationDisplay;
this.locationDisplayData = data;
document.getElementById('lunar-loc-country').textContent = data.country || 'N/A';
document.getElementById('lunar-loc-state').textContent = data.state || 'N/A';
document.getElementById('lunar-loc-city').textContent = data.city || 'N/A';
document.getElementById('lunar-row-country').style.display = settings.showCountry ? 'flex' : 'none';
document.getElementById('lunar-row-state').style.display = settings.showState ? 'flex' : 'none';
document.getElementById('lunar-row-city').style.display = settings.showCity ? 'flex' : 'none';
const flagImg = document.getElementById('lunar-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('lunar-map-image').style.backgroundImage = `url("${mapUrl}")`;
this.syncLocationPopout(data, mapUrl, settings);
}
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('lunar-loc-popout');
if (btn) {
btn.textContent = '↗';
btn.title = 'Pop out';
}
}
}
popOutLocationDisplay() {
if (this.locationDisplayPopped) return;
this.locationDisplayWindow = window.open('', 'lunarLocationPopout', 'width=360,height=260');
if (!this.locationDisplayWindow) return;
this.locationDisplayPopped = true;
this.locationDisplay.style.display = 'none';
const css = `
body { margin:0; background:rgba(12,12,18,0.96); color:#fff; font-family: Inter, system-ui, sans-serif; }
.card { padding:12px; background:rgba(24,24,32,0.85); border:1px solid rgba(255,255,255,0.08); border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,0.45); }
.header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; font-weight:600; }
.row { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.flag { width:24px; height:18px; border-radius:3px; object-fit:cover; }
.map { width:300px; height:150px; background-size:cover; background-position:center; border-radius:8px; border:1px solid rgba(255,255,255,0.1); }
.label { color:#c9c9d1; font-size:12px; }
`;
const doc = this.locationDisplayWindow.document;
doc.open();
doc.write(`Location
public Country: N/A
map State: N/A
location_city City: N/A
`);
doc.close();
const popInBtn = doc.getElementById('pop-in');
popInBtn.addEventListener('click', () => this.popInLocationDisplay());
this.locationDisplayWindow.addEventListener('beforeunload', () => {
this.locationDisplayWindow = null;
if (this.locationDisplayPopped) {
this.popInLocationDisplay(true);
}
});
const btn = document.getElementById('lunar-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('lunar-loc-popout');
if (btn) {
btn.innerHTML = 'open_in_new ';
btn.title = 'Pop out';
}
}
}
makeDraggable(element, saveKey = null) {
let isDragging = false;
let currentX = 0, currentY = 0, initialX = 0, initialY = 0;
const app = this.app;
if (saveKey) {
const positions = app.settings.get('elementPositions');
if (positions && positions[saveKey]) {
element.style.left = positions[saveKey].left;
element.style.top = positions[saveKey].top;
}
}
element.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(e.target.tagName)) return;
e.preventDefault();
isDragging = true;
initialX = e.clientX;
initialY = e.clientY;
const rect = element.getBoundingClientRect();
currentX = rect.left;
currentY = rect.top;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
if (!isDragging) return;
e = e || window.event;
e.preventDefault();
const deltaX = e.clientX - initialX;
const deltaY = e.clientY - initialY;
element.style.left = (currentX + deltaX) + "px";
element.style.top = (currentY + deltaY) + "px";
}
function closeDragElement() {
isDragging = false;
document.onmouseup = null;
document.onmousemove = null;
if (saveKey) {
const positions = app.settings.get('elementPositions') || {};
positions[saveKey] = {
left: element.style.left,
top: element.style.top
};
app.settings.set('elementPositions', positions);
}
}
}
setupNavigation() {
const navItems = this.menu.querySelectorAll('.lunar-nav-item');
const headerBtns = this.menu.querySelectorAll('.lunar-header-btn');
const highlight = this.menu.querySelector('.lunar-nav-highlight');
const updateHighlight = (item) => {
if (highlight && item.classList.contains('lunar-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('.lunar-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('lunar-nav-item')) {
updateHighlight(item);
} else {
if (highlight) highlight.style.opacity = '0';
}
this.renderTab(item.dataset.tab);
});
});
}
renderTab(tabName) {
const content = this.menu.querySelector('#lunar-tab-content');
content.innerHTML = '';
switch (tabName) {
case 'location':
this.renderLocationTab(content);
break;
case 'hud':
this.renderHUDTab(content);
break;
case 'autoplay':
content.innerHTML = `
warning
Maintenance Mode
This feature is currently being updated.
`;
break;
case 'settings':
this.renderSettingsTab(content);
break;
case 'logs':
this.renderLogsTab(content);
break;
case 'customize':
this.renderCustomizeTab(content);
break;
}
}
renderLocationTab(container) {
this.createFeature(container, 'Open Google Maps ⚠️', 'openGM');
this.createFeature(container, 'Open PlonkIT', 'openPlonkIT');
this.createFeature(container, 'Location Display', 'locationDisplay', true);
this.createFeature(container, 'Text to Speech', 'tts', true);
this.createFeature(container, 'Discord Webhook', 'discordWebhook', true);
}
renderHUDTab(container) {
this.createFeature(container, 'Watermark', 'watermark', true);
this.createFeature(container, 'Classic Map Timer', 'mapTimer');
this.createFeature(container, 'Hotkey Display', 'hotkeyDisplay', true);
this.createFeature(container, 'Toast Notifications', 'toastNotifications', true);
}
renderCustomizeTab(container) {
const defaultSection = document.createElement('div');
defaultSection.style.marginBottom = '24px';
defaultSection.innerHTML = 'Default Themes ';
container.appendChild(defaultSection);
const defaultThemes = this.app.settings.get('defaultThemes');
for (const [key, theme] of Object.entries(defaultThemes)) {
this.createThemeItem(defaultSection, theme.name, key, false);
}
const customSection = document.createElement('div');
customSection.style.marginBottom = '24px';
customSection.innerHTML = `
Custom Themes
add
Create Theme
`;
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(--lunar-text-dim); font-size:13px; border:1px dashed var(--lunar-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 = `lunar-feature ${isActive ? 'active' : ''}`;
el.innerHTML = `
${isCustom ? 'delete ' : ''}
`;
el.addEventListener('click', (e) => {
if (e.target.dataset.action === 'delete') {
this.deleteTheme(key);
return;
}
this.loadTheme(key);
});
el.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.openThemeCustomization(key, isCustom);
});
container.appendChild(el);
}
createNewTheme() {
const themeName = prompt('Enter theme name:');
if (!themeName) return;
const themeKey = 'custom_' + Date.now();
const customThemes = this.app.settings.get('customThemes');
const currentThemeKey = this.app.settings.get('currentTheme');
const currentTheme = this.app.settings.get('defaultThemes')[currentThemeKey] ||
this.app.settings.get('customThemes')[currentThemeKey];
customThemes[themeKey] = {
name: themeName,
colors: { ...currentTheme.colors }
};
this.app.settings.set('customThemes', customThemes);
this.showToast(`Theme "${themeName}" created`);
this.renderTab('customize');
}
deleteTheme(key) {
const customThemes = this.app.settings.get('customThemes');
const themeName = customThemes[key].name;
if (!confirm(`Delete theme "${themeName}"?`)) return;
delete customThemes[key];
this.app.settings.set('customThemes', customThemes);
if (this.app.settings.get('currentTheme') === key) {
this.loadTheme('default');
}
this.showToast(`Theme "${themeName}" deleted`);
this.renderTab('customize');
}
loadTheme(key) {
this.app.settings.set('currentTheme', key);
this.applyTheme(key);
this.showToast('Theme loaded');
this.renderTab('customize');
}
applyTheme(key) {
const theme = this.app.settings.get('defaultThemes')[key] ||
this.app.settings.get('customThemes')[key];
if (!theme) return;
const root = document.documentElement;
for (const [colorKey, colorData] of Object.entries(theme.colors)) {
const cssVar = this.colorKeyToCssVar(colorKey);
if (typeof colorData === 'string') {
root.style.setProperty(cssVar, colorData);
} else {
const rgba = this.hexToRgba(colorData.color, colorData.alpha);
root.style.setProperty(cssVar, rgba);
this.applyColorEffect(cssVar, colorData);
}
}
}
colorKeyToCssVar(key) {
const map = {
carbonBlack: '--carbon-black',
carbonBlack2: '--carbon-black-2',
carbonBlack3: '--carbon-black-3',
accent: '--lunar-accent',
accentHover: '--lunar-accent-hover',
text: '--lunar-text',
textDim: '--lunar-text-dim',
border: '--lunar-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('#lunar-tab-content');
const sidebar = this.menu.querySelector('.lunar-sidebar');
sidebar.style.display = 'none';
content.innerHTML = `
arrow_back
Back
Customize: ${theme.name}
save
Save
${this.createAdvancedColorInput('Background', 'carbonBlack', theme.colors.carbonBlack)}
${this.createAdvancedColorInput('Background 2', 'carbonBlack2', theme.colors.carbonBlack2)}
${this.createAdvancedColorInput('Background 3', 'carbonBlack3', theme.colors.carbonBlack3)}
${this.createAdvancedColorInput('Accent', 'accent', theme.colors.accent)}
${this.createAdvancedColorInput('Accent Hover', 'accentHover', theme.colors.accentHover)}
${this.createAdvancedColorInput('Text', 'text', theme.colors.text)}
${this.createAdvancedColorInput('Text Dim', 'textDim', theme.colors.textDim)}
${this.createAdvancedColorInput('Border', 'border', theme.colors.border)}
`;
document.getElementById('theme-back-btn').addEventListener('click', () => {
this.saveThemeColors(key);
sidebar.style.display = '';
this.renderTab('customize');
});
document.getElementById('theme-save-btn').addEventListener('click', () => {
this.saveThemeColors(key);
sidebar.style.display = '';
this.renderTab('customize');
});
content.querySelectorAll('input[type="color"], input[type="range"]').forEach(input => {
input.addEventListener('input', () => {
this.updateLivePreview(content);
});
});
content.querySelectorAll('.lunar-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('.lunar-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(`.lunar-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 `
`;
}
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('#lunar-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 = 'lunar-color-picker-overlay';
const picker = document.createElement('div');
picker.className = 'lunar-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 = `
`;
overlay.appendChild(picker);
document.body.appendChild(overlay);
const canvas = picker.querySelector('.lunar-color-canvas');
const ctx = canvas.getContext('2d');
const hueSlider = picker.querySelector('.lunar-hue-slider');
const hueThumb = picker.querySelector('.lunar-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('.lunar-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('--lunar-accent', colors.accent);
root.style.setProperty('--lunar-accent-hover', colors.accentHover);
root.style.setProperty('--lunar-text', colors.text);
root.style.setProperty('--lunar-text-dim', colors.textDim);
root.style.setProperty('--lunar-border', colors.border);
}
renderLogsTab(container) {
const controls = document.createElement('div');
controls.style.marginBottom = '15px';
controls.innerHTML = `Clear Logs `;
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(--lunar-border)';
container.appendChild(logContainer);
const renderLogs = () => {
logContainer.innerHTML = Utils.logs.map(log => `
[${log.timestamp}] ${log.message}
`).join('');
};
renderLogs();
Utils.addLogListener(() => renderLogs());
controls.querySelector('#clear-logs').addEventListener('click', () => {
Utils.logs = [];
renderLogs();
});
}
renderSettingsTab(container) {
const createInput = (label, value, onChange, isHotkey = false) => {
const wrapper = document.createElement('div');
wrapper.style.marginBottom = '15px';
wrapper.innerHTML = `${label} `;
const inputWrapper = document.createElement('div');
inputWrapper.className = 'lunar-input-wrapper';
const input = document.createElement('input');
input.type = 'text';
input.value = value;
input.className = 'lunar-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 = `LocationIQ API Key `;
const select = document.createElement('select');
select.className = 'lunar-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 = `
API Key Status
${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 `
`;
}).join('')}
`;
container.appendChild(statusWrapper);
const noteWrapper = document.createElement('div');
noteWrapper.style.marginBottom = '15px';
noteWrapper.innerHTML = `
info
The first API call will trigger a Tampermonkey permission popup. Click "Always allow" to enable location lookups.
`;
container.appendChild(noteWrapper);
const resetSection = document.createElement('div');
resetSection.style.cssText = 'margin-top:30px; padding-top:20px; border-top:1px solid var(--lunar-border);';
const participateState = this.app.settings.get('participateInLeaderboard');
resetSection.innerHTML = `
Onboarding
refresh
Reset Onboarding
Leaderboard
${participateState ? 'Enabled' : 'Disabled'}
`;
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(--lunar-accent)' : 'transparent';
btn.style.color = newState ? 'white' : 'var(--lunar-text-dim)';
});
}
createFeature(container, name, key, hasSubSettings = false) {
const el = document.createElement('div');
el.className = `lunar-feature ${this.app.settings.get('features')[key] ? 'active' : ''}`;
el.innerHTML = `
${hasSubSettings ? 'settings ' : ''}
`;
el.addEventListener('click', (e) => {
if (e.target.classList.contains('lunar-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('.lunar-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('lunar-loc-popout');
if (btn) {
btn.innerHTML = 'open_in_new ';
btn.title = 'Pop out';
}
}
} else if (key === 'watermark') {
this.watermark.style.display = newState ? 'block' : 'none';
} else if (key === 'mapTimer') {
this.mapTimer.style.display = newState ? 'block' : 'none';
} else if (key === 'hotkeyDisplay') {
this.hotkeyDisplay.style.display = newState ? 'block' : 'none';
}
this.updateHotkeyDisplay();
});
if (hasSubSettings) {
el.querySelector('.lunar-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('lunar-popup');
if (existing) existing.remove();
const popup = document.createElement('div');
popup.id = 'lunar-popup';
popup.className = 'lunar-popup';
popup.style.left = `${x}px`;
popup.style.top = `${y}px`;
let content = `${name} Settings
`;
const settings = this.app.settings.get('featureSettings')[key];
if (key === 'locationDisplay') {
const createCheckbox = (label, prop) => `
`;
const createSlider = (label, prop, min, max) => `
`;
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 += `
Reset Position
`;
} else if (key === 'tts') {
content += `
`;
} else if (key === 'discordWebhook') {
content += `
Webhook URL
`;
} else if (key === 'watermark') {
const createCheckbox = (label, prop) => `
`;
content += createCheckbox('Show Name', 'showName');
content += createCheckbox('Show Username', 'showUsername');
content += createCheckbox('Show Clock', 'showClock');
content += `
Time Format
12 Hour
24 Hour
`;
content += `
Position
Top Left
Top Center
Top Right
Bottom Left
Bottom Center
Bottom Right
`;
} else if (key === 'hotkeyDisplay') {
content += `
Display Mode
Active Features Only
All Bound Features
`;
const createCheckbox = (label, prop) => `
`;
content += createCheckbox('Show Toggle Key', 'showToggle');
content += createCheckbox('Show Trigger Key', 'showTrigger');
} else if (key === 'toastNotifications') {
content += `
Position
Top Left
Top Right
Bottom Left
Bottom Right
`;
}
content += `
`;
popup.innerHTML = content;
document.body.appendChild(popup);
if (key === 'locationDisplay') {
popup.querySelectorAll('.lunar-checkbox-wrapper').forEach(el => {
el.addEventListener('click', () => {
const checkbox = el.querySelector('.lunar-checkbox');
checkbox.classList.toggle('checked');
});
});
popup.querySelectorAll('.lunar-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('.lunar-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('.lunar-checkbox-wrapper').forEach(el => {
el.addEventListener('click', () => {
const prop = el.dataset.prop;
const checked = el.querySelector('.lunar-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('.lunar-checkbox-wrapper').forEach(el => {
el.addEventListener('click', () => {
const prop = el.dataset.prop;
const checked = el.querySelector('.lunar-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('.lunar-checkbox-wrapper').forEach(el => {
const prop = el.dataset.prop;
const checked = el.querySelector('.lunar-checkbox').classList.contains('checked');
this.app.settings.setFeatureSetting(key, prop, checked);
});
popup.querySelectorAll('.lunar-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('.lunar-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('.lunar-checkbox-wrapper').forEach(el => {
const prop = el.dataset.prop;
const checked = el.querySelector('.lunar-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('.lunar-checkbox-wrapper').forEach(el => {
const prop = el.dataset.prop;
const checked = el.querySelector('.lunar-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('lunar-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('lunar-popup');
if (existing) existing.remove();
const popup = document.createElement('div');
popup.id = 'lunar-popup';
popup.className = 'lunar-popup';
popup.style.left = `${x}px`;
popup.style.top = `${y}px`;
const hotkeys = this.app.settings.get('hotkeys')[key] || {};
const createInput = (id, val) => `
close
`;
popup.innerHTML = `
${name} Hotkeys
Toggle Key
${createInput('hk-toggle', hotkeys.toggle)}
${['openGM', 'openPlonkIT', 'locationDisplay', 'tts', 'discordWebhook'].includes(key) ? `
Trigger Key
${createInput('hk-trigger', hotkeys.trigger)}
` : ''}
Close
Save
`;
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('.lunar-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('lunar-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'lunar-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 = 'lunar-toast';
toast.innerHTML = `${type === 'error' ? 'error' : 'info'} ${message}`;
container.appendChild(toast);
toast.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
updateWatermarkPosition() {
const pos = this.app.settings.get('featureSettings').watermark.position || 'top-center';
this.watermark.className = pos;
}
updateWatermarkContent() {
const settings = this.app.settings.get('featureSettings').watermark;
const logoGroup = document.getElementById('lunar-watermark-logo-group');
const userGroup = document.getElementById('lunar-watermark-user-group');
const clockGroup = document.getElementById('lunar-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('lunar-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('lunar-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 `
`;
}).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 LunarApp {
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('.lunar-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);
}
}
/* =========================
LYNN FINAL MOBILE PATCH V3
tempel tepat DI ATAS: new LunarApp();
hapus patch lama sebelumnya
========================= */
(function LynnFinalMobilePatchV3() {
'use strict';
if (window.__LYNN_FINAL_MOBILE_PATCH_V3__) return;
window.__LYNN_FINAL_MOBILE_PATCH_V3__ = true;
const CYAN_THEME_KEY = 'lynn_cyan_mobile_v3';
const DEFAULT_MENU_W = 850;
const DEFAULT_MENU_H = 600;
const STORAGE = {
locMin: 'lynn_loc_min_v3',
locScale: 'lynn_loc_scale_v3',
locLeft: 'lynn_loc_left_v3',
locTop: 'lynn_loc_top_v3',
locZoomIndex: 'lynn_loc_zoom_index_v3',
menuLeft: 'lynn_menu_left_v3',
menuTop: 'lynn_menu_top_v3',
menuWidth: 'lynn_menu_width_v3',
menuHeight: 'lynn_menu_height_v3',
panelLeft: 'lynn_panel_left_v3',
panelTop: 'lynn_panel_top_v3'
};
/* level zoom loc:
1 = jauh
5 = paling dekat
*/
const MAP_ZOOMS = [2, 3, 5, 7, 10];
const WELCOME_HTML = `
Welcome to Lynn
cara pakai menunya:
tekan Insert buat buka / tutup menu
Left Click buat on / off fitur
Right Click buat atur hotkey
tap icon settings buat setting lanjutan
lanjut
`;
const API_HTML = `
Izin Location
saat pertama kali dipakai, biasanya tampermonkey bakal minta izin.
pilih "Always allow" biar fitur location bisa jalan normal.
info
izin ini dipakai buat reverse geocoding, jadi country / state / city bisa muncul.
oke
`;
function clamp(num, min, max) {
return Math.max(min, Math.min(max, num));
}
function getSavedZoomIndex() {
const idx = parseInt(localStorage.getItem(STORAGE.locZoomIndex) || '0', 10);
if (Number.isNaN(idx)) return 0;
return clamp(idx, 0, MAP_ZOOMS.length - 1);
}
function getMenuSize() {
let width = parseInt(localStorage.getItem(STORAGE.menuWidth) || String(DEFAULT_MENU_W), 10);
let height = parseInt(localStorage.getItem(STORAGE.menuHeight) || String(DEFAULT_MENU_H), 10);
if (Number.isNaN(width)) width = DEFAULT_MENU_W;
if (Number.isNaN(height)) height = DEFAULT_MENU_H;
return {
width: clamp(width, 680, 1100),
height: clamp(height, 500, 820)
};
}
/* =========================
patch prototype
========================= */
const oldInit = LunarApp.prototype.init;
LunarApp.prototype.init = function patchedInit() {
oldInit.apply(this, arguments);
setTimeout(() => {
installLynnFinalPatch(this);
}, 800);
};
const oldUpdateLocationDisplay = UI.prototype.updateLocationDisplay;
UI.prototype.updateLocationDisplay = function patchedUpdateLocationDisplay(data) {
oldUpdateLocationDisplay.apply(this, arguments);
setTimeout(() => {
patchLocationPanel(this.app);
updateLocMap(this.app);
syncBranding(this.app);
}, 80);
};
const oldUpdateWatermarkContent = UI.prototype.updateWatermarkContent;
UI.prototype.updateWatermarkContent = function patchedUpdateWatermarkContent() {
oldUpdateWatermarkContent.apply(this, arguments);
const nameEl = document.getElementById('lunar-watermark-name');
if (nameEl) nameEl.textContent = 'Lynn';
};
UI.prototype.checkFirstRun = function patchedCheckFirstRun() {
if (!this.app.settings.get('firstRun')) return;
const old = document.getElementById('lunar-welcome');
if (old) old.remove();
const modal = document.createElement('div');
modal.id = 'lunar-welcome';
modal.innerHTML = WELCOME_HTML;
document.body.appendChild(modal);
const closeBtn = document.getElementById('lynn-welcome-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
modal.remove();
this.showLocationApiPopup();
});
}
};
UI.prototype.showLocationApiPopup = function patchedShowLocationApiPopup() {
const old = document.getElementById('lunar-welcome');
if (old) old.remove();
const modal = document.createElement('div');
modal.id = 'lunar-welcome';
modal.innerHTML = API_HTML;
document.body.appendChild(modal);
const closeBtn = document.getElementById('lynn-api-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
modal.remove();
this.app.settings.set('firstRun', false);
this.app.network.getLocationDetails(40.7128, -74.0060);
this.toggleMenu();
});
}
};
/* =========================
main install
========================= */
function installLynnFinalPatch(app) {
applyThemeSettings(app);
addPatchStyle();
createSimple3Buttons(app);
enableSimple3PanelDrag();
enableMenuDrag(app);
createMenuResizeControls(app);
applySavedMenuSize(app);
patchLocationPanel(app);
updateLocMap(app);
syncBranding(app);
restoreWatermark(app);
setInterval(() => {
syncBranding(app);
patchLocationPanel(app);
updateLocMap(app);
createMenuResizeControls(app);
applySavedMenuSize(app);
}, 1000);
}
function applyThemeSettings(app) {
try {
app.settings.data.customThemes = app.settings.data.customThemes || {};
app.settings.data.customThemes[CYAN_THEME_KEY] = {
name: 'Lynn Cyan',
colors: {
carbonBlack: { color: '#061417', alpha: 1, effect: 'none' },
carbonBlack2: { color: '#082026', alpha: 1, effect: 'none' },
carbonBlack3: { color: '#0b2b33', alpha: 1, effect: 'none' },
accent: { color: '#00e5ff', alpha: 1, effect: 'none' },
accentHover: { color: '#00bcd4', alpha: 1, effect: 'none' },
text: { color: '#eaffff', alpha: 1, effect: 'none' },
textDim: { color: '#8ccbd3', alpha: 1, effect: 'none' },
border: { color: '#115967', alpha: 1, effect: 'none' }
}
};
app.settings.data.currentTheme = CYAN_THEME_KEY;
app.settings.data.features.watermark = true;
app.settings.save();
if (app.ui && app.ui.applyTheme) {
app.ui.applyTheme(CYAN_THEME_KEY);
}
if (app.ui && app.ui.watermark) {
app.ui.watermark.style.display = 'block';
}
} catch (e) {
console.log('[Lynn Patch] applyThemeSettings error:', e);
}
}
function restoreWatermark(app) {
try {
if (!app || !app.ui || !app.ui.watermark) return;
const wm = app.ui.watermark;
wm.style.removeProperty('display');
wm.style.removeProperty('opacity');
wm.style.removeProperty('visibility');
if (app.settings.get('features').watermark) {
wm.style.display = 'block';
}
const nameEl = document.getElementById('lunar-watermark-name');
if (nameEl) nameEl.textContent = 'Lynn';
} catch (e) {}
}
function syncBranding(app) {
const menuLogo = document.querySelector('.lunar-logo-text');
if (menuLogo) menuLogo.textContent = 'Lynn';
const wmName = document.getElementById('lunar-watermark-name');
if (wmName) wmName.textContent = 'Lynn';
const welcomeTitle = document.querySelector('#lunar-welcome .lunar-welcome-title');
if (welcomeTitle && /Lunar/i.test(welcomeTitle.textContent)) {
welcomeTitle.textContent = welcomeTitle.textContent.replace(/Lunar/gi, 'Lynn');
}
}
/* =========================
styles
========================= */
function addPatchStyle() {
if (document.getElementById('lynn-final-mobile-style-v3')) return;
const css = `
:root {
--carbon-black: #061417 !important;
--carbon-black-2: #082026 !important;
--carbon-black-3: #0b2b33 !important;
--lunar-bg: #061417 !important;
--lunar-sidebar: #082026 !important;
--lunar-item-bg: #0b2b33 !important;
--lunar-accent: #00e5ff !important;
--lunar-accent-hover: #00bcd4 !important;
--lunar-text: #eaffff !important;
--lunar-text-dim: #8ccbd3 !important;
--lunar-border: #115967 !important;
}
#lunar-menu {
border-color: #00e5ff55 !important;
box-shadow: 0 20px 50px rgba(0, 229, 255, 0.12), 0 0 0 1px rgba(0, 229, 255, 0.18) !important;
}
.lunar-logo-text {
color: #eaffff !important;
text-shadow: 0 0 16px rgba(0, 229, 255, 0.65) !important;
}
.lunar-feature.active {
border-color: #00e5ff !important;
background: rgba(0, 229, 255, 0.08) !important;
}
.lunar-feature-dot {
background: #00e5ff !important;
box-shadow: 0 0 10px #00e5ff !important;
}
.lunar-nav-highlight::before {
background: #00e5ff !important;
}
.lunar-header-btn.active,
.lunar-header-btn:hover {
color: #00e5ff !important;
background: rgba(0, 229, 255, 0.10) !important;
}
.lunar-btn {
background: #00bcd4 !important;
color: #001014 !important;
font-weight: 700 !important;
}
.lunar-btn:hover {
background: #00e5ff !important;
}
.lunar-input:focus {
border-color: #00e5ff !important;
}
.lunar-loc-icon,
.lunar-hk-key-badge {
color: #00e5ff !important;
}
#lunar-location-display,
#lunar-hotkey-display,
#lunar-map-timer,
.lunar-popup,
.lunar-toast {
border-color: #00e5ff55 !important;
border-bottom-color: #00e5ff !important;
box-shadow: 0 8px 24px rgba(0, 229, 255, 0.12) !important;
}
.lunar-loc-row {
background: rgba(0, 229, 255, 0.06) !important;
border-color: rgba(0, 229, 255, 0.15) !important;
}
.lunar-loc-icon {
background: rgba(0, 229, 255, 0.15) !important;
}
#lunar-watermark {
background: rgba(26, 26, 26, 0.6) !important;
backdrop-filter: blur(4px) !important;
color: rgba(255,255,255,0.8) !important;
border: 1px solid rgba(255,255,255,0.1) !important;
border-bottom: 2px solid #8b5cf6 !important;
box-shadow: none !important;
}
#lunar-watermark-user-group {
color: #8b5cf6 !important;
}
/* =========================
3 tombol samping modern
size tetap sama
========================= */
#lynn-simple3-panel {
position: fixed;
right: 10px;
top: 42%;
transform: translateY(-50%);
z-index: 2147483647;
display: flex;
flex-direction: column;
gap: 7px;
padding: 8px;
background: linear-gradient(180deg, rgba(8, 24, 31, 0.88), rgba(5, 16, 21, 0.88));
border: 1px solid rgba(0, 229, 255, 0.20);
border-radius: 16px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow:
0 14px 35px rgba(0, 0, 0, 0.40),
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 0 0 1px rgba(0, 229, 255, 0.06);
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
#lynn-simple3-panel::before {
content: '';
position: absolute;
inset: 0;
border-radius: 16px;
pointer-events: none;
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0));
}
.lynn-side-btn {
position: relative;
width: 52px;
height: 32px;
border: 1px solid rgba(0, 229, 255, 0.22);
border-radius: 10px;
background:
linear-gradient(180deg, rgba(15, 46, 57, 0.96), rgba(9, 27, 35, 0.96));
color: #eaffff;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.25px;
text-transform: lowercase;
padding: 0;
overflow: hidden;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.06),
0 6px 14px rgba(0,0,0,0.26);
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease, background .15s ease;
touch-action: manipulation;
}
.lynn-side-btn::before {
content: '';
position: absolute;
left: 6px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
border-radius: 999px;
background: linear-gradient(180deg, #00e5ff, #00a9bd);
box-shadow: 0 0 8px rgba(0, 229, 255, 0.50);
}
.lynn-side-btn span {
position: relative;
display: inline-block;
transform: translateX(4px);
}
.lynn-side-btn:hover {
border-color: rgba(0, 229, 255, 0.45);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.08),
0 10px 20px rgba(0,0,0,0.32),
0 0 16px rgba(0,229,255,0.14);
}
.lynn-side-btn:active {
transform: scale(0.95);
background: linear-gradient(180deg, rgba(20, 60, 73, 0.96), rgba(11, 34, 43, 0.96));
}
/* =========================
tombol resize menu
========================= */
#lynn-menu-size-controls {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
margin-right: 4px !important;
}
.lynn-menu-size-btn {
width: 34px !important;
height: 34px !important;
border: 1px solid rgba(0, 229, 255, 0.22) !important;
border-radius: 9px !important;
background: rgba(0, 229, 255, 0.08) !important;
color: #dffcff !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04) !important;
transition: transform .15s ease, background .15s ease, border-color .15s ease !important;
padding: 0 !important;
}
.lynn-menu-size-btn:hover {
background: rgba(0, 229, 255, 0.14) !important;
border-color: rgba(0, 229, 255, 0.42) !important;
}
.lynn-menu-size-btn:active {
transform: scale(0.95) !important;
}
.lynn-menu-size-btn .material-icons {
font-size: 18px !important;
line-height: 1 !important;
}
#lunar-menu {
max-width: 96vw !important;
max-height: calc(100vh - 90px) !important;
}
#lunar-menu .lunar-header {
touch-action: none !important;
}
#lynn-loc-controls {
display: flex !important;
align-items: center !important;
gap: 5px !important;
}
#lynn-loc-controls button {
width: 31px !important;
height: 28px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
background: rgba(0, 229, 255, 0.10) !important;
border: 1px solid rgba(0, 229, 255, 0.35) !important;
color: #00e5ff !important;
border-radius: 8px !important;
cursor: pointer !important;
font-size: 15px !important;
font-weight: 900 !important;
line-height: 1 !important;
padding: 0 !important;
}
#lynn-loc-controls button:active {
background: #00e5ff !important;
color: #001014 !important;
transform: scale(.94);
}
#lunar-map-image {
position: relative !important;
cursor: pointer !important;
overflow: hidden !important;
height: 170px !important;
background-size: cover !important;
background-position: center !important;
}
/* NOTE:
bulat merah sengaja dihapus
karena bentrok sama pin bawaan map
*/
#lynn-loc-zoom-row {
margin-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
#lynn-loc-zoom-row button {
flex: 0 0 44px;
height: 30px;
border: none;
border-radius: 8px;
background: rgba(0, 229, 255, 0.10);
border: 1px solid rgba(0, 229, 255, 0.35);
color: #00e5ff;
font-size: 16px;
font-weight: 900;
}
#lynn-loc-zoom-row button:active {
background: #00e5ff;
color: #001014;
transform: scale(.94);
}
#lynn-loc-zoom-label {
flex: 1;
text-align: center;
padding: 6px 8px;
border-radius: 8px;
font-size: 11px;
font-weight: 800;
color: #8ccbd3;
background: rgba(0, 229, 255, 0.06);
border: 1px solid rgba(0, 229, 255, 0.15);
}
#lynn-location-mini {
display: none !important;
}
#lunar-location-display.lynn-loc-minimized {
width: 150px !important;
min-width: 150px !important;
max-width: 150px !important;
height: 42px !important;
min-height: 42px !important;
max-height: 42px !important;
padding: 0 !important;
overflow: hidden !important;
transform: none !important;
border-radius: 12px !important;
background: rgba(6, 20, 23, 0.92) !important;
border: 1px solid rgba(0, 229, 255, 0.45) !important;
border-bottom: 2px solid #00e5ff !important;
box-shadow: 0 7px 18px rgba(0, 229, 255, .14) !important;
cursor: move !important;
}
#lunar-location-display.lynn-loc-minimized > * {
display: none !important;
}
#lunar-location-display.lynn-loc-minimized > #lynn-location-mini {
display: flex !important;
}
#lynn-location-mini {
width: 100% !important;
height: 100% !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 8px !important;
box-sizing: border-box !important;
}
#lynn-location-mini-title {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
gap: 7px !important;
line-height: 1 !important;
}
#lynn-location-mini-title strong {
font-size: 12px !important;
color: #eaffff !important;
letter-spacing: .2px !important;
white-space: nowrap !important;
}
#lynn-location-mini-title strong::before {
content: '';
width: 8px;
height: 8px;
border-radius: 999px;
background: #00e5ff;
box-shadow: 0 0 8px #00e5ff;
display: inline-block;
margin-right: 6px;
}
#lynn-location-mini-open {
width: 32px !important;
height: 30px !important;
border: none !important;
border-radius: 8px !important;
background: #00bcd4 !important;
color: #001014 !important;
font-size: 18px !important;
font-weight: 900 !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
}
#lynn-location-mini-open:active {
background: #00e5ff !important;
transform: scale(.94);
}
`;
GM_addStyle(css);
}
/* =========================
3 tombol samping
========================= */
function createSimple3Buttons(app) {
if (document.getElementById('lynn-simple3-panel')) return;
const panel = document.createElement('div');
panel.id = 'lynn-simple3-panel';
panel.innerHTML = `
loc
gm
`;
const savedLeft = localStorage.getItem(STORAGE.panelLeft);
const savedTop = localStorage.getItem(STORAGE.panelTop);
if (savedLeft && savedTop) {
panel.style.left = savedLeft;
panel.style.top = savedTop;
panel.style.right = 'auto';
panel.style.transform = 'none';
}
document.body.appendChild(panel);
document.getElementById('lynn-btn-menu').addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
app.ui.toggleMenu();
});
document.getElementById('lynn-btn-loc').addEventListener('click', async function (e) {
e.preventDefault();
e.stopPropagation();
app.settings.data.features.locationDisplay = true;
app.settings.save();
if (app.ui.locationDisplay) {
app.ui.locationDisplay.style.display = 'block';
}
await app.triggerFeature('locationDisplay');
setTimeout(() => {
patchLocationPanel(app);
updateLocMap(app);
}, 120);
});
document.getElementById('lynn-btn-gm').addEventListener('click', async function (e) {
e.preventDefault();
e.stopPropagation();
await app.triggerFeature('openGM');
});
}
function enableSimple3PanelDrag() {
const panel = document.getElementById('lynn-simple3-panel');
if (!panel) return;
if (panel.dataset.dragReady === '1') return;
panel.dataset.dragReady = '1';
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
function start(clientX, clientY, target) {
if (target.closest('button')) return;
dragging = true;
startX = clientX;
startY = clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
panel.style.right = 'auto';
panel.style.transform = 'none';
}
function move(clientX, clientY) {
if (!dragging) return;
const dx = clientX - startX;
const dy = clientY - startY;
const rect = panel.getBoundingClientRect();
let left = startLeft + dx;
let top = startTop + dy;
left = Math.max(0, Math.min(window.innerWidth - rect.width, left));
top = Math.max(0, Math.min(window.innerHeight - rect.height, top));
panel.style.left = left + 'px';
panel.style.top = top + 'px';
}
function stop() {
if (!dragging) return;
dragging = false;
localStorage.setItem(STORAGE.panelLeft, panel.style.left || '0px');
localStorage.setItem(STORAGE.panelTop, panel.style.top || '0px');
}
panel.addEventListener('touchstart', function (e) {
if (!e.touches || !e.touches[0]) return;
start(e.touches[0].clientX, e.touches[0].clientY, e.target);
}, { passive: false });
document.addEventListener('touchmove', function (e) {
if (!dragging || !e.touches || !e.touches[0]) return;
e.preventDefault();
move(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
document.addEventListener('touchend', stop);
panel.addEventListener('mousedown', function (e) {
start(e.clientX, e.clientY, e.target);
});
document.addEventListener('mousemove', function (e) {
move(e.clientX, e.clientY);
});
document.addEventListener('mouseup', stop);
}
/* =========================
drag menu
========================= */
function enableMenuDrag(app) {
if (!app || !app.ui || !app.ui.menu) return;
const menu = app.ui.menu;
const header = menu.querySelector('.lunar-header');
if (!header) return;
if (header.dataset.dragReady === '1') return;
header.dataset.dragReady = '1';
const savedLeft = localStorage.getItem(STORAGE.menuLeft);
const savedTop = localStorage.getItem(STORAGE.menuTop);
if (savedLeft && savedTop) {
menu.style.left = savedLeft;
menu.style.top = savedTop;
menu.style.transform = 'none';
}
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
function isControl(target) {
return target.closest('button, input, select, textarea, .lunar-header-btn, .lynn-menu-size-btn');
}
function start(clientX, clientY, target) {
if (isControl(target)) return;
dragging = true;
startX = clientX;
startY = clientY;
const rect = menu.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
menu.style.transform = 'none';
}
function move(clientX, clientY) {
if (!dragging) return;
const dx = clientX - startX;
const dy = clientY - startY;
const rect = menu.getBoundingClientRect();
let left = startLeft + dx;
let top = startTop + dy;
left = Math.max(0, Math.min(window.innerWidth - rect.width, left));
top = Math.max(0, Math.min(window.innerHeight - 40, top));
menu.style.left = left + 'px';
menu.style.top = top + 'px';
}
function stop() {
if (!dragging) return;
dragging = false;
localStorage.setItem(STORAGE.menuLeft, menu.style.left || '0px');
localStorage.setItem(STORAGE.menuTop, menu.style.top || '0px');
}
header.addEventListener('touchstart', function (e) {
if (!e.touches || !e.touches[0]) return;
start(e.touches[0].clientX, e.touches[0].clientY, e.target);
}, { passive: false });
document.addEventListener('touchmove', function (e) {
if (!dragging || !e.touches || !e.touches[0]) return;
e.preventDefault();
move(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
document.addEventListener('touchend', stop);
header.addEventListener('mousedown', function (e) {
start(e.clientX, e.clientY, e.target);
});
document.addEventListener('mousemove', function (e) {
move(e.clientX, e.clientY);
});
document.addEventListener('mouseup', stop);
}
/* =========================
tombol resize menu
========================= */
function createMenuResizeControls(app) {
if (!app || !app.ui || !app.ui.menu) return;
const actions = app.ui.menu.querySelector('.lunar-header-actions');
if (!actions) return;
if (document.getElementById('lynn-menu-size-controls')) return;
const wrap = document.createElement('div');
wrap.id = 'lynn-menu-size-controls';
wrap.innerHTML = `
`;
actions.prepend(wrap);
const btnSmaller = document.getElementById('lynn-menu-smaller');
const btnBigger = document.getElementById('lynn-menu-bigger');
if (btnSmaller) {
btnSmaller.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
changeMenuSize(app, -40, -28);
});
}
if (btnBigger) {
btnBigger.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
changeMenuSize(app, 40, 28);
});
}
}
function applySavedMenuSize(app) {
if (!app || !app.ui || !app.ui.menu) return;
const menu = app.ui.menu;
const size = getMenuSize();
menu.style.width = size.width + 'px';
menu.style.height = size.height + 'px';
}
function changeMenuSize(app, deltaW, deltaH) {
if (!app || !app.ui || !app.ui.menu) return;
const menu = app.ui.menu;
const currentWidth = parseInt(menu.style.width || localStorage.getItem(STORAGE.menuWidth) || String(DEFAULT_MENU_W), 10);
const currentHeight = parseInt(menu.style.height || localStorage.getItem(STORAGE.menuHeight) || String(DEFAULT_MENU_H), 10);
const nextWidth = clamp((Number.isNaN(currentWidth) ? DEFAULT_MENU_W : currentWidth) + deltaW, 680, 1100);
const nextHeight = clamp((Number.isNaN(currentHeight) ? DEFAULT_MENU_H : currentHeight) + deltaH, 500, 820);
menu.style.width = nextWidth + 'px';
menu.style.height = nextHeight + 'px';
localStorage.setItem(STORAGE.menuWidth, String(nextWidth));
localStorage.setItem(STORAGE.menuHeight, String(nextHeight));
}
/* =========================
location panel
========================= */
function patchLocationPanel(app) {
const loc = document.getElementById('lunar-location-display');
if (!loc) return;
createMiniBox(loc);
createLocControls(loc);
createZoomRow(loc, app);
restoreLocationState(loc);
applyLocScale(loc);
enableMiniDrag(loc);
setupMapTap(app);
}
function createMiniBox(loc) {
if (document.getElementById('lynn-location-mini')) return;
const mini = document.createElement('div');
mini.id = 'lynn-location-mini';
mini.innerHTML = `
Location
+
`;
loc.appendChild(mini);
const openBtn = document.getElementById('lynn-location-mini-open');
if (openBtn) {
openBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
setLocationMinimized(false);
}, true);
}
}
function createLocControls(loc) {
if (document.getElementById('lynn-loc-controls')) return;
const topBar = loc.querySelector('div');
if (!topBar) return;
const oldBtn =
document.getElementById('lunar-loc-popout') ||
document.getElementById('lunar-loc-minimize') ||
document.getElementById('lunar-loc-minimize-safe') ||
document.getElementById('lunar-loc-minimize-final');
const controls = document.createElement('div');
controls.id = 'lynn-loc-controls';
controls.innerHTML = `
−
+
▁
`;
if (oldBtn) {
oldBtn.replaceWith(controls);
} else {
topBar.appendChild(controls);
}
document.getElementById('lynn-loc-scale-down').addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
changeLocScale(-0.1);
}, true);
document.getElementById('lynn-loc-scale-up').addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
changeLocScale(0.1);
}, true);
document.getElementById('lynn-loc-minimize').addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
setLocationMinimized(true);
}, true);
}
function createZoomRow(loc, app) {
if (document.getElementById('lynn-loc-zoom-row')) {
updateZoomLabel();
return;
}
const map = document.getElementById('lunar-map-image');
if (!map) return;
const row = document.createElement('div');
row.id = 'lynn-loc-zoom-row';
row.innerHTML = `
←
zoom 1 / 5
→
`;
map.insertAdjacentElement('afterend', row);
document.getElementById('lynn-loc-prev').addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
stepZoom(-1, app);
}, true);
document.getElementById('lynn-loc-next').addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
stepZoom(1, app);
}, true);
updateZoomLabel();
}
function updateZoomLabel() {
const label = document.getElementById('lynn-loc-zoom-label');
if (!label) return;
const idx = getSavedZoomIndex();
label.textContent = `zoom ${idx + 1} / ${MAP_ZOOMS.length}`;
}
function stepZoom(direction, app) {
let idx = getSavedZoomIndex();
idx += direction;
idx = clamp(idx, 0, MAP_ZOOMS.length - 1);
localStorage.setItem(STORAGE.locZoomIndex, String(idx));
updateZoomLabel();
updateLocMap(app);
}
function changeLocScale(delta) {
const loc = document.getElementById('lunar-location-display');
if (!loc) return;
let scale = parseFloat(localStorage.getItem(STORAGE.locScale) || '1');
if (Number.isNaN(scale)) scale = 1;
scale += delta;
scale = clamp(scale, 0.65, 1.25);
localStorage.setItem(STORAGE.locScale, String(scale));
applyLocScale(loc);
}
function applyLocScale(loc) {
if (!loc) return;
if (loc.classList.contains('lynn-loc-minimized')) {
loc.style.transform = 'none';
return;
}
let scale = parseFloat(localStorage.getItem(STORAGE.locScale) || '1');
if (Number.isNaN(scale)) scale = 1;
loc.style.transform = `scale(${scale})`;
loc.style.transformOrigin = 'top left';
}
function setLocationMinimized(state) {
const loc = document.getElementById('lunar-location-display');
if (!loc) return;
loc.classList.toggle('lynn-loc-minimized', state);
if (state) {
loc.style.transform = 'none';
} else {
applyLocScale(loc);
}
localStorage.setItem(STORAGE.locMin, state ? '1' : '0');
}
function restoreLocationState(loc) {
if (!loc) return;
if (localStorage.getItem(STORAGE.locMin) === '1') {
loc.classList.add('lynn-loc-minimized');
} else {
loc.classList.remove('lynn-loc-minimized');
}
const savedLeft = localStorage.getItem(STORAGE.locLeft);
const savedTop = localStorage.getItem(STORAGE.locTop);
if (savedLeft && savedTop) {
loc.style.left = savedLeft;
loc.style.top = savedTop;
}
}
function enableMiniDrag(loc) {
if (!loc) return;
if (loc.dataset.lynnMiniDragReady === '1') return;
loc.dataset.lynnMiniDragReady = '1';
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
function start(clientX, clientY, target) {
if (!loc.classList.contains('lynn-loc-minimized')) return;
if (target.closest('button')) return;
dragging = true;
startX = clientX;
startY = clientY;
const rect = loc.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
}
function move(clientX, clientY) {
if (!dragging) return;
const dx = clientX - startX;
const dy = clientY - startY;
const rect = loc.getBoundingClientRect();
let left = startLeft + dx;
let top = startTop + dy;
left = Math.max(0, Math.min(window.innerWidth - rect.width, left));
top = Math.max(0, Math.min(window.innerHeight - rect.height, top));
loc.style.left = left + 'px';
loc.style.top = top + 'px';
}
function stop() {
if (!dragging) return;
dragging = false;
localStorage.setItem(STORAGE.locLeft, loc.style.left || '20px');
localStorage.setItem(STORAGE.locTop, loc.style.top || '20px');
}
loc.addEventListener('touchstart', function (e) {
if (!e.touches || !e.touches[0]) return;
start(e.touches[0].clientX, e.touches[0].clientY, e.target);
}, { passive: false });
document.addEventListener('touchmove', function (e) {
if (!dragging || !e.touches || !e.touches[0]) return;
e.preventDefault();
move(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
document.addEventListener('touchend', stop);
loc.addEventListener('mousedown', function (e) {
start(e.clientX, e.clientY, e.target);
});
document.addEventListener('mousemove', function (e) {
move(e.clientX, e.clientY);
});
document.addEventListener('mouseup', stop);
}
function updateLocMap(app) {
const map = document.getElementById('lunar-map-image');
if (!map || !app || !app.network) return;
const coords = app.network.globalCoordinates;
if (!coords || coords.lat == null || coords.lng == null) return;
const lat = Number(coords.lat);
const lng = Number(coords.lng);
if (Number.isNaN(lat) || Number.isNaN(lng)) return;
const zoomIndex = getSavedZoomIndex();
const zoom = MAP_ZOOMS[zoomIndex];
const mapUrl =
`https://static-maps.yandex.ru/1.x/?ll=${lng},${lat}` +
`&z=${zoom}` +
`&size=450,220` +
`&l=map` +
`&lang=en` +
`&pt=${lng},${lat},pm2rdm`;
map.style.backgroundImage = `url("${mapUrl}")`;
map.style.backgroundSize = 'cover';
map.style.backgroundPosition = 'center';
updateZoomLabel();
}
function setupMapTap(app) {
const map = document.getElementById('lunar-map-image');
if (!map) return;
if (map.dataset.lynnTapReady === '1') return;
map.dataset.lynnTapReady = '1';
function nextZoom(e) {
e.preventDefault();
e.stopPropagation();
stepZoom(1, app);
}
map.addEventListener('click', nextZoom, true);
map.addEventListener('touchend', nextZoom, { passive: false, capture: true });
}
})();
new LunarApp();
})();