// ==UserScript== // @name ScoreSaber Utils // @namespace https://ssu.fascinated.cc // @version 1.0.5 // @description Useful additions to ScoreSaber! // @author Fascinated // @match https://scoresaber.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=scoresaber.com // @license MIT // @updateURL https://git.fascinated.cc/Fascinated/ScoreSaberUtils-Backend/raw/branch/master/scoresaber-utils.user.js // @downloadURL https://git.fascinated.cc/Fascinated/ScoreSaberUtils-Backend/raw/branch/master/scoresaber-utils.user.js // @run-at document-end // ==/UserScript== /** * Fetches data from an API endpoint. * * @param {string} url The URL of the API endpoint * @returns {Promise} The JSON response from the API */ async function fetchData(url) { const response = await fetch(url); return await response.json(); } /** * Inserts a stat into the specified container. * * @param {string} containerSelector The selector for the container to insert the stat into * @param {string} stat The stat name * @param {string} value The stat value * @param {string} hoverText The hover text */ function addStat(containerSelector, stat, value, hoverText) { const container = document.querySelector(containerSelector); if (!container) return; const svelteClass = container.classList.item(1); const statElement = document.createElement("div"); statElement.className = `stat-item ${svelteClass}`; statElement.innerHTML = ` ${stat} ${value} `; container.appendChild(statElement); } /** * Delays execution for the specified duration. * * @param {number} ms The duration to delay in milliseconds * @returns {Promise} A promise that resolves after the delay */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Loads ScoreSaber Utils data on player pages. */ async function loadPlayerData(path) { if (!path) { path = window.location.pathname; } path = path.replace("https://scoresaber.com", ""); const isPlayerPage = path.startsWith("/u/"); if (!isPlayerPage) { // Only run on player pages return; } // Wait for the stats container to load while (!document.querySelector(".stats-container")) { await sleep(250); } const playerId = path.split("/")[2]; // Get the title element await sleep(250); const titleElement = document.querySelector(".title.is-5.player.has-text-centered-mobile"); if (!titleElement) { console.error("Failed to find title element"); return; } const svelteClass = titleElement.classList.item(1); // Add a loading indicator const loadingElement = document.createElement("span"); loadingElement.className = `title-header pp ${svelteClass}`; loadingElement.textContent = "Loading ScoreSaber Utils Data..."; titleElement.appendChild(loadingElement); try { const playerData = await fetchData(`https://ssu.fascinated.cc/account/${playerId}`); addStat( ".stats-container", "+1 PP", `${playerData.rawPerGlobalPerformancePoints.toFixed(2)}pp`, "The amount of pp to increase the global pp by 1pp" ); } catch (error) { console.error("Failed to load player data:", error); } // Remove the loading indicator loadingElement.remove(); } // Watch for URL changes let previousUrl = ""; const observer = new MutationObserver(function (mutations) { const currentUrl = location.pathname; // Get the current URL without parameters if (currentUrl !== previousUrl) { previousUrl = currentUrl; loadPlayerData(currentUrl); } }); const config = { subtree: true, childList: true }; observer.observe(document, config);