Last active
June 29, 2024 08:06
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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; | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Changes:
If you want to disable color for clan tags on profile pages, search for
And replace it with
// userTagElement.style.color = "#ffffff";
Preview:
![image](https://camo.githubusercontent.com/64ae0854fdbdc762bb52d4af0c9c38d5324c802afc3b631e7b4ff0cb52c1081e/68747470733a2f2f75702e61657374682e6465762f52317339646d6b412e706e67)
If you want to disable color for clan tags on ranking pages completely, search for
and replace it with
// clanTag.style.color = "#ffffff";
Preview:
![image](https://private-user-images.githubusercontent.com/8437201/344377443-59abe840-3b46-4192-b90a-db9ce002b437.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjE5MTY4NzMsIm5iZiI6MTcyMTkxNjU3MywicGF0aCI6Ii84NDM3MjAxLzM0NDM3NzQ0My01OWFiZTg0MC0zYjQ2LTQxOTItYjkwYS1kYjljZTAwMmI0MzcucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDcyNSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA3MjVUMTQwOTMzWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YjJhYTNiZWNmNDA0NGQxNjMxZjI3OTQxYTViYTdhOTVkNzhlYjhlNWU1MjVmYTU4NzY2NmZhNjhhOTZkZmRiZiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.H1Yytnc4DKg_kUAO09R1FXj0jbBRA1fuBb60d_fOOVU)
Or alternatively, disable them completely by searching for
and editing to