re-code the userscript
Some checks failed
Deploy App / docker (ubuntu-latest) (push) Failing after 7s
Some checks failed
Deploy App / docker (ubuntu-latest) (push) Failing after 7s
This commit is contained in:
74
src/common/page-utils.ts
Normal file
74
src/common/page-utils.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Get a callback for when the page changes
|
||||
*
|
||||
* @param callback The callback to call when the page changes
|
||||
*/
|
||||
export function pageChangeCallback(callback: (path: string) => void) {
|
||||
let previousUrl = "";
|
||||
const observer = new MutationObserver(() => {
|
||||
const currentUrl = location.pathname; // Get the current URL without parameters
|
||||
if (currentUrl == previousUrl) {
|
||||
return;
|
||||
}
|
||||
previousUrl = currentUrl;
|
||||
|
||||
callback(currentUrl);
|
||||
});
|
||||
const config = { subtree: true, childList: true };
|
||||
observer.observe(document, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Svelte class of an element
|
||||
*
|
||||
* @param baseClass The base class of the element
|
||||
*/
|
||||
export function getSvelteClass(baseClass: string): string | null {
|
||||
const element = document.querySelector(baseClass);
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the Svelte class
|
||||
for (let string of element.className.split(" ")) {
|
||||
if (string.startsWith("svelte-")) {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an element from the page and waits
|
||||
* for it to load or be available
|
||||
*
|
||||
* @param selector the selector of the element
|
||||
* @param checkInterval the interval to check for the element
|
||||
*/
|
||||
export function getElement(selector: string, checkInterval: number = 250): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
resolve(element as HTMLElement);
|
||||
return;
|
||||
}
|
||||
|
||||
let checkCount = 0;
|
||||
const checkElement = () => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
clearInterval(interval);
|
||||
resolve(element as HTMLElement);
|
||||
return;
|
||||
}
|
||||
checkCount++;
|
||||
if (checkCount * checkInterval >= 2500) { // Give up after 2.5 seconds
|
||||
clearInterval(interval);
|
||||
reject(new Error("Element not found within timeout"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(checkElement, checkInterval);
|
||||
});
|
||||
}
|
26
src/common/player.ts
Normal file
26
src/common/player.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {API_URL} from "../consts";
|
||||
|
||||
/**
|
||||
* Gets a player from the ScoreSaber Utils API
|
||||
*
|
||||
* @param id The player's ID
|
||||
*/
|
||||
export async function getPlayer(id: string) {
|
||||
const response = await fetch(`${API_URL}/account/${id}`);
|
||||
|
||||
// There was an error fetching the player
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch player");
|
||||
}
|
||||
|
||||
// Return the player's data
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player id from the current URL.
|
||||
*/
|
||||
export function getPlayerIdFromUrl(): string {
|
||||
const url = new URL(location.href);
|
||||
return url.pathname.split("/")[2];
|
||||
}
|
9
src/common/utils.ts
Normal file
9
src/common/utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Delays execution for the specified duration.
|
||||
*
|
||||
* @param ms The duration to delay in milliseconds
|
||||
* @returns A promise that resolves after the delay
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
4
src/consts.ts
Normal file
4
src/consts.ts
Normal file
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* The URL of the ScoreSaber Utils API
|
||||
*/
|
||||
export const API_URL = "https://ssu.fascinated.cc";
|
3
src/index.ts
Normal file
3
src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import PageHandler from "./pages/page-handler";
|
||||
|
||||
new PageHandler();
|
57
src/pages/impl/player-page.ts
Normal file
57
src/pages/impl/player-page.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import Page from "../page";
|
||||
import {getElement, getSvelteClass} from "../../common/page-utils";
|
||||
import {getPlayer, getPlayerIdFromUrl} from "../../common/player";
|
||||
|
||||
export default class PlayerPage extends Page {
|
||||
constructor() {
|
||||
super("/u/");
|
||||
}
|
||||
|
||||
public async onLoad() {
|
||||
try {
|
||||
// Wait for the title element to load, so we know the page is fully loaded
|
||||
const titleElement = await getElement(".title.is-5.player.has-text-centered-mobile", 250);
|
||||
|
||||
const id = getPlayerIdFromUrl();
|
||||
const player = await getPlayer(id);
|
||||
|
||||
console.log(player);
|
||||
await this.addStats(player);
|
||||
} catch (error) {
|
||||
console.error("Failed to load player page", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the custom stats to the player's page
|
||||
*
|
||||
* @param player the player to add stats for
|
||||
*/
|
||||
private async addStats(player: any) {
|
||||
await this.addStat(
|
||||
"+1 PP",
|
||||
`${player.rawPerGlobalPerformancePoints.toFixed(2)}pp`,
|
||||
"Raw performance points to gain +1 global PP"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a stat to the player's stats
|
||||
*
|
||||
* @param stat the title of the stat
|
||||
* @param value the value of the stat
|
||||
* @param hover the hover text of the stat
|
||||
*/
|
||||
private async addStat(stat: string, value: string, hover?: string) {
|
||||
const statsContainer = await getElement(".stats-container");
|
||||
const statElement = document.createElement("div");
|
||||
const svelteClass = getSvelteClass(".stats-container");
|
||||
|
||||
statElement.className = `stat-item ${svelteClass}`;
|
||||
statElement.innerHTML = `
|
||||
<span class="stat-title ${svelteClass}">${stat}</span>
|
||||
<span class="stat-spacer ${svelteClass}"></span>
|
||||
<span class="stat-content ${hover && "has-hover"} ${svelteClass}" ${hover && `title="${hover}"`}>${value}</span>`;
|
||||
statsContainer.appendChild(statElement);
|
||||
}
|
||||
}
|
34
src/pages/page-handler.ts
Normal file
34
src/pages/page-handler.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import Page from "./page";
|
||||
import PlayerPage from "./impl/player-page";
|
||||
import {pageChangeCallback} from "../common/page-utils";
|
||||
|
||||
export default class PageHandler {
|
||||
private pages: Page[] = [];
|
||||
|
||||
constructor() {
|
||||
// Register the pages to handle
|
||||
this.registerPage(new PlayerPage());
|
||||
|
||||
// Handle page changes
|
||||
pageChangeCallback((path) => {
|
||||
console.log(`Page changed to: ${path}`);
|
||||
|
||||
for (let page of this.pages) {
|
||||
if (path.startsWith(page.route)) {
|
||||
console.log(`Handling page: ${page.route}`);
|
||||
page.onLoad();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a page to be handled
|
||||
*
|
||||
* @param page The page to register
|
||||
*/
|
||||
private registerPage(page: Page) {
|
||||
console.log(`Registered page: ${page.route}`)
|
||||
this.pages.push(page);
|
||||
}
|
||||
}
|
24
src/pages/page.ts
Normal file
24
src/pages/page.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export default class Page {
|
||||
|
||||
/**
|
||||
* The route of the page
|
||||
* eg: /ranking
|
||||
*/
|
||||
private readonly _route: string;
|
||||
|
||||
constructor(route: string) {
|
||||
this._route = route;
|
||||
}
|
||||
|
||||
/*
|
||||
* This gets called when the page is loaded
|
||||
*/
|
||||
public onLoad() {}
|
||||
|
||||
/**
|
||||
* The route of the page
|
||||
*/
|
||||
get route(): string {
|
||||
return this._route;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user