2024-04-25 06:11:06 +00:00
|
|
|
// ==UserScript==
|
|
|
|
// @name ScoreSaber Utils
|
|
|
|
// @namespace https://ssu.fascinated.cc
|
2024-04-25 19:05:51 +00:00
|
|
|
// @version 1.0.3
|
2024-04-25 06:11:06 +00:00
|
|
|
// @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
|
2024-04-25 06:34:22 +00:00
|
|
|
// @run-at document-end
|
2024-04-25 06:11:06 +00:00
|
|
|
// ==/UserScript==
|
|
|
|
|
|
|
|
/**
|
2024-04-25 07:09:59 +00:00
|
|
|
* Fetches data from an API endpoint.
|
2024-04-25 06:11:06 +00:00
|
|
|
*
|
2024-04-25 07:09:59 +00:00
|
|
|
* @param {string} url The URL of the API endpoint
|
2024-04-25 07:30:50 +00:00
|
|
|
* @returns {Promise<any>} The JSON response from the API
|
2024-04-25 06:11:06 +00:00
|
|
|
*/
|
2024-04-25 07:09:59 +00:00
|
|
|
async function fetchData(url) {
|
|
|
|
const response = await fetch(url);
|
2024-04-25 06:11:06 +00:00
|
|
|
return await response.json();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-04-25 07:09:59 +00:00
|
|
|
* Inserts a stat into the specified container.
|
2024-04-25 06:11:06 +00:00
|
|
|
*
|
2024-04-25 07:09:59 +00:00
|
|
|
* @param {string} containerSelector The selector for the container to insert the stat into
|
2024-04-25 06:11:06 +00:00
|
|
|
* @param {string} stat The stat name
|
|
|
|
* @param {string} value The stat value
|
|
|
|
* @param {string} hoverText The hover text
|
|
|
|
*/
|
2024-04-25 07:09:59 +00:00
|
|
|
function addStat(containerSelector, stat, value, hoverText) {
|
|
|
|
const container = document.querySelector(containerSelector);
|
|
|
|
if (!container) return;
|
|
|
|
|
2024-04-25 07:30:50 +00:00
|
|
|
const svelteClass = container.classList.item(1);
|
2024-04-25 06:11:06 +00:00
|
|
|
|
|
|
|
const statElement = document.createElement("div");
|
|
|
|
statElement.className = `stat-item ${svelteClass}`;
|
|
|
|
statElement.innerHTML = `
|
2024-04-25 07:09:59 +00:00
|
|
|
<span class="stat-title ${svelteClass}">${stat}</span>
|
|
|
|
<span class="stat-spacer ${svelteClass}"></span>
|
|
|
|
<span class="stat-content ${svelteClass} has-hover" title="${hoverText}">${value}</span>
|
2024-04-25 06:11:06 +00:00
|
|
|
`;
|
2024-04-25 07:09:59 +00:00
|
|
|
container.appendChild(statElement);
|
2024-04-25 06:11:06 +00:00
|
|
|
}
|
|
|
|
|
2024-04-25 07:09:59 +00:00
|
|
|
/**
|
|
|
|
* Delays execution for the specified duration.
|
|
|
|
*
|
|
|
|
* @param {number} ms The duration to delay in milliseconds
|
2024-04-25 07:30:50 +00:00
|
|
|
* @returns {Promise<void>} A promise that resolves after the delay
|
2024-04-25 07:09:59 +00:00
|
|
|
*/
|
2024-04-25 06:34:22 +00:00
|
|
|
function sleep(ms) {
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
}
|
|
|
|
|
2024-04-25 07:09:59 +00:00
|
|
|
/**
|
|
|
|
* Loads ScoreSaber Utils data on player pages.
|
|
|
|
*/
|
2024-04-25 07:30:50 +00:00
|
|
|
async function loadPlayerData(path) {
|
|
|
|
if (!path) {
|
|
|
|
path = window.location.pathname;
|
|
|
|
}
|
|
|
|
path = path.replace("https://scoresaber.com", "");
|
|
|
|
|
2024-04-25 06:11:06 +00:00
|
|
|
const isPlayerPage = path.startsWith("/u/");
|
|
|
|
|
2024-04-25 07:09:59 +00:00
|
|
|
if (!isPlayerPage) {
|
|
|
|
// Only run on player pages
|
|
|
|
return;
|
|
|
|
}
|
2024-04-25 06:34:22 +00:00
|
|
|
|
2024-04-25 07:09:59 +00:00
|
|
|
// Wait for the stats container to load
|
|
|
|
while (!document.querySelector(".stats-container")) {
|
|
|
|
await sleep(250);
|
2024-04-25 06:11:06 +00:00
|
|
|
}
|
2024-04-25 07:09:59 +00:00
|
|
|
const playerId = path.split("/")[2];
|
|
|
|
|
|
|
|
// Get the title element
|
2024-04-25 07:30:50 +00:00
|
|
|
await sleep(250);
|
2024-04-25 07:09:59 +00:00
|
|
|
const titleElement = document.querySelector(".title.is-5.player.has-text-centered-mobile");
|
2024-04-25 07:30:50 +00:00
|
|
|
if (!titleElement) {
|
|
|
|
console.error("Failed to find title element");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const svelteClass = titleElement.classList.item(1);
|
2024-04-25 06:34:22 +00:00
|
|
|
|
2024-04-25 07:09:59 +00:00
|
|
|
// 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();
|
2024-04-25 06:34:22 +00:00
|
|
|
}
|
|
|
|
|
2024-04-25 07:09:59 +00:00
|
|
|
loadPlayerData();
|
2024-04-25 07:30:50 +00:00
|
|
|
|
|
|
|
// Watch for URL changes
|
2024-04-25 19:05:51 +00:00
|
|
|
let previousUrl = "";
|
|
|
|
const observer = new MutationObserver(function (mutations) {
|
|
|
|
if (location.href !== previousUrl) {
|
|
|
|
previousUrl = location.href;
|
|
|
|
|
|
|
|
loadPlayerData(location.pathname);
|
2024-04-25 18:56:06 +00:00
|
|
|
}
|
2024-04-25 19:05:51 +00:00
|
|
|
});
|
|
|
|
const config = { subtree: true, childList: true };
|
|
|
|
observer.observe(document, config);
|