您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds graphing for which hours scores were set at
// ==UserScript== // @name OsuProfileScoresTimeGraph // @namespace https://github.com/Magnus-Cosmos // @version 1.1.12 // @description Adds graphing for which hours scores were set at // @author Magnus Cosmos // @match https://osu.ppy.sh/* // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://greasyfork.org/scripts/441005-osuweb/code/OsuWeb.js // @require https://greasyfork.org/scripts/441010-osupageobserver/code/OsuPageObserver.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.34/moment-timezone-with-data.min.js // ==/UserScript== const web = new Web(); const timezones = moment.tz.names(); let localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; let currTimezone = localTimezone; let selectedTz = null; function wait(delay) { return new Promise(resolve => setTimeout(resolve, delay)); } function escRegex(text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } const flagUrl = code => { const baseFileName = code.split('').map((c) => (c.charCodeAt(0) + 127397).toString(16)).join("-"); return `/assets/images/flags/${baseFileName}.svg`; }; $(document.head).append($("<style class='circlescript-style'></style>").html( ` .graph-button { display: inline-block; } .graph-close { background-color: hsl(var(--hsl-red-3)) !important; } .graph-close:hover { background-color: hsl(var(--hsl-red-2)) !important; } .graph-button-container { display: table-cell; text-align: right; } .graph-container { margin: 2px 0; padding: 10px; border-radius: 10px; background-color: hsl(var(--hsl-b3)); } .timezone { display: table-cell; text-align: center; vertical-align: middle; } .timezone-label { display: inline; font-weight: 700; color: hsl(var(--hsl-f1)); } .timezone-input { text-align: center; font-size: 12px; } .graph-heading { display: table; table-layout: fixed; width: 100%; margin-top: 10px; } .timezones-popup { position: absolute; right: 50%; transform: translateX(50%); z-index: 100; margin-top: 3px; } .timezones-popup .simple-menu--nav2 { will-change: auto; } .timezones-content { max-height: 189px !important; min-height: 0px !important; -ms-overflow-style: none; scrollbar-width: none; } .timezones-content::-webkit-scrollbar { display: none; } .timezones-menu__item--hover { background-color: var(--item-hover-bg); color: var(--item-hover-colour); } .timezones-menu__item--hover:before { content: ""; border-radius: 10px; position: absolute; top: 7px; left: 10px; width: 3px; height: calc(100% - 14px); background-color: var(--stripe-bg); } .flag-country--flat { image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; } #best-plays-graph { display: block; } ` )); function staticPage() { [this.type, ...this.paths] = location.pathname.split("/").slice(1).map(val => { const int = parseInt(val); return val == int ? int : val; }); } function getProfileJson() { const json = $(".osu-layout--full").attr("data-initial-data"); if (json) { return JSON.parse(json); } return null; } const osuWebObserver = new OsuWebObserver(staticPage, function() { switch(this.type) { case "users": { const profile = getProfileJson(); const self = currentUser.id; const user = profile.user; const userIsSelf = self === user.id; const mode = this.paths[1] ? this.paths[1] : user.playmode; const lazyObserver = new MutationObserver((mutations) => { if (user) { graphTimes("best", user, mode, userIsSelf); graphTimes("firsts", user, mode, userIsSelf); } }); lazyObserver.observe($(`div[data-page-id="top_ranks"]`).find(".lazy-load")[0], { attributes: true, attributeFilter: ["class"] }); } } }); const ScoreType = { firsts: 2, best: 1 } function filterTimezones() { $(this).next().children().first().attr("data-visibility", "visible"); $(this).next().children().first().removeClass("hidden"); $(this).next().find(".simple-menu__content").empty(); const matches = []; const input = $(this).val().replace(" ", "_"); const regex = new RegExp(escRegex(input), "i"); const prevSelectedTimezone = selectedTz?.attr("data-timezone"); selectedTz = null; if (input.length === 2 && input.toUpperCase() === input) { const tzs = moment.tz.zonesForCountry(input); tzs?.forEach(tz => { matches.push(tz); const isActive = $(this).next().children().first().attr("data-current-timezone") === tz; const button = $( `<button type="button" class="timezones-menu__item simple-menu__item${isActive ? " simple-menu__item--active" : ""}" data-timezone="${tz}"> <span class="nav2-locale-item"> <span class="nav2-locale-item__flag"> <div class="flag-country flag-country--flat" style="background-image: url('${flagUrl(input)}');"></div> </span> ${tz} </span> </button>`); $(this).next().find(".simple-menu__content").append(button); if (button.attr("data-timezone") === prevSelectedTimezone) { selectedTz = button.addClass("timezones-menu__item--hover"); } }); } else { timezones.forEach(timezone => { const match = timezone.match(regex); if (match) { matches.push(timezone); const isActive = $(this).next().children().first().attr("data-current-timezone") === timezone; const countries = moment.tz.zone(timezone).countries(); const countryCode = countries[0]; const button = $( `<button type="button" class="timezones-menu__item simple-menu__item${isActive ? " simple-menu__item--active" : ""}" data-timezone="${timezone}"> <span class="nav2-locale-item"> <span class="nav2-locale-item__flag"> <div class="flag-country flag-country--flat" style="background-image: url('${flagUrl(countryCode ? countryCode : "XX")}');"></div> </span> ${timezone} </span> </button>`); $(this).next().find(".simple-menu__content").append(button); if (button.attr("data-timezone") === prevSelectedTimezone) { selectedTz = button.addClass("timezones-menu__item--hover"); } if (countries.length > 1) { const swapFlagBg = i => { const flag = button.find(".flag-country"); const visible = button.parent().parent().attr("data-visibility") === "visible"; if (flag && visible) { const code = countries[i]; flag.css("background-image", `url('${flagUrl(code)}')`); wait(2000).then(swapFlagBg.bind(this, (i + 1) % countries.length)); } } swapFlagBg(Math.floor(Math.random() * countries.length)); } } }); } if (matches.length === 0) { $(this).next().children().first().attr("data-visibility", "hidden"); } else { $(".timezones-menu__item").on("mousedown", function() { $(this).parent().parent().parent().prev().val($(this).attr("data-timezone")); currTimezone = $(this).attr("data-timezone"); selectedTz = null; }); } return matches; } function graphTimes(type, user, mode, userIsSelf) { let data = null; let matches = []; const oh3 = $(`div[data-page-id="top_ranks"]`).find("h3").toArray().filter(el => $(el).css("display") !== "none")[ScoreType[type]]; const amount = $(oh3).find(".title__count").text(); if (parseInt(amount) === 0 || !oh3) { return; } currTimezone = localTimezone; if (!userIsSelf) { const userTimezones = moment.tz.zonesForCountry(user.country.code); if (userTimezones.length > 0) { currTimezone = userTimezones[0]; } } const h3 = $(oh3).clone().get(); $(oh3).css("display", "none"); $(h3).insertAfter(oh3); $(h3).wrap(`<div class="graph-heading"></div>`); $(`<div class="graph-button-container"> <button type="button" class="show-more-link graph-button graph-button--${type}"> <span class="show-more-link__spinner"> <div class="la-ball-clip-rotate"></div> </span> <span class="show-more-link__label"> <span class="show-more-link__label-text">graph</span> </span> </button> </div>`).insertAfter(h3); $(`<div class="timezone"> <input list="timezones" class="account-edit-entry__input timezone-input timezone-input--${type}" value="${currTimezone}" title="Timezone"> <div class="timezones-popup"> <div class="simple-menu simple-menu--nav2" data-visibility="hidden" data-current-timezone="${currTimezone}"> <div class="simple-menu__content timezones-content"></div> </div> </div> </div>`).insertAfter(h3); $(`.timezone-input--${type}`).on("focus", function() { matches = filterTimezones.call(this); }); $(`.timezone-input--${type}`).on("input", function() { matches = filterTimezones.call(this); }); $(`.timezone-input--${type}`).on("change", function() { let count = 0; for (const timezone of timezones) { const regex = new RegExp(`^${escRegex($(this).val().replace(" ", "_"))}$`, "i"); const match = timezone.match(regex); if (match) { count++; currTimezone = timezone; $(this).next().children().first().attr("data-current-timezone", timezone); $(this).val(timezone); const chart = Chart.getChart(`graph-${type}`); if (chart) { chart.destroy(); if (data) { const hidden = $(`.graph-container--${type}`).css("display") === "none"; if (!hidden) { graph(`graph-${type}`, hourBins(data)); } } } break; } } if (count === 0) { $(this).val($(this).next().children().first().attr("data-current-timezone")); } if (selectedTz) { $(this).next().find(".timezones-menu__item").removeClass("simple-menu__item--active"); selectedTz.addClass("simple-menu__item--active"); } }); $(`.timezone-input--${type}`).on("blur", function() { $(this).next().children().first().attr("data-visibility", "hidden"); selectedTz = null; }); $(`.timezone-input--${type}`).keydown(function (e) { const tzs = $(this).next().find(".timezones-menu__item"); if (e.which === 13) { if (selectedTz) { $(this).next().children().first().attr("data-current-timezone", selectedTz.attr("data-timezone")); $(this).val(selectedTz.attr("data-timezone")); $(this).blur(); } else { if (matches.length > 0) { $(this).next().children().first().attr("data-current-timezone", matches[0]); $(this).val(matches[0]); $(this).blur(); } } } else if (e.which === 38 || e.which === 40) { if (selectedTz) { selectedTz.removeClass("timezones-menu__item--hover"); let nextTz; if (e.which === 38) { nextTz = selectedTz.prev(); if (nextTz.length > 0) { selectedTz = nextTz.addClass("timezones-menu__item--hover"); } else { selectedTz = tzs.eq(tzs.length - 1).addClass("timezones-menu__item--hover"); } } else { nextTz = selectedTz.next(); if (nextTz.length > 0) { selectedTz = nextTz.addClass("timezones-menu__item--hover"); } else { selectedTz = tzs.eq(0).addClass("timezones-menu__item--hover"); } } } else { if (e.which === 38) { selectedTz = tzs.eq(tzs.length - 1).addClass("timezones-menu__item--hover"); } else { selectedTz = tzs.eq(0).addClass("timezones-menu__item--hover"); } } const container = selectedTz.parent().get(0); const selectedEl = selectedTz.get(0); container.scrollTop = selectedEl.offsetTop - (container.clientHeight + 10) + selectedEl.clientHeight; return false; } }); $(`div[data-page-id="top_ranks"]`).find(".show-more-link--profile-page").css("margin-bottom", "5px"); $(`.graph-button--${type}`).on("click", function() { $(this).children().first().css("display", "inline-flex"); $(this).children().last().css("visibility", "hidden"); $(this).prop("disabled", true); const successFn = scores => { data = scores; const bins = hourBins(data); $(`<div class="graph-container graph-container--${type}"></div>`).append(`<canvas id="graph-${type}"></canvas>`).insertAfter($(this).parent().parent()); graph(`graph-${type}`, bins); $(this).children().first().removeAttr("style"); $(this).children().last().removeAttr("style"); $(this).find(".show-more-link__label-text").text("close graph"); $(this).addClass("graph-close"); $(this).unbind("click"); $(this).prop("disabled", false); $(this).on("click", function() { const hidden = $(`.graph-container--${type}`).css("display") === "none"; if (hidden) { $(this).find(".show-more-link__label-text").text("close graph"); $(this).addClass("graph-close"); $(`.graph-container--${type}`).show(); const chart = Chart.getChart(`graph-${type}`); if (!chart && data) { graph(`graph-${type}`, hourBins(data)); } } else { $(this).find(".show-more-link__label-text").text("show graph"); $(this).removeClass("graph-close"); $(`.graph-container--${type}`).hide(); } }); }; const errorFn = err => { $(this).children().first().removeAttr("style"); $(this).children().last().removeAttr("style"); $(this).prop("disabled", false); console.log(err); } switch (type) { case "firsts": { getFirstPlaces(user, [], mode, 0, successFn, errorFn); break; } case "best": { web.get(`/users/${user.id}/scores/best`, { mode: mode, limit: 100 }, res => { res.json().then(successFn).catch(errorFn); }, errorFn, { credentials: "include" }); break; } } }); } function getFirstPlaces(user, scores, mode, offset, successFn, errorFn, tries = 0) { web.get(`/users/${user.id}/scores/firsts`, { mode: mode, limit: 100, offset: offset }, res => { res.json().then(data => { if (data.length === 0) { successFn(scores); return; } scores.push(...data); getFirstPlaces(user, scores, mode, offset + 100, successFn, errorFn); }).catch(err => { if (tries > 8) { errorFn(err); return; } wait(5000 * (tries + 1)).then(() => getFirstPlaces(user, scores, mode, offset, successFn, errorFn, tries + 1)); }); }, errorFn, { credentials: "include" }); } function hourBins(scores) { const bins = scores.reduce((obj, score) => { let hour = null; if (timezones.includes(currTimezone)) { const date = moment(score.ended_at).tz(currTimezone); hour = date.hours(); } else { const date = new Date(score.ended_at); hour = date.getHours(); } obj[hour]++; return obj; }, Object.fromEntries([...Array(24).keys()].map(k => [k, 0]))); return Object.entries(bins).map(([k, v]) => { return { x: k, y: v }; }); } function graph(id, data) { const ctx = document.getElementById(id).getContext("2d"); const font = "Torus,Inter,Helvetica Neue,Tahoma,Arial,Hiragino Kaku Gothic ProN,Meiryo,Microsoft YaHei,Apple SD Gothic Neo,sans-serif"; const chartAreaBorder = { id: "chartAreaBorder", beforeDraw(chart, args, options) { const {ctx, chartArea: {left, top, width, height}} = chart; ctx.save(); ctx.strokeStyle = options.borderColor; ctx.lineWidth = options.borderWidth; ctx.setLineDash(options.borderDash || []); ctx.lineDashOffset = options.borderDashOffset; ctx.strokeRect(left, top, width, height); ctx.restore(); } }; return new Chart(ctx, { type: "bar", data: { datasets: [{ data: data, backgroundColor: [ "rgba(31, 119, 180, 0.5)", ], barPercentage: 1, categoryPercentage: 0.96, borderRadius: 4, borderWidth: 1 }] }, options: { responsive: true, layout: { padding: { right: 10 } }, scales: { x: { title: { display: true, text: "Hour of the day", color: "rgba(255,255,255,0.6)", font: { family: font, weight: 400, size: 14 } }, ticks: { offset: false, color: "rgba(255,255,255,0.4)", font: { family: font, weight: 100 } }, offset: true, grid: { offset: true, drawBorder: false } }, y: { title: { display: true, text: "# of plays set", color: "rgba(255,255,255,0.6)", font: { family: font, weight: 400, size: 14 } }, ticks: { color: "rgba(255,255,255,0.4)", font: { family: font, weight: 100 }, callback: (label, index, labels) => { if (Math.floor(label) === label) { return label; } } }, grid: { drawBorder: false } } }, plugins: { legend: { display: false }, tooltip: { enabled: false }, chartAreaBorder: { borderColor: "rgba(0,0,0,0.25)", borderWidth: 1 } } }, plugins: [chartAreaBorder] }); }