Skip to content

Instantly share code, notes, and snippets.

@NiceAesth
Last active June 29, 2024 08:06
Show Gist options
  • Save NiceAesth/9fe0b68cf8579b1965e4772cee60ed08 to your computer and use it in GitHub Desktop.
Save NiceAesth/9fe0b68cf8579b1965e4772cee60ed08 to your computer and use it in GitHub Desktop.
osu! scores inspector userscript but with pretty much everything disabled other than clan tags since that's the only thing i wanted and the rest is fugly
// ==UserScript==
// @name osu! scores inspector
// @namespace https://score.kirino.sh
// @version 2024-06-27.28
// @description Display osu!alt and scores inspector data on osu! website
// @author Amayakase
// @match https://osu.ppy.sh/*
// @icon https://raw.githubusercontent.com/darkchii/score-inspector-extension/main/icon48.png
// @noframes
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @require https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js
// @require https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0
// @downloadURL https://github.com/darkchii/score-inspector-extension/raw/main/inspector.user.js
// @updateURL https://github.com/darkchii/score-inspector-extension/raw/main/inspector.user.js
// ==/UserScript==
(function () {
'use strict';
const SCORE_INSPECTOR_API = "https://api.kirino.sh/inspector/";
const MODE_NAMES = [
"osu!",
"osu!taiko",
"osu!catch",
"osu!mania"
];
const MODE_SLUGS = [
"osu",
"taiko",
"catch",
"mania"
]
const MODE_SLUGS_ALT = [
"osu",
"taiko",
"fruits",
"mania"
]
document.addEventListener("turbolinks:load", async function () {
await run();
});
//lets script know what elements to wait for before running
const PAGE_ELEMENT_WAIT_LIST = {
'user_page': '.profile-info__name',
}
const CUSTOM_RANKINGS_ATTRIBUTES = {}
const CUSTOM_RANKINGS = []
const lb_page_nav_items = [
{
name: "performance",
attr: "performance",
link: "/rankings/osu/performance"
}, {
name: "score",
attr: "score",
link: "/rankings/osu/score"
},
// {
// name: "total ss",
// attr: "total-ss",
// link: "/rankings/osu/ss"
// },
//add all CUSTOM_RANKINGS to the dropdown here
...CUSTOM_RANKINGS.map(ranking => {
return {
name: ranking.name,
attr: ranking.api_path,
link: ranking.path
}
}),
{
name: "country",
attr: "country",
link: "/rankings/osu/country"
}, {
name: "multiplayer",
attr: "multiplayer",
link: "/multiplayer/rooms/latest"
}, {
name: "seasons",
attr: "seasons",
link: "/seasons/latest"
}, {
name: "spotlights (old)",
attr: "spotlights",
link: "/ranking/osu/charts"
}, {
name: "kudosu",
attr: "kudosu",
link: "/rankings/kudosu"
}
]
const MAX_RANK_PAGE = 200;
let is_osuplus_active = false;
const shortNum = (number) => {
const postfixes = ['', 'k', 'M', 'B', 't']
let count = 0
while (number >= 1000 && count < postfixes.length) {
number /= 1000
count++
}
//round number to 2 decimal places
number = Math.round(number * 100) / 100;
return number + postfixes[count];
}
async function run() {
//check for id "osuplusSettingsBtn"
if (document.getElementById("osuplusSettingsBtn")) {
is_osuplus_active = true;
}
//if userpage
if (window.location.href.includes("/users/")) {
//override css font-size for class "value-display__value"
GM_addStyle(`
.value-display--rank .value-display__value {
font-size: 20px;
}
.value-display__label {
font-size: 12px;
}
`);
}
if (window.location.href.includes("/rankings/") ||
window.location.href.includes("/multiplayer/rooms/") ||
window.location.href.includes("/seasons/")) {
//header-nav-v4--list needs to flex start fill
//if game-mode is active, set width to 80%, else 100%
GM_addStyle(`
.header-nav-v4--list {
flex-grow: 1;
width: 88%;
}
.header-nav-v4__item {
padding-top: 2px;
padding-bottom: 2px;
}
.game-mode {
position: relative;
right: 0;
left: 50;
width: 12%;
flex-grow: 1;
}
`);
await handleLeaderboardPage();
}
await runUserPage();
await runUsernames();
await runScoreRankCompletionPercentages();
}
run();
async function handleHeader() {
//find 3rd with class "nav2__col nav2__col--menu"
const nav2 = document.getElementsByClassName("nav2__col nav2__col--menu")[2];
//get popup (nav2__menu-popup)
const popup = nav2.getElementsByClassName("nav2__menu-popup")[0];
//first child
const popup_dropdown = popup.children[0];
// const ss_rank_link = document.createElement("a");
// ss_rank_link.classList.add("simple-menu__item", "u-section-rankings--before-bg-normal");
// ss_rank_link.href = "/rankings/osu/ss";
// ss_rank_link.textContent = "total ss";
// //insert at index 2
// popup_dropdown.insertBefore(ss_rank_link, popup_dropdown.children[2]);
//add all custom rankings to the dropdown
CUSTOM_RANKINGS.forEach((ranking, index) => {
const link = document.createElement("a");
link.classList.add("simple-menu__item", "u-section-rankings--before-bg-normal");
link.href = ranking.path;
link.textContent = ranking.name;
popup_dropdown.insertBefore(link, popup_dropdown.children[2 + index]);
});
}
async function handleLeaderboardPage() {
//find ul with class "header-nav-v4 header-nav-v4--list"
let headerNav = document.getElementsByClassName("header-nav-v4 header-nav-v4--list")[0];
//check if we are on any of the rankings pages in CUSTOM_RANKINGS
//remove the query string from the url
let url = window.location.href.split("?")[0];
//remove the domain from the url
url = url.replace("https://osu.ppy.sh", "");
const active_custom_ranking = CUSTOM_RANKINGS.find(ranking => ranking.path === url);
if (active_custom_ranking) {
//wait 0.5s for the page to load
//set page title to "total ss (bullet) rankings | osu!"
document.title = `${active_custom_ranking.name} • rankings | osu!`;
const container = document.getElementsByClassName("osu-layout__section osu-layout__section--full")[0];
container.innerHTML = "";
const rankings_container = document.createElement("div");
rankings_container.classList.add("header-v4", "header-v4--rankings");
container.appendChild(rankings_container);
const rankings_header = document.createElement("div");
rankings_header.classList.add("header-v4__container", "header-v4__container--main");
rankings_container.appendChild(rankings_header);
const rankings_header_bg_container = document.createElement("div");
rankings_header_bg_container.classList.add("header-v4__bg-container");
rankings_header.appendChild(rankings_header_bg_container);
const rankings_header_bg_container_bg = document.createElement("div");
rankings_header_bg_container_bg.classList.add("header-v4__bg");
rankings_header_bg_container.appendChild(rankings_header_bg_container_bg);
const rankings_header_content = document.createElement("div");
rankings_header_content.classList.add("header-v4__content");
rankings_header.appendChild(rankings_header_content);
const rankings_header_content_title = document.createElement("div");
rankings_header_content_title.classList.add("header-v4__row", "header-v4__row--title");
rankings_header_content.appendChild(rankings_header_content_title);
const rankings_header_content_title_icon = document.createElement("div");
rankings_header_content_title_icon.classList.add("header-v4__icon");
rankings_header_content_title.appendChild(rankings_header_content_title_icon);
const rankings_header_content_title_text = document.createElement("div");
rankings_header_content_title_text.classList.add("header-v4__title");
rankings_header_content_title_text.textContent = "rankings";
rankings_header_content_title.appendChild(rankings_header_content_title_text);
const ranking_headers_container = document.createElement("div");
ranking_headers_container.classList.add("header-v4__container");
rankings_container.appendChild(ranking_headers_container);
const ranking_headers_content = document.createElement("div");
ranking_headers_content.classList.add("header-v4__content");
ranking_headers_container.appendChild(ranking_headers_content);
const ranking_headers_row = document.createElement("div");
ranking_headers_row.classList.add("header-v4__row", "header-v4__row--bar");
ranking_headers_content.appendChild(ranking_headers_row);
const ranking_headers_row_nav = document.createElement("ul");
ranking_headers_row_nav.classList.add("header-nav-v4", "header-nav-v4--list");
ranking_headers_row.appendChild(ranking_headers_row_nav);
headerNav = ranking_headers_row_nav;
const scores_container = document.createElement("div");
scores_container.classList.add("osu-page", "osu-page--generic");
scores_container.id = "scores";
container.appendChild(scores_container);
//get page from url query
let page = new URLSearchParams(window.location.search).get("page") ?? 1;
page = Number(page) || 1;
//first try to get data now
const fetch_url = `${SCORE_INSPECTOR_API}extension/rank/${active_custom_ranking.api_path}/${page}`;
const response = await fetch(fetch_url, {
headers: {
"Access-Control-Allow-Origin": "*",
}
});
let data = null;
try {
if (response.status !== 200) {
throw new Error("An error occurred while fetching the data. Please try again later.");
}
data = await response.json();
} catch (e) {
scores_container.innerHTML = "An error occurred while fetching the data. Please try again later.";
return;
}
const createPagination = (page) => {
const nav = document.createElement("nav");
nav.classList.add("pagination-v2");
const nav_prev_col = document.createElement("div");
nav_prev_col.classList.add("pagination-v2__col");
let nav_prev_span = null;
if (page === 1) {
nav_prev_span = document.createElement("span");
nav_prev_span.classList.add("pagination-v2__link", "pagination-v2__link--quick", "pagination-v2__link--disabled");
} else {
nav_prev_span = document.createElement("a");
nav_prev_span.classList.add("pagination-v2__link", "pagination-v2__link--quick");
// nav_prev_span.href = `/rankings/osu/ss?page=${page - 1}`;
nav_prev_span.href = `/rankings/osu/${active_custom_ranking.api_path}?page=${page - 1}`;
}
const nav_prev_span_icon = document.createElement("i");
nav_prev_span_icon.classList.add("fas", "fa-angle-left");
nav_prev_span.appendChild(nav_prev_span_icon);
nav_prev_span.appendChild(document.createTextNode(" "));
const nav_prev_span_text = document.createElement("span");
nav_prev_span_text.textContent = "PREV";
nav_prev_span.appendChild(nav_prev_span_text);
nav_prev_col.appendChild(nav_prev_span);
nav.appendChild(nav_prev_col);
const nav_next_col = document.createElement("div");
nav_next_col.classList.add("pagination-v2__col");
const BUTTONS_BEFORE_CURRENT_PAGE = 2;
const BUTTONS_AFTER_CURRENT_PAGE = 2;
//1 and 200 are always shown
const _createPageButton = (_page, active = false) => {
const li = document.createElement("li");
li.classList.add("pagination-v2__item");
let a = null;
if (_page === page) {
a = document.createElement("span");
} else {
a = document.createElement("a");
}
a.classList.add("pagination-v2__link");
// a.href = `/rankings/osu/ss?page=${_page}`;
a.href = `/rankings/osu/${active_custom_ranking.api_path}?page=${_page}`;
if (active) {
a.classList.add("pagination-v2__link--active");
}
a.textContent = _page;
li.appendChild(a);
return li;
}
const pagination_items = document.createElement("div");
pagination_items.classList.add("pagination-v2__col", "pagination-v2__col--pages");
nav.appendChild(pagination_items);
//just loop between 1 and 200
for (let i = 1; i <= MAX_RANK_PAGE; i++) {
if (i === 1 || i === MAX_RANK_PAGE || (i >= page - BUTTONS_BEFORE_CURRENT_PAGE && i <= page + BUTTONS_AFTER_CURRENT_PAGE)) {
pagination_items.appendChild(_createPageButton(i, i === page));
} else if (i === page - BUTTONS_BEFORE_CURRENT_PAGE - 1 || i === page + BUTTONS_AFTER_CURRENT_PAGE + 1) {
const li = document.createElement("li");
li.classList.add("pagination-v2__item");
li.textContent = "...";
pagination_items.appendChild(li);
}
}
let nav_next_span = null;
if (page === MAX_RANK_PAGE) {
nav_next_span = document.createElement("span");
nav_next_span.classList.add("pagination-v2__link", "pagination-v2__link--quick", "pagination-v2__link--disabled");
} else {
nav_next_span = document.createElement("a");
nav_next_span.classList.add("pagination-v2__link", "pagination-v2__link--quick");
// nav_next_span.href = `/rankings/osu/ss?page=${page + 1}`;
nav_next_span.href = `/rankings/osu/${active_custom_ranking.api_path}?page=${page + 1}`;;
}
const nav_next_span_icon = document.createElement("i");
const nav_next_span_text = document.createElement("span");
nav_next_span_text.textContent = "NEXT";
nav_next_span.appendChild(nav_next_span_text);
nav_next_span.appendChild(document.createTextNode(" "));
nav_next_span_icon.classList.add("fas", "fa-angle-right");
nav_next_span.appendChild(nav_next_span_icon);
nav_next_col.appendChild(nav_next_span);
nav.appendChild(nav_next_col);
return nav;
}
scores_container.appendChild(createPagination(page));
//leaderboard, tbd
const ranking_page = document.createElement("div");
ranking_page.classList.add("ranking-page");
scores_container.appendChild(ranking_page);
const ranking_table = document.createElement("table");
ranking_table.classList.add("ranking-page-table");
ranking_page.appendChild(ranking_table);
const ranking_thead = document.createElement("thead");
ranking_table.appendChild(ranking_thead);
const _addTableHeaderItem = (text = '', is_focus = false, is_grade = false) => {
const th = document.createElement("th");
th.textContent = text;
th.classList.add("ranking-page-table__heading");
if (is_grade) {
th.classList.add("ranking-page-table__heading--grade");
}
if (is_focus) {
th.classList.add("ranking-page-table__heading--focused");
}
return th;
}
ranking_thead.appendChild(_addTableHeaderItem());
ranking_thead.appendChild(_addTableHeaderItem());
// ranking_thead.appendChild(_addTableHeaderItem('Total Score'));
// ranking_thead.appendChild(_addTableHeaderItem('Ranked Score'));
// ranking_thead.appendChild(_addTableHeaderItem('SS', true, true));
// ranking_thead.appendChild(_addTableHeaderItem('S', false, true));
// ranking_thead.appendChild(_addTableHeaderItem('A', false, true));
// ranking_thead.appendChild(_addTableHeaderItem('Clears', false));
for (let attr of active_custom_ranking.attributes) {
ranking_thead.appendChild(_addTableHeaderItem(attr[0].name, attr[1] ?? false));
}
const ranking_tbody = document.createElement("tbody");
ranking_table.appendChild(ranking_tbody);
const _addTableBodyRow = (data, i) => {
const tr = document.createElement("tr");
tr.classList.add("ranking-page-table__row");
const td_rank = document.createElement("td");
td_rank.classList.add("ranking-page-table__column", "ranking-page-table__rank");
td_rank.textContent = `#${i + 1 + (page - 1) * 50}`;
tr.appendChild(td_rank);
const td_user = document.createElement("td");
td_user.classList.add("ranking-page-table__column", "ranking-page-table__user");
const userLinkParent = document.createElement("div");
userLinkParent.classList.add("ranking-page-table__user-link");
const countryFlagUrl = document.createElement("a");
countryFlagUrl.href = `/rankings/osu/performance?country=${data.country_code}`;
countryFlagUrl.style.display = "inline-block";
const countryFlag = document.createElement("span");
countryFlag.classList.add("flag-country", "flag-country--medium");
countryFlag.style.backgroundImage = `url(https://flagpedia.net/data/flags/h24/${data.country_code.toLowerCase()}.webp)`;
countryFlag.setAttribute("title", data.country_name);
countryFlagUrl.appendChild(countryFlag);
userLinkParent.appendChild(countryFlagUrl);
const userLink = document.createElement("a");
userLink.classList.add("ranking-page-table__user-link-text", "js-usercard");
userLink.href = `/users/${data.user_id}`;
userLink.textContent = data.username;
userLink.setAttribute("data-user-id", data.user_id);
userLinkParent.appendChild(userLink);
td_user.appendChild(userLinkParent);
tr.appendChild(td_user);
for (let attr of active_custom_ranking.attributes) {
const formatter = attr[0].formatter ?? ((val) => val.toLocaleString());
const td = document.createElement("td");
td.classList.add("ranking-page-table__column");
if (!attr[1]) {
td.classList.add("ranking-page-table__column--dimmed");
}
td.textContent = formatter(attr[0].val(data));
if (attr[0].tooltip_formatter) {
td.setAttribute("data-html-title", attr[0].tooltip_formatter(attr[0].val(data)));
td.setAttribute("title", "");
}
tr.appendChild(td);
}
return tr;
}
data.forEach((d, i) => {
ranking_tbody.appendChild(_addTableBodyRow(d, i));
});
//another pagination at the bottom
scores_container.appendChild(createPagination(page));
}
//empty the header nav
// CUSTOM_RANKINGS.forEach(ranking => {
// if (!headerNav.querySelector(`[data-content="${ranking.api_path}"]`)) {
// const li = document.createElement("li");
// li.classList.add("header-nav-v4__item");
// const a = document.createElement("a");
// a.classList.add("header-nav-v4__link");
// a.href = ranking.path;
// a.textContent = ranking.name;
// a.setAttribute("data-content", ranking.api_path);
// if (url !== ranking.path) {
// a.classList.remove("header-nav-v4__link--active");
// }
// li.appendChild(a);
// headerNav.appendChild(li);
// }
// });
//empty the header nav
headerNav.innerHTML = "";
lb_page_nav_items.forEach(item => {
if (!headerNav.querySelector(`[data-content="${item.attr}"]`)) {
const li = document.createElement("li");
li.classList.add("header-nav-v4__item");
const a = document.createElement("a");
a.classList.add("header-nav-v4__link");
a.href = item.link;
a.textContent = item.name;
a.setAttribute("data-content", item.attr);
if (url === item.link) {
a.classList.add("header-nav-v4__link--active");
}
li.appendChild(a);
headerNav.appendChild(li);
}
});
}
//finds all usernames on the page and adds clan tags to them
async function runUsernames() {
let isWorking = false;
const _func = async () => {
if (isWorking) {
return;
}
isWorking = true;
try {
await new Promise(r => setTimeout(r, 1000));
if (window.location.href.includes("/beatmapsets/")) {
if (is_osuplus_active) {
await WaitForElement('.osu-plus', 1000); //osu-plus updates leaderboards, so we wait for it in case user has it enabled
}
}
const usercards = document.getElementsByClassName("js-usercard");
const usercards_big = document.getElementsByClassName("user-card");
const user_ids = Array.from(usercards).map(card => card.getAttribute("data-user-id"));
const user_ids_big = Array.from(usercards_big).map(card => getUserCardBigID(card));
const _user_ids = user_ids.concat(user_ids_big).filter((v, i, a) => a.indexOf(v) === i);
//unique user ids
const clan_data = await getUsersClans(_user_ids);
if (clan_data && Array.isArray(clan_data) && clan_data.length > 0) {
modifyJsUserCards(clan_data);
}
} catch (err) {
console.error(err);
}
isWorking = false;
}
await _func();
const observer = new MutationObserver((mutationsList, observer) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
if (window.location.href.includes("/beatmapsets/")) {
if (mutation.target.classList.contains("beatmapset-scoreboard__main")) {
_func();
}
}
if (window.location.href.includes("/community/chat")) {
if (mutation.target.classList.contains("chat-conversation")) {
_func();
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getUserCardBigID(card) {
const a = card.querySelector("a");
const href_split = a.href.split("/");
const user_id = href_split[href_split.length - 1];
return user_id;
}
function modifyJsUserCards(clan_data) {
// let usercards = document.querySelectorAll("[class*='js-usercard']");
//get all usercards that have class "js-usercard" or "user-card"
let usercards = document.querySelectorAll("[class*='js-usercard'], [class*='user-card']");
//filter out with class "comment__avatar"
usercards = Array.from(usercards).filter(card => !card.classList.contains("comment__avatar"));
//filter out with child class "avatar avatar--guest avatar--beatmapset"
usercards = usercards.filter(card => !card.querySelector(".avatar.avatar--guest.avatar--beatmapset"));
//filter out with parent class "chat-conversation__new-chat-avatar"
usercards = usercards.filter(card => !card.parentElement.classList.contains("chat-conversation__new-chat-avatar"));
if (window.location.href.includes("/rankings/")) {
//check if "ranking-page-table__user-link" have a div as first child
const userLinks = document.getElementsByClassName("ranking-page-table__user-link");
const userLinksArray = Array.from(userLinks);
let uses_region_flags = false;
//if the first child is a div, and any has more than 1 child inside the div, then it uses region flags
uses_region_flags = userLinksArray.some(link => link.children[0].tagName === "DIV" && link.children[0].children.length > 1);
//if we use region flags, we append a fake one for divs that only have 1 child, to fill the gap
//basically duplicate the first child, as a test
if (uses_region_flags) {
usercards = usercards.map((card, i) => {
const userLink = userLinksArray[i];
if (userLink) {
//if first element is A with "country" somewhere in the url, create a div at index 0, and move the A into it
if (userLink.children[0].tagName === "A" && userLink.children[0].href.includes("country")) {
//create a div at index 0
const div = document.createElement("div");
//move div into it
div.appendChild(userLink.children[0]);
//move div to index 1
userLink.insertBefore(div, userLink.children[0]);
}
if (userLink.children[0].tagName === "DIV" && userLink.children[0].children.length === 1) {
const cloned = userLink.children[0].children[0].cloneNode(true);
userLink.children[0].appendChild(cloned);
//add display: inline-block to both children
userLink.children[0].children[0].style.display = "inline-block";
userLink.children[0].children[1].style.display = "inline-block";
//margin-left 4px to the second child
userLink.children[0].children[1].style.marginLeft = "4px";
//opacity 0 to second child
userLink.children[0].children[1].style.opacity = "0";
}
}
return card;
});
}
}
for (let i = 0; i < usercards.length; i++) {
if (!clan_data || clan_data.length === 0) return;
let user_id = null;
let user_clan_data = null;
//if user-card
if (usercards[i].classList.contains("user-card")) {
user_id = getUserCardBigID(usercards[i]);
user_clan_data = clan_data.find(clan => clan.osu_id == user_id);
if (!user_clan_data || !user_id) {
continue;
}
setBigUserCardClanTag(usercards[i], user_clan_data);
continue;
}
user_id = usercards[i].getAttribute("data-user-id");
user_clan_data = clan_data.find(clan => clan.osu_id == user_id);
if (!user_clan_data || !user_id) {
continue;
}
setUserCardBrickClanTag(usercards[i], user_clan_data);
}
}
const generateTagSpan = (clan) => {
const clanTag = document.createElement("a");
clanTag.textContent = `[${clan.clan.tag}] `;
clanTag.style.color = "#ffffff";
clanTag.style.fontWeight = "bold";
clanTag.href = `https://score.kirino.sh/clan/${clan.clan.id}`;
clanTag.target = "_blank";
//force single line
clanTag.style.whiteSpace = "nowrap";
//set id
clanTag.classList.add("inspector_user_tag");
return clanTag;
}
function setBigUserCardClanTag(card, clan) {
const usernameElement = card.getElementsByClassName("user-card__username u-ellipsis-pre-overflow")[0];
const clanTag = generateTagSpan(clan);
usernameElement.insertBefore(clanTag, usernameElement.childNodes[0]);
}
function setUserCardBrickClanTag(card, clan) {
//get content of the element (the username)
let username = card.textContent;
//trim the username
username = username.trim();
//create a span element ([clan_tag] username), set the color and url to the clan tag
const clanTag = generateTagSpan(clan);
//if usercard has class "beatmap-scoreboard-table__cell-content" along with it, add whitespace-width padding
if (card.classList.contains("beatmap-scoreboard-table__cell-content")) {
clanTag.style.paddingRight = "5px";
}
//if usercard has a "user-card-brick__link" child, insert the clan tag in there at index 1
const usercardLink = card.getElementsByClassName("user-card-brick__link")[0];
if (usercardLink) {
//first check if one exists already
if (usercardLink.getElementsByClassName("inspector_user_tag").length > 0) {
return;
}
clanTag.style.marginRight = "5px";
usercardLink.insertBefore(clanTag, usercardLink.childNodes[1]);
//if usercard has parent with class "chat-message-group__sender"
} else if (card.parentElement.classList.contains("chat-message-group__sender")) {
//check if one exists already
if (card.parentElement.getElementsByClassName("inspector_user_tag").length > 0) {
return;
}
const parent = card.parentElement;
//find child in parent with class "chat-message-group__username"
const usernameElement = parent.getElementsByClassName("chat-message-group__username")[0];
//insert clan tag in usernameElement before the text
usernameElement.insertBefore(clanTag, usernameElement.childNodes[0]);
} else {
//check if one exists already
if (card.getElementsByClassName("inspector_user_tag").length > 0) {
return;
}
card.insertBefore(clanTag, card.childNodes[0]);
}
}
//replaces the accuracy column with a completion percentage column
async function runScoreRankCompletionPercentages() {
//check if we are on "/rankings/osu/score" page
const _url = window.location.href;
if (!_url.includes("/rankings/osu/score")) {
return;
}
//wait for class 'ranking-page-table' to load
await WaitForElement('.ranking-page-table');
//get all the rows in the table
//rows are in the tbody of the table
const table = document.getElementsByClassName('ranking-page-table')[0];
const thead = table.getElementsByTagName('thead')[0];
const tbody = table.getElementsByTagName('tbody')[0];
const rows = tbody.getElementsByTagName('tr');
const headerRow = thead.getElementsByTagName('tr')[0];
//accuracy row is index 2
const USER_INDEX = 1;
const ACCURACY_INDEX = 2;
//change header to "Completion"
const headerCells = headerRow.getElementsByTagName('th');
headerCells[ACCURACY_INDEX].textContent = "Completion";
//change all rows to completion percentage (first do a dash, then do the percentage when the data is loaded)
let ids = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const cells = row.getElementsByTagName('td');
cells[ACCURACY_INDEX].textContent = "-";
//get the user id from the data-user-id attribute
//from column 1, get the the first child element with class 'js-usercard' in it, then get the data-user-id attribute
const user_id = cells[USER_INDEX].getElementsByClassName('js-usercard')[0].getAttribute('data-user-id');
ids.push(user_id);
}
//comma separated string
const id_string = ids.join(',');
const url = `${SCORE_INSPECTOR_API}users/stats/completion_percentage/${id_string}`;
const response = await fetch(url, {
headers: {
"Access-Control-Allow-Origin": "*"
}
});
const data = await response.json();
if (data.error) {
console.error(data.error);
return;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const cells = row.getElementsByTagName('td');
const user_id = cells[USER_INDEX].getElementsByClassName('js-usercard')[0].getAttribute('data-user-id');
let completion_percentage = data.find(d => d.user_id == user_id)?.completion ?? "-";
if (completion_percentage !== "-") {
//cap it at 100%, used profile stats for SS,S,A, which may be different from osu!alt
completion_percentage = Math.min(completion_percentage, 100);
completion_percentage = completion_percentage.toFixed(2);
}
//round to 2 decimal places
cells[ACCURACY_INDEX].textContent = `${completion_percentage}%`;
}
}
async function runUserPage() {
const url = window.location.href;
let fixedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
let user_id = null;
try {
user_id = fixedUrl.match(/\/users\/(\d+)/)[1];
} catch (e) { }
if (!user_id) {
return;
}
let mode = fixedUrl.match(/\/users\/\d+\/(osu|taiko|fruits|mania)/);
mode = mode ? mode[1] : "osu";
//wait for game-mode-link--active to load
await WaitForElement(".game-mode-link--active");
const activeModeElement = document.getElementsByClassName("game-mode-link game-mode-link--active")[0];
if (activeModeElement) {
mode = activeModeElement.getAttribute("data-mode");
}
await WaitForElement(PAGE_ELEMENT_WAIT_LIST.user_page);
//get username (first span element in profile-info__name)
const username = document.getElementsByClassName("profile-info__name")[0].getElementsByTagName("span")[0].textContent;
const data = await getUserData(user_id, username, mode);
if (data.clan && !data.clan?.pending) {
setOrCreateUserClanTagElement(data.clan.clan);
}
if (data.completion) {
setCompletionistBadges(data.completion);
}
}
async function WaitForElement(selector, timeout = 5000) {
const startTime = new Date().getTime();
while (document.querySelectorAll(selector).length == 0) {
if (new Date().getTime() - startTime > timeout) {
return null;
}
await new Promise(r => setTimeout(r, 100));
}
}
let _userClansCache = [];
async function getUsersClans(user_ids) {
// //first get all the cached users
let cached_users = [];
if (_userClansCache.length > 0) {
for (let i = 0; i < user_ids.length; i++) {
const user = _userClansCache.find(c => c.osu_id == user_ids[i]);
if (user) {
cached_users.push(user);
}
}
}
// filter out the cached users from the user_ids
if (cached_users.length > 0) {
user_ids = user_ids.filter(id => !cached_users.find(c => c.osu_id == id));
}
let uncached_users = [];
if (user_ids.length > 0) {
const url = SCORE_INSPECTOR_API + "extension/clans/users";
const response = await fetch(url, {
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify({
ids: user_ids
})
});
const data = await response.json();
if (!data || data.error) {
console.error(data?.error);
uncached_users = [];
} else {
uncached_users = JSON.parse(JSON.stringify(data));
}
}
//add the uncached users to the cache if they are not already in it (async might have race conditions, we don't worry about it)
const merged_data = [...cached_users, ...uncached_users];
//push to cache if not already in it
if (uncached_users?.length > 0) {
uncached_users.forEach(u => {
if (!_userClansCache.find(c => c.osu_id == u.osu_id)) {
_userClansCache.push(u);
}
});
}
return merged_data;
}
async function getUserData(user_id, username, mode = "osu") {
const modeIndex = MODE_SLUGS_ALT.indexOf(mode);
let data = null;
try {
const url = SCORE_INSPECTOR_API + `extension/profile`;
const response = await fetch(url, {
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify({
user_id: user_id,
mode: modeIndex,
username: username
})
});
data = await response.json();
return data;
} catch (err) {
console.error(err);
return null;
}
}
function setCompletionistBadges(badge_data) {
if (!badge_data || badge_data.length === 0) {
return;
}
//check if we have a badge area already (class "profile-badges"), otherwise create it
var badgeArea = document.getElementsByClassName("profile-badges")[0];
if (!badgeArea) {
badgeArea = document.createElement("div");
badgeArea.className = "profile-badges";
//insert it before "profile-detail"
const profileDetail = document.getElementsByClassName("profile-detail")[0];
profileDetail.parentNode.insertBefore(badgeArea, profileDetail);
}
//order newest to oldest
badge_data.sort((a, b) => new Date(b.completion_date) - new Date(a.completion_date));
//create a badge for each completionist badge
badge_data.forEach(badge => {
if (badgeArea.querySelector(`img[src='https://assets.ppy.sh/profile-badges/completionist_${MODE_SLUGS[badge.mode]}.png']`)) {
return;
}
var a = document.createElement("a");
a.href = `https://score.kirino.sh/completionists`;
badgeArea.appendChild(a);
const pretty_date = new Date(badge.completion_date).toLocaleDateString("en-GB", {
day: 'numeric',
month: 'long',
year: 'numeric'
});
var img = document.createElement("img");
// img.src = MODE_COMPLETION_BADGES[badge.mode];
img.src = `https://assets.ppy.sh/profile-badges/completionist_${MODE_SLUGS[badge.mode]}.png`;
img.className = "profile-badges__badge";
a.setAttribute("data-html-title", `
<div>${MODE_NAMES[badge.mode]} completionist (awarded ${badge.completion_date})</div>
<div>Scores: ${badge.scores.toLocaleString()}</div>
<div class='profile-badges__date'>${pretty_date}</div>
`);
a.title = `${MODE_NAMES[badge.mode]} completionist (awarded ${pretty_date})`
a.appendChild(img);
});
const badges = Array.from(badgeArea.children);
if (badges && badges.length > 1) {
for (let i = badges.length - 1; i > 0; i--) {
const current = badges[i];
const previous = badges[i - 1];
//find both 'data-html-title' attributes in the current and next tree, may be on the element or any child element
let current_data_html_title = searchElementForAttribute(current, "data-html-title");
let previous_data_html_title = searchElementForAttribute(previous, "data-html-title");
//find profile-badges__date
const dateCurrent = current_data_html_title.match(/<div class='profile-badges__date'>(.*?)<\/div>/)[1] ?? "";
const datePrevious = previous_data_html_title.match(/<div class='profile-badges__date'>(.*?)<\/div>/)[1] ?? "";
//if previous is older than current, swap them
if (new Date(datePrevious) < new Date(dateCurrent)) {
badgeArea.insertBefore(current, previous);
}
}
}
}
function getValueDisplay(label, value, is_rank = false, tooltip = null) {
var div = document.createElement("div");
div.className = `value-display value-display--${is_rank ? 'rank' : 'plain'}`;
var labelDiv = document.createElement("div");
labelDiv.className = "value-display__label";
labelDiv.textContent = label;
div.appendChild(labelDiv);
var valueDiv = document.createElement("div");
valueDiv.className = "value-display__value";
if (value === 'NaN') {
valueDiv.textContent = `${is_rank ? '#' : ''}-`;
div.setAttribute("data-html-title", `<div>Data not available</div>`);
div.setAttribute("title", "");
} else {
valueDiv.textContent = `${is_rank ? '#' : ''}${value}`;
if (tooltip) {
valueDiv.setAttribute("data-html-title", `<div>${tooltip}</div>`);
valueDiv.setAttribute("title", "");
}
}
div.appendChild(valueDiv);
return div;
}
function setOrCreateUserClanTagElement(clan) {
//check if element with id "inspector_user_tag" exists
var userTagElement = document.getElementById("inspector_user_tag");
var userTagParent = null;
//if it doesn't, create it (clone it from the first child of the profile-info__name node)
if (!userTagElement) {
var profileNameParentNode = document.getElementsByClassName("profile-info__name")[0];
userTagElement = profileNameParentNode.childNodes[0].cloneNode(true);
userTagElement.id = "inspector_user_tag";
//create a div
var div = document.createElement("a");
div.style.display = "inline";
//no underline
div.style.textDecoration = "none";
//add cloned element to the div
div.appendChild(userTagElement);
userTagParent = div;
//add the div to the parent node
profileNameParentNode.insertBefore(div, profileNameParentNode.childNodes[0]);
} else {
//get the parent of the userTagElement
userTagParent = userTagElement.parentNode;
}
//set the text content of the element to the inspector_user tag
userTagElement.textContent = `[${clan.tag}]`;
userTagElement.style.color = "#ffffff";
userTagElement.style.marginRight = "5px";
userTagElement.style.fontWeight = "bold";
//give it a tooltip
userTagParent.setAttribute("data-html-title", `<div>${clan.name}</div>`);
userTagParent.setAttribute("title", "");
//make it a link to the clan page
userTagParent.href = `https://score.kirino.sh/clan/${clan.id}`;
userTagParent.target = "_blank";
}
const getOrCreateTooltip = (chart) => {
let tooltipEl = chart.canvas.parentNode.querySelector('div');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)';
tooltipEl.style.borderRadius = '3px';
tooltipEl.style.color = 'white';
tooltipEl.style.opacity = 1;
tooltipEl.style.pointerEvents = 'none';
tooltipEl.style.position = 'absolute';
tooltipEl.style.transform = 'translate(-50%, -140%)';
tooltipEl.style.transition = 'all .1s ease';
const table = document.createElement('table');
table.style.margin = '0px';
tooltipEl.appendChild(table);
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
const externalTooltipHandler = (context) => {
// Tooltip Element
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map(b => b.lines);
const tableHead = document.createElement('thead');
titleLines.forEach(title => {
const tr = document.createElement('tr');
tr.style.borderWidth = 0;
const th = document.createElement('th');
th.style.borderWidth = 0;
const text = document.createTextNode(title);
th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});
const tableBody = document.createElement('tbody');
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i];
const span = document.createElement('span');
span.style.background = colors.backgroundColor;
span.style.borderColor = colors.borderColor;
span.style.borderWidth = '2px';
span.style.marginRight = '10px';
span.style.height = '10px';
span.style.width = '10px';
span.style.display = 'inline-block';
const tr = document.createElement('tr');
tr.style.backgroundColor = 'inherit';
tr.style.borderWidth = 0;
const td = document.createElement('td');
td.style.borderWidth = 0;
const text = document.createTextNode(body);
td.appendChild(span);
td.appendChild(text);
tr.appendChild(td);
tableBody.appendChild(tr);
});
const tableRoot = tooltipEl.querySelector('table');
// Remove old children
while (tableRoot.firstChild) {
tableRoot.firstChild.remove();
}
// Add new children
tableRoot.appendChild(tableHead);
tableRoot.appendChild(tableBody);
}
const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;
// Display, position, and set styles for font
tooltipEl.style.opacity = 1;
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
tooltipEl.style.font = tooltip.options.bodyFont.string;
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
};
function searchElementForAttribute(element, attribute) {
if (element.getAttribute(attribute)) {
return element.getAttribute(attribute);
}
for (let i = 0; i < element.children.length; i++) {
const child = element.children[i];
if (child.getAttribute(attribute)) {
return child.getAttribute(attribute);
}
}
return null;
}
})();
@NiceAesth
Copy link
Author

NiceAesth commented Jun 29, 2024

Changes:

  • Removed statistics on profile pages
  • Removed alternative graphs
  • Disabled clan tag colors (they look horrible), set to all white

If you want to disable color for clan tags on profile pages, search for

userTagElement.style.color = "#ffffff";

And replace it with

// userTagElement.style.color = "#ffffff";

Preview:
image

If you want to disable color for clan tags on ranking pages completely, search for

clanTag.style.color = "#ffffff";

and replace it with

// clanTag.style.color = "#ffffff";

Preview:
image

Or alternatively, disable them completely by searching for

for (let i = 0; i < usercards.length; i++) {

and editing to

return;
for (let i = 0; i < usercards.length; i++) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment