// ==UserScript== // @name MWI Nex Leaderboard // @namespace https://milkywayidle.com/ // @version 1.0 // @license MIT // @description Overhauls the leaderboard UI using data from li-mwi-leaderboard.ngrok.io // @match https://*.milkywayidle.com/* // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // @downloadURL none // ==/UserScript== (function () { "use strict"; // --- Configuration --- const SORT_LIST_WIDTH = "140px"; const COLLAPSED_STATE_KEY = "mwi_leaderboard_sort_collapsed"; const style = document.createElement("style"); style.textContent = ` /* Main Wrapper */ .mwi-wrapper { display: flex; height: 1000px; /* <<< Your preferred height */ border: 1px solid #333; background: rgba(5, 5, 10, 0.85); color: #f0f0f0; font-family: 'Segoe UI', sans-serif; overflow: hidden; /* Control width for responsiveness */ width: 98%; max-width: 1600px; /* Keep or remove based on testing */ margin: 5px auto; } /* Left Panel - Flex Row */ .mwi-left { width: 45%; /* Default width */ min-width: 30px; border-right: 1px solid #222; display: flex; flex-direction: row; overflow: hidden; transition: width 0.3s ease; } /* Sort Options List (Column 1) */ .mwi-sort-options-list { width: ${SORT_LIST_WIDTH}; height: 100%; display: flex; flex-direction: column; background: rgba(10, 10, 20, 0.4); flex-shrink: 0; transition: width 0.3s ease, padding 0.3s ease, border 0.3s ease; overflow: hidden; } .mwi-sort-options-scroll { flex-grow: 1; overflow-y: auto; overflow-x: hidden; padding-bottom: 5px; } .mwi-sort-option { padding: 7px 10px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; color: #bbb; border-bottom: 1px solid #222; transition: background-color 0.2s, color 0.2s; } .mwi-sort-option:hover { background-color: rgba(255, 255, 255, 0.08); color: #fff; } .mwi-sort-option.active { background-color: rgba(60, 120, 180, 0.3); color: #7dd3fc; font-weight: bold; } /* Toggle Button (Sibling Column) */ .mwi-sort-toggle-btn { display: flex; align-items: center; justify-content: center; width: 20px; height: 100%; background-color: #18181f; border-right: 1px solid #2b2b33; color: #aaa; cursor: pointer; flex-shrink: 0; transition: background-color 0.2s, color 0.2s; font-size: 16px; font-weight: bold; line-height: 1; } .mwi-sort-toggle-btn:hover { background-color: #2a2a33; color: #eee; } /* Player List Area (Column 3) */ .mwi-player-area { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 150px; } .mwi-player-list-container { flex-grow: 1; overflow-y: auto; padding-right: 5px; } /* Header Row Styles */ .mwi-list-header { display: flex; justify-content: space-between; padding: 5px 4px; border-bottom: 1px solid #555; margin: 0 4px 4px 4px; font-weight: bold; color: #ddd; font-size: 11px; text-transform: uppercase; flex-shrink: 0; } .mwi-list-header > span { text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Default Player list column widths */ .mwi-header-rank, .mwi-player-rank { flex-basis: 15%; color: #facc15; font-weight: bold; } .mwi-header-name, .mwi-player-name { flex-basis: 55%; padding: 0 6px; color: #f0f0f0;} .mwi-header-value, .mwi-player-value { flex-basis: 30%; text-align: center; color: #aaa; } .mwi-header-value { color: #ddd; text-align: center; } .mwi-header-rank { color: #ddd; text-align: left; } /* Player Row Styles */ .mwi-player-row { cursor: pointer; padding: 7px 4px; margin: 0 4px; border-bottom: 1px solid #222; transition: background 0.2s; display: flex; justify-content: space-between; align-items: center; font-size: 13px; } .mwi-player-row:hover { background: rgba(255, 255, 255, 0.05); } .mwi-player-row.selected { background: rgba(80, 100, 120, 0.2); } .mwi-player-row > span { flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Right Panel */ .mwi-right { width: 55%; padding: 0; background: rgba(10, 10, 20, 0.6); display: flex; flex-direction: column; overflow-y: hidden; } .mwi-right-content { flex-grow: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; } .mwi-right-content > em { margin: auto; font-size: 14px; color: #aaa; } .mwi-header { font-size: 20px; font-weight: bold; color: #ffe27a; margin-bottom: 8px; flex-shrink: 0; } .mwi-meta { font-size: 13px; line-height: 1.5; color: #bbb; margin-bottom: 8px; flex-shrink: 0; } .mwi-meta b { color: #ddd; } .mwi-meta-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 20px; font-size: 13px; margin-top: 6px; align-items: start; } .mwi-meta-grid > div { white-space: nowrap; } .mwi-rank-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #222; font-size: 13px; } .mwi-xphr { color: #38bdf8; font-size: 13px; margin-left: 4px; } .mwi-rank { color: #facc15; font-weight: bold; font-size: 13px; } .mwi-ranks-container { flex-shrink: 0; } .mwi-right h3 { color: #7dd3fc; margin-top: 12px; margin-bottom: 4px; font-size: 16px; flex-shrink: 0; } /* Collapsed State */ .mwi-left.collapsed .mwi-sort-options-list { width: 0; border-right: none; padding: 0; } .mwi-left.collapsed .mwi-sort-toggle-btn { border-right: 1px solid #333; } /* Media Query for Wider Screens */ @media (min-width: 2000px) { /* Adjusted breakpoint */ .mwi-left { width: 35%; } /* Adjusted width */ .mwi-right { width: 65%; } /* Adjusted width */ /* Adjust player list columns */ .mwi-header-rank, .mwi-player-rank { flex-basis: 12%; } .mwi-header-name, .mwi-player-name { flex-basis: 63%; } .mwi-header-value, .mwi-player-value { flex-basis: 25%; } } `; document.head.appendChild(style); let leaderboardData = null; let currentSortBy = "Total Level"; let isSortListCollapsed = GM_getValue(COLLAPSED_STATE_KEY, false); // Load saved state // --- *** REPLACED setInterval WITH MutationObserver Logic *** --- // Function to Apply Changes to a panel function applyLeaderboardEnhancements(targetNode) { let leaderboardDiv = null; if (targetNode.nodeType === Node.ELEMENT_NODE) { if (targetNode.matches('[class^="LeaderboardPanel_leaderboardPanel"]')) { leaderboardDiv = targetNode; } else if (targetNode.querySelector) { leaderboardDiv = targetNode.querySelector('[class^="LeaderboardPanel_leaderboardPanel"]:not([data-custom-injected="true"])'); } } // Check if found and not already processed if (leaderboardDiv && !leaderboardDiv.dataset.customInjected) { console.log('[MWI] Applying enhancements to leaderboard panel:', leaderboardDiv); leaderboardDiv.dataset.customInjected = "true"; // Mark it fetchLeaderboardData().then(() => { if (leaderboardData) { injectCustomLeaderboard(leaderboardDiv); } else { leaderboardDiv.innerHTML = "Failed to load custom leaderboard data."; } }).catch(err => { console.error("[MWI] Error during fetch/inject:", err); leaderboardDiv.innerHTML = "Error loading custom leaderboard."; }); } } // MutationObserver Setup const observerCallback = (mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { applyLeaderboardEnhancements(node); }); } } }; const observer = new MutationObserver(observerCallback); // Start observing the document body observer.observe(document.body, { childList: true, subtree: true }); // Initial Check on script load document.querySelectorAll('[class^="LeaderboardPanel_leaderboardPanel"]:not([data-custom-injected="true"])').forEach(panel => { applyLeaderboardEnhancements(panel); }); console.log('[MWI] Leaderboard Enhancer initialized and observing.'); // --- *** END OF MutationObserver Logic *** --- async function fetchLeaderboardData() { try { const res = await fetch("https://li-mwi-leaderboard.ngrok.io/api/leaderboard"); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } leaderboardData = await res.json(); if (!Array.isArray(leaderboardData)) { console.warn("[MWI] Leaderboard data is not an array:", leaderboardData); leaderboardData = []; } console.log("[MWI] Loaded leaderboard snapshot:", leaderboardData?.length); } catch (err) { console.error("[MWI] Failed to fetch leaderboard data:", err); leaderboardData = null; } } function injectCustomLeaderboard(container) { container.innerHTML = ""; const wrapper = document.createElement("div"); wrapper.className = "mwi-wrapper"; const left = document.createElement("div"); left.className = "mwi-left"; if (isSortListCollapsed) { left.classList.add("collapsed"); } const sortOptionsList = document.createElement("div"); sortOptionsList.className = "mwi-sort-options-list"; const sortOptionsScroll = document.createElement("div"); sortOptionsScroll.className = "mwi-sort-options-scroll"; const sortOptions = [ "Total Level", "XP", "Combat Level", "House Level", "Enhancing", "Magic", "Crafting", "Milking", "Stamina", "Cooking", "Tailoring", "Brewing", "Cheesesmithing", "Intelligence", "Power", "Ranged", "Attack", "Defense", "Foraging", "Alchemy", "Woodcutting" ]; sortOptions.forEach((opt) => { const optionDiv = document.createElement("div"); optionDiv.className = "mwi-sort-option"; optionDiv.textContent = opt; optionDiv.dataset.sortBy = opt; if (opt === currentSortBy) { optionDiv.classList.add("active"); } sortOptionsScroll.appendChild(optionDiv); }); sortOptionsList.appendChild(sortOptionsScroll); const toggleBtn = document.createElement("div"); toggleBtn.className = "mwi-sort-toggle-btn"; toggleBtn.textContent = isSortListCollapsed ? "»" : "«"; toggleBtn.title = isSortListCollapsed ? "Expand Sort List" : "Collapse Sort List"; const playerArea = document.createElement("div"); playerArea.className = "mwi-player-area"; const listHeader = document.createElement("div"); listHeader.className = "mwi-list-header"; listHeader.innerHTML = ` RANK NAME TYPE`; const headerValueSpan = listHeader.querySelector(".mwi-header-value"); const playerListContainer = document.createElement("div"); playerListContainer.className = "mwi-player-list-container"; playerArea.appendChild(listHeader); playerArea.appendChild(playerListContainer); left.appendChild(sortOptionsList); left.appendChild(toggleBtn); left.appendChild(playerArea); const right = document.createElement("div"); right.className = "mwi-right"; const rightContent = document.createElement("div"); rightContent.className = "mwi-right-content"; rightContent.innerHTML = "Select a player to view details"; right.appendChild(rightContent); function updateListHeader(sortByValue) { if (headerValueSpan) { headerValueSpan.textContent = sortByValue; headerValueSpan.title = `Sorted by: ${sortByValue}`; } } function renderPlayers(sortBy) { updateListHeader(sortBy); if (!leaderboardData) { playerListContainer.innerHTML = "