osu!web enhancement

Some small improvements to osu!web, featuring beatmapset filter and profile page improvement.

当前为 2023-09-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name osu!web enhancement
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3.1
  5. // @description Some small improvements to osu!web, featuring beatmapset filter and profile page improvement.
  6. // @author VoltaXTY
  7. // @match https://osu.ppy.sh/*
  8. // @icon http://ppy.sh/favicon.ico
  9. // @grant none
  10. // @run-at document-end
  11. // ==/UserScript==
  12. console.log("osu!web enhancement loaded");
  13. const inj_style =
  14. `#osu-db-input{
  15. display: none;
  16. }
  17. .osu-db-button{
  18. align-items: center;
  19. }
  20. .osu-db-button:hover{
  21. cursor: pointer;
  22. }
  23. .beatmapsets__item.owned-beatmapset{
  24. opacity: 1.0;
  25. }
  26. .beatmapsets__item.owned-beatmapset .beatmapset-panel__menu-container{
  27. background-color: #87dda8;
  28. }
  29. .beatmapsets__item.owned-beatmapset .fas, .beatmapsets__item.owned-beatmapset .far{
  30. color: #5c9170;
  31. }
  32. .play-detail__accuracy{
  33. margin: 0px 12px;
  34. }
  35. .play-detail__accuracy.ppAcc{
  36. color: #8ef9f1;
  37. padding: 0;
  38. }
  39. .play-detail__weighted-pp{
  40. margin: 0px;
  41. }
  42. .play-detail__pp{
  43. flex-direction: column;
  44. }
  45. .lost-pp{
  46. font-size: 10px;
  47. position: relative;
  48. right: 7px;
  49. font-weight: 600;
  50. /* color: #81ff81; */
  51. }
  52. .score-detail{
  53. display: inline-block;
  54. }
  55. /*
  56. .score-detail-mania-max-300{
  57. width: 6em;
  58. }
  59. .score-detail-mania-200, .score-detail-mania-100, .score-detail-mania-miss{
  60. width: 4em;
  61. }
  62. .score-detail-mania-50{
  63. width: 3.5em;
  64. }
  65. */
  66. .score-detail-data-text{
  67. margin-left: 5px;
  68. margin-right: 10px;
  69. width: auto;
  70. display: inline-block;
  71. }
  72. @keyframes rainbow{
  73. 0%{
  74. color: #be19ff;
  75. }
  76. 25%{
  77. color: #0075ff;
  78. }
  79. 50%{
  80. color: #4ddf86;
  81. }
  82. 75%{
  83. color: #e9ea00;
  84. }
  85. 100%{
  86. color: #ff7800;
  87. }
  88. }
  89. .play-detail__accuracy-and-weighted-pp{
  90. display: flex;
  91. flex-direction: row-reverse;
  92. }
  93. .mania-max{
  94. animation: 0.16s infinite alternate rainbow;
  95. }
  96. .mania-300{
  97. color: #fbff00;
  98. }
  99. .osu-100{
  100. color: #67ff5b;
  101. }
  102. .mania-200{
  103. color: #6cd800;
  104. }
  105. .osu-300{
  106. color: #7dfbff;
  107. }
  108. .mania-100{
  109. color: #257aea;
  110. }
  111. .mania-50{
  112. color: #d2d2d2;
  113. }
  114. .osu-50{
  115. color: #ffbf00;
  116. }
  117. .mania-miss{
  118. color: #cc2626;
  119. }
  120. .mania-max, .mania-300, .mania-200, .mania-100, .mania-50, .mania-miss, .osu-300, .osu-100, .osu-50, .osu-miss{
  121. font-weight: 600;
  122. }
  123. .score-detail-data-text{
  124. font-weight: 500;
  125. }
  126. .osu-miss{
  127. display: inline-block;
  128. }
  129. .osu-miss > svg{
  130. width: 14px;
  131. height: 14px;
  132. top: 3px;
  133. position: relative;
  134. }
  135. div.bar__exp-info{
  136. position: relative;
  137. bottom: 100%;
  138. }
  139. `;
  140. let scriptContent =
  141. String.raw`console.log("page script injected from osu!web enhancement");
  142. let oldXHROpen = window.XMLHttpRequest.prototype.open;
  143. window.XMLHttpRequest.prototype.open = function() {
  144. this.addEventListener("load", function() {
  145. const url = this.responseURL;
  146. const trreg = /https:\/\/osu\.ppy\.sh\/users\/([0-9]+)\/extra-pages\/(top_ranks|historical)\?mode=(osu|taiko|fruits|mania)/.exec(url);
  147. const adreg = /https:\/\/osu\.ppy\.sh\/users\/([0-9]+)\/scores\/(firsts|best|recent|pinned)\?mode=(osu|taiko|fruits|mania)&limit=[0-9]*&offset=[0-9]*/.exec(url);
  148. let info;
  149. if(trreg) info = {
  150. type: trreg[2],
  151. userId: Number(trreg[1]),
  152. mode: trreg[3],
  153. }
  154. else{
  155. if(adreg) info = {
  156. type: adreg[2],
  157. userId: Number(adreg[1]),
  158. mode: adreg[3],
  159. }
  160. else return;
  161. }
  162. const responseBody = this.responseText;
  163. info.data = JSON.parse(responseBody);
  164. info.id = "osu!web enhancement";
  165. window.postMessage(info, "*");
  166. });
  167. return oldXHROpen.apply(this, arguments);
  168. };`;
  169. const scriptId = "osu-web-enhancement-XHR-script";
  170. if(!document.querySelector(`script#${scriptId}`)){
  171. const script = document.createElement("script");
  172. script.textContent = scriptContent;
  173. document.body.appendChild(script);
  174. }
  175. const HTML = (tagname, attrs, ...children) => {
  176. if(attrs === undefined) return document.createTextNode(tagname);
  177. const ele = document.createElement(tagname);
  178. if(attrs) for(let [key, value] of Object.entries(attrs)){
  179. if(key === "eventListener"){
  180. for(let listener of value){
  181. ele.addEventListener(listener.type, listener.listener, listener.options);
  182. }
  183. }
  184. else ele.setAttribute(key, value);
  185. }
  186. for(let child of children) ele.append(child);
  187. return ele;
  188. };
  189. const html = (html) => {
  190. const t = document.createElement("template");
  191. t.innerHTML = html;
  192. return t.content.firstElementChild;
  193. };
  194. const PostMessage = (msg) => { console.error(msg); };
  195. const OsuMod = {
  196. NoFail: 1 << 0,
  197. Easy: 1 << 1,
  198. TouchDevice: 1 << 2,
  199. NoVideo: 1 << 2,
  200. Hidden: 1 << 3,
  201. HardRock: 1 << 4,
  202. SuddenDeath: 1 << 5,
  203. DoubleTime: 1 << 6,
  204. Relax: 1 << 7,
  205. HalfTime: 1 << 8,
  206. Nightcore: 1 << 9, // always with DT
  207. Flashlight: 1 << 10,
  208. Autoplay: 1 << 11,
  209. SpunOut: 1 << 12,
  210. Autopilot: 1 << 13,
  211. Perfect: 1 << 14,
  212. Key4: 1 << 15,
  213. Key5: 1 << 16,
  214. Key6: 1 << 17,
  215. Key7: 1 << 18,
  216. Key8: 1 << 19,
  217. KeyMod: 1 << 19 | 1 << 18 | 1 << 17 | 1 << 16 | 1 << 15,
  218. FadeIn: 1 << 20,
  219. Random: 1 << 21,
  220. Cinema: 1 << 22,
  221. TargetPractice: 1 << 23,
  222. Key9: 1 << 24,
  223. Coop: 1 << 25,
  224. Key1: 1 << 26,
  225. Key3: 1 << 27,
  226. Key2: 1 << 28,
  227. ScoreV2: 1 << 29,
  228. Mirror: 1 << 30,
  229. };
  230. class Byte{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++]; } };
  231. class RankedStatus extends Byte{
  232. constructor(arr, iter){
  233. super(arr, iter);
  234. switch(this.value){
  235. case 1: this.description = "unsubmitted"; break;
  236. case 2: this.description = "pending/wip/graveyard"; break;
  237. case 3: this.description = "unused"; break;
  238. case 4: this.description = "ranked"; break;
  239. case 5: this.description = "approved"; break;
  240. case 6: this.description = "qualified"; break;
  241. case 7: this.description = "loved"; break;
  242. default: this.description = "unknown"; this.value = 0;
  243. }
  244. }
  245. };
  246. class OsuMode extends Byte{
  247. constructor(arr, iter){
  248. super(arr, iter);
  249. switch(this.value){
  250. case 1: this.description = "taiko"; break;
  251. case 2: this.description = "catch"; break;
  252. case 3: this.description = "mania"; break;
  253. default: this.value = 0; this.description = "osu";
  254. }
  255. }
  256. };
  257. class Grade extends Byte{
  258. constructor(arr, iter){
  259. super(arr, iter);
  260. switch(this.value){
  261. case 0: this.description = "SSH"; break;
  262. case 1: this.description = "SH"; break;
  263. case 2: this.description = "SS"; break;
  264. case 3: this.description = "S"; break;
  265. case 4: this.description = "A"; break;
  266. case 5: this.description = "B"; break;
  267. case 6: this.description = "C"; break;
  268. case 7: this.description = "D"; break;
  269. default: this.description = "not played";
  270. }
  271. }
  272. };
  273. class Short{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8; } };
  274. class Int{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8 | arr[iter.nxtpos++] << 16 | arr[iter.nxtpos++] << 24; } };
  275. class Long{ value = 0n; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 8).getBigUint64(0, true); iter.nxtpos += 8; } };
  276. class ULEB128{
  277. value = 0n;
  278. constructor(arr, iter){
  279. let shift = 0n;
  280. while(true){
  281. let peek = BigInt(arr[iter.nxtpos++]);
  282. this.value |= (peek & 0x7Fn) << shift;
  283. if((peek & 0x80n) === 0n) break;
  284. shift += 7n;
  285. }
  286. }
  287. };
  288. class Single{ value = 0; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 4).getFloat32(0, true); iter.nxtpos += 4; } };
  289. class Double{ value = 0; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 8).getFloat64(0, true); iter.nxtpos += 8; } };
  290. class Boolean{ value = false; constructor(arr, iter){ this.value = arr[iter.nxtpos++] !== 0x00; } };
  291. class OString{
  292. value = "";
  293. constructor(arr, iter){
  294. switch(arr[iter.nxtpos++]){
  295. case 0: break;
  296. case 0x0b:
  297. const l = new ULEB128(arr, iter).value;
  298. const bv = new Uint8Array(arr.buffer, iter.nxtpos, Number(l));
  299. this.value = new TextDecoder().decode(bv);
  300. iter.nxtpos += Number(l);
  301. break;
  302. default: console.assert(false, `error occurred while parsing osu string with the first byte.`);
  303. }
  304. }
  305. };
  306. class IntDouble{
  307. int = 0;
  308. double = 0;
  309. constructor(arr, iter){
  310. const m1 = arr[iter.nxtpos++];
  311. console.assert(m1 === 0x08, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  312. this.int = new Int(arr, iter).value;
  313. const m2 = arr[iter.nxtpos++];
  314. console.assert(m2 === 0x0d, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  315. this.double = new Double(arr, iter).value;
  316. }
  317. };
  318. class IntDoubleArray extends Array{
  319. constructor(arr, iter){
  320. super(new Int(arr, iter).value);
  321. for(let i = 0; i < this.length; i++) this[i] = new IntDouble(arr, iter);
  322. }
  323. };
  324. class TimingPoint{
  325. BPM = 0;
  326. offset = 0;
  327. notInherited = false;
  328. constructor(arr, iter){
  329. this.BPM = new Double(arr, iter).value;
  330. this.offset = new Double(arr, iter).value;
  331. this.notInherited = new Boolean(arr, iter).value;
  332. }
  333. };
  334. class TimingPointArray extends Array{
  335. constructor(arr, iter){
  336. super(new Int(arr, iter).value);
  337. for(let i = 0; i < this.length; i++) this[i] = new TimingPoint(arr, iter);
  338. }
  339. };
  340. class DateTime extends Long{};
  341. class Beatmap{
  342. constructor(arr, iter){
  343. if(iter.osuVersion < 20191106) this.bytes = new Int(arr, iter);
  344. this.artistName = new OString(arr, iter);
  345. this.artistNameUnicode = new OString(arr, iter);
  346. this.songTitle = new OString(arr, iter);
  347. this.songTitleUnicode = new OString(arr, iter);
  348. this.creatorName = new OString(arr, iter);
  349. this.difficultyName = new OString(arr, iter);
  350. this.audioFilename = new OString(arr, iter);
  351. this.MD5Hash = new OString(arr, iter);
  352. this.beatmapFilename = new OString(arr, iter);
  353. this.rankedStatus = new RankedStatus(arr, iter);
  354. this.hitcircleCount = new Short(arr, iter);
  355. this.sliderCount = new Short(arr, iter);
  356. this.spinnerCount = new Short(arr, iter);
  357. this.lastModified = new Long(arr, iter);
  358. this.AR = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  359. this.CS = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  360. this.HP = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  361. this.OD = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  362. this.sliderVelocity = new Double(arr, iter);
  363. if(iter.osuVersion >= 20140609) this.osuSRInfoArr = new IntDoubleArray(arr, iter);
  364. if(iter.osuVersion >= 20140609) this.taikoSRInfoArr = new IntDoubleArray(arr, iter);
  365. if(iter.osuVersion >= 20140609) this.catchSRInfoArr = new IntDoubleArray(arr, iter);
  366. if(iter.osuVersion >= 20140609) this.maniaSRInfoArr = new IntDoubleArray(arr, iter);
  367. this.drainTime = new Int(arr, iter);
  368. this.totalTime = new Int(arr, iter);
  369. this.audioPreviewTime = new Int(arr, iter);
  370. this.timingPointArr = new TimingPointArray(arr, iter);
  371. this.difficultyID = new Int(arr, iter);
  372. this.beatmapID = new Int(arr, iter);
  373. this.threadID = new Int(arr, iter);
  374. this.osuGrade = new Grade(arr, iter);
  375. this.taikoGrade = new Grade(arr, iter);
  376. this.catchGrade = new Grade(arr, iter);
  377. this.maniaGrade = new Grade(arr, iter);
  378. this.offsetLocal = new Short(arr, iter);
  379. this.stackLeniency = new Single(arr, iter);
  380. this.mode = new OsuMode(arr, iter);
  381. this.sourceStr = new OString(arr, iter);
  382. this.tagStr = new OString(arr, iter);
  383. this.offsetOnline = new Short(arr, iter);
  384. this.titleFont = new OString(arr, iter);
  385. this.unplayed = new Boolean(arr, iter);
  386. this.lastTimePlayed = new Long(arr, iter);
  387. this.isOsz2 = new Boolean(arr, iter);
  388. this.folderName = new OString(arr, iter);
  389. this.lastTimeChecked = new Long(arr, iter);
  390. this.ignoreBeatmapSound = new Boolean(arr, iter);
  391. this.ignoreBeatmapSkin = new Boolean(arr, iter);
  392. this.disableStoryboard = new Boolean(arr, iter);
  393. this.disableVideo = new Boolean(arr, iter);
  394. this.visualOverride = new Boolean(arr, iter);
  395. if(iter.osuVersion < 20140609) this.uselessShort = new Short(arr, iter);
  396. this.lastModified = new Int(arr, iter);
  397. this.scrollSpeedMania = new Byte(arr, iter);
  398. }
  399. };
  400. class BeatmapArray extends Array{
  401. constructor(arr, iter){
  402. super(new Int(arr, iter).value);
  403. for(let i = 0; i < this.length; i++) this[i] = new Beatmap(arr, iter);
  404. }
  405. };
  406. class OsuDb{
  407. constructor(arr, iter){
  408. this.version = new Int(arr, iter);
  409. iter.osuVersion = this.version.value;
  410. this.folderCount = new Int(arr, iter);
  411. this.accountUnlocked = new Boolean(arr, iter);
  412. this.timeTillUnlock = new DateTime(arr, iter);
  413. this.playerName = new OString(arr, iter);
  414. this.beatmapArray = new BeatmapArray(arr, iter);
  415. this.permission = new Int(arr, iter);
  416. }
  417. };
  418. const beatmapsets = new Set();
  419. const beatmaps = new Set();
  420. const bmsReg = /https:\/\/osu\.ppy\.sh\/beatmapsets\/([0-9]+)/;
  421. const bmReg = /https:\/\/osu\.ppy\.sh\/beatmapsets\/(?:[0-9]+)#(?:mania|osu|fruits|taiko)\/([0-9]+)/;
  422. const BeatmapsetRefresh = () => {
  423. for(const bm of window.osudb.beatmapArray){
  424. beatmaps.add(bm.difficultyID.value);
  425. beatmapsets.add(bm.beatmapID.value);
  426. }
  427. OnMutation();
  428. };
  429. const GetBestScores = async (id, mode) => {
  430. const querystr = new URLSearchParams({
  431. k: apiK,
  432. u: id,
  433. m: mode,
  434. limit: 100,
  435. type: "id",
  436. }).toString();
  437. const r = await fetch(`${guburl}?${querystr}`);
  438. return await r.json();
  439. };
  440. const NewOsuDb = (r) => {
  441. return new Promise((resolve, reject) => {
  442. const start = performance.now();
  443. const result = new Uint8Array(r.result);
  444. const length = result.length;
  445. console.log(`start reading osu!.db(${length} Bytes).`);
  446. const iter = {
  447. nxtpos: 0,
  448. };
  449. window.osudb = new OsuDb(result, iter);
  450. console.assert(iter.nxtpos === length, "there are still remaining unread bytes, something may be wrong. iter: %o", iter);
  451. console.log(`finished reading osu!.db in ${performance.now() - start} ms.`);
  452. resolve();
  453. })
  454. };
  455. const ReadOsuDb = (file) => {
  456. if(file.name !== "osu!.db"){ console.assert( false, "filename should be 'osu!.db'."); return; }
  457. const r = new FileReader();
  458. r.onload = () => {
  459. NewOsuDb(r);
  460. BeatmapsetRefresh();
  461. };
  462. r.onerror = () => console.assert(false, "error occurred while reading file.");
  463. r.readAsArrayBuffer(file);
  464. };
  465. const SelectOsuDb = (event) => {
  466. const t = event.target;
  467. const l = t.files;
  468. console.assert(l && l.length === 1, "No file or multiple files are selected.");
  469. ReadOsuDb(l[0]);
  470. };
  471. const PlaceSelectOsuDbButton = () => {
  472. if(document.querySelector(".osu-db-button")) return;
  473. const i = HTML("input", {type: "file", id: "osu-db-input", accept: ".db", eventListener: [{
  474. type: "change",
  475. listener: SelectOsuDb,
  476. options: false,
  477. }]});
  478. const d = HTML("div", {class: "osu-db-button nav2__col nav2__col--menu", eventListener: [{
  479. type: "click",
  480. listener: () => {if(i) i.click();},
  481. options: false,
  482. }]}, HTML("osu!.db"));
  483. document.body.appendChild(i);
  484. const a = document.querySelector("div.nav2__col.nav2__col--menu");
  485. a.parentElement.insertBefore(d, a);
  486. };
  487. const FilterBeatmapSet = () => {
  488. document.querySelectorAll(".beatmapsets__item").forEach((item) => {
  489. const bmsID = Number(bmsReg.exec(item.innerHTML)?.[1]);
  490. if(bmsID && beatmapsets.has(bmsID)){
  491. item.classList.add("owned-beatmapset");
  492. }
  493. });
  494. ShowBeatmapsSetInfo();
  495. };
  496. const ShowBeatmapsSetInfo = () => {
  497. const p = document.querySelector(".beatmaps-popup__group");
  498. if(!p) return;
  499. };
  500. const AdjustStyle = (modeId, sectionName) => {
  501. const styleSheetId = `userscript-generated-stylesheet-${sectionName}`;
  502. let e = document.getElementById(styleSheetId);
  503. if(!e){
  504. e = document.createElement("style");
  505. e.id = styleSheetId;
  506. document.head.appendChild(e);
  507. }
  508. const s = e.sheet;
  509. while(s.cssRules.length) s.deleteRule(0);
  510. const sectionSelector = `div.js-sortable--page[data-page-id="${sectionName}"]`;
  511. let ll;
  512. switch(modeId){
  513. case 3: ll = [".mania-300", ".mania-200", ".mania-100", ".mania-50", ".mania-miss"]; break;
  514. case 0: ll = [".osu-300", ".osu-100", ".osu-50", ".osu-miss"]; break;
  515. }
  516. ll.forEach((str) =>
  517. s.insertRule(
  518. `${sectionSelector} ${str} + .score-detail-data-text {
  519. width: ${[...document.querySelectorAll(`${sectionSelector} ${str} + .score-detail-data-text`)].reduce((max, ele) => ele.clientWidth > max ? ele.clientWidth : max, 0) + 2}px;
  520. }` ,0
  521. )
  522. );
  523. s.insertRule(
  524. `${sectionSelector} .play-detail__pp{
  525. width: ${[...document.querySelectorAll(`${sectionSelector} .play-detail__pp`)].reduce((max, ele) => ele.clientWidth > max ? ele.clientWidth : max, 0) + 1}px;
  526. }`
  527. ,0
  528. );
  529. };
  530. const TopRanksWorker = (items, tabId, sectionName = "top_ranks") => {
  531. if(!items.length) return true;
  532. const tabEle = document.querySelector(`div.js-sortable--page[data-page-id="${sectionName}"]`);
  533. if(!tabEle) return false;
  534. const listEle = tabEle.querySelectorAll(`.title.title--page-extra-small + div.play-detail-list.u-relative`)?.[tabId];
  535. if(!listEle) return false;
  536. let completed = true;
  537. for(const item of [...listEle.querySelectorAll(".play-detail.play-detail--highlightable")]){
  538. const a = item.children[0].children[1].children[0];
  539. const bm = bmReg.exec(a.href)[1];
  540. const data = items.find(item => item.beatmap_id === Number(bm));
  541. if(!data) completed = false;
  542. else ListItemWorker(item, data);
  543. }
  544. if(!completed) return false;
  545. AdjustStyle(items[0].ruleset_id, sectionName, tabId);
  546. return true;
  547. };
  548. const ListItemWorker = (ele, data) => {
  549. if(ele.classList.contains("improved")) return;
  550. ele.classList.add("improved");
  551. if(data.pp){
  552. const pptext = ele.querySelector(".play-detail__pp > span").childNodes[0];
  553. pptext.nodeValue = Number(data.pp).toPrecision(5);
  554. }
  555. const detail= ele.querySelector("div.play-detail__score-detail-top-right");
  556. const du = detail.children[0];
  557. if(!detail.children[1]) detail.append(HTML("div", {classList: "play-detail__pp-weight"}));
  558. const db = detail.children[1];
  559. data.statistics.perfect ??= 0, data.statistics.great ??= 0, data.statistics.good ??= 0, data.statistics.ok ??= 0, data.statistics.meh ??= 0, data.statistics.miss ??= 0;
  560. switch(data.ruleset_id){
  561. case 0:{
  562. du.replaceChildren(
  563. HTML("span", {class: "play-detail__accuracy"}, HTML(`V1Acc: ${(data.accuracy * 100).toFixed(2)}%`)),
  564. );
  565. const m_300 = HTML("span", {class: "score-detail score-detail-osu-300"},
  566. HTML("span", {class: "osu-300"},
  567. HTML("300")
  568. ),
  569. HTML("span", {class: "score-detail-data-text"},
  570. HTML(`${data.statistics.great + data.statistics.perfect}`)
  571. )
  572. );
  573. const s100 = HTML("span", {class: "score-detail score-detail-osu-100"},
  574. HTML("span", {class: "osu-100"},
  575. HTML("100")
  576. ),
  577. HTML("span", {class: "score-detail-data-text"},
  578. HTML(`${data.statistics.ok + data.statistics.good}`)
  579. )
  580. );
  581. const s50 = HTML("span", {class: "score-detail score-detail-osu-50"},
  582. HTML("span", {class: "osu-50"},
  583. HTML("50")
  584. ),
  585. HTML("span", {class: "score-detail-data-text"},
  586. HTML(`${data.statistics.meh}`)
  587. )
  588. );
  589. const s0 = HTML("span", {class: "score-detail score-detail-osu-miss"},
  590. HTML("span", {class: "osu-miss"},
  591. html(`<svg viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
  592. <filter id="blur">
  593. <feFlood flood-color="red" flood-opacity="0.5" in="SourceGraphic" />
  594. <feComposite operator="in" in2="SourceGraphic" />
  595. <feGaussianBlur stdDeviation="6" />
  596. <feComponentTransfer result="glow1">
  597. <feFuncA type="linear" slope="10" intercept="0" />
  598. </feComponentTransfer>
  599. <feGaussianBlur in="glow1" stdDeviation="1" result="glow2" />
  600. <feMerge>
  601. <feMergeNode in="SourceGraphic" />
  602. <feMergeNode in="glow2" />
  603. </feMerge>
  604. </filter>
  605. <filter id="blur2"> <feGaussianBlur stdDeviation="0.2"/> </filter>
  606. <path id="cross" d="M 26 16 l -10 10 l 38 38 l -38 38 l 10 10 l 38 -38 l 38 38 l 10 -10 l -38 -38 l 38 -38 l -10 -10 l -38 38 Z" />
  607. <use href="#cross" stroke="red" stroke-width="2" fill="transparent" filter="url(#blur)"/>
  608. <use href="#cross" fill="white" stroke="transparent" filter="url(#blur2)"/>
  609. </svg>`)
  610. ),
  611. HTML("span", {class: "score-detail-data-text"},
  612. HTML(`${data.statistics.miss}`)
  613. )
  614. );
  615. db.replaceChildren(m_300, s100, s50, s0);
  616. break;
  617. }
  618. case 1:{
  619. break;
  620. }
  621. case 2:{
  622. break;
  623. }
  624. case 3:{
  625. const v2acc = (320*data.statistics.perfect+300*data.statistics.great+200*data.statistics.good+100*data.statistics.ok+50*data.statistics.meh)/(320*(data.statistics.perfect+data.statistics.great+data.statistics.good+data.statistics.ok+data.statistics.meh+data.statistics.miss));
  626. du.replaceChildren(
  627. HTML("span", {class: "play-detail__accuracy"}, HTML(`V1Acc: ${(data.accuracy * 100).toFixed(2)}%`)),
  628. HTML("span", {class: "play-detail__accuracy ppAcc"}, HTML(`PPAcc: ${(v2acc * 100).toFixed(2)}%`)),
  629. );
  630. if(data.pp){
  631. const lostpp = data.pp * (0.2 / (Math.min(Math.max(v2acc, 0.8), 1) - 0.8) - 1);
  632. ele.children[1].children[2].appendChild(HTML("span", {class: "lost-pp"}, HTML(`-${lostpp.toPrecision(4)}`)));
  633. }
  634. const M_300 = Number(data.statistics.perfect) / Math.max(Number(data.statistics.great), 1);
  635. db.replaceChildren(
  636. HTML("span", {class: "score-detail score-detail-mania-max-300"},
  637. HTML("span", {class: "mania-max"}, HTML("M")),
  638. HTML("/"),
  639. HTML("span", {class: "mania-300"}, HTML("300")),
  640. HTML("span", {class: "score-detail-data-text"}, HTML(`${M_300.toFixed(2)}`))
  641. ),
  642. HTML("span", {class: "score-detail score-detail-mania-max-200"},
  643. HTML("span", {class: "mania-200"}, HTML("200")),
  644. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.good))
  645. ),
  646. HTML("span", {class: "score-detail score-detail-mania-max-100"},
  647. HTML("span", {class: "mania-100"}, HTML("100")),
  648. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.ok))
  649. ),
  650. HTML("span", {class: "score-detail score-detail-mania-max-50"},
  651. HTML("span", {class: "mania-50"}, HTML("50")),
  652. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.meh))
  653. ),
  654. HTML("span", {class: "score-detail score-detail-mania-max-0"},
  655. HTML("span", {class: "mania-miss"}, HTML("miss")),
  656. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss))
  657. )
  658. );
  659. break;
  660. }
  661. }
  662. }
  663. let lastLocationStr = "";
  664. let lastInitData;
  665. const OsuLevelToExp = (n) => {
  666. if(n <= 100) return 5000 / 3 * (4 * n ** 3 - 3 * n ** 2 - n) + 1.25 * 1.8 ** (n - 60);
  667. else return 26_931_190_827 + 99_999_999_999 * (n - 100);
  668. }
  669. const OsuExpValToStr = (num) => {
  670. let suffix = "";
  671. let exp = Math.log10(num);
  672. if(exp >= 12){
  673. return `${(num / 10 ** 12).toPrecision(4)}T`;
  674. }
  675. else if(exp >= 9){
  676. return `${(num / 10 ** 9).toPrecision(4)}B`;
  677. }
  678. else if(exp >= 6){
  679. return `${(num / 10 ** 6).toPrecision(4)}M`;
  680. }
  681. else if(exp >= 4){
  682. return `${(num / 10 ** 3).toPrecision(4)}K`;
  683. }
  684. else return `${num.toPrecision(4)}`;
  685. }
  686. const ImproveProfile = (message) => {
  687. let initData;
  688. if(window.location.toString() === lastLocationStr){
  689. initData = lastInitData;
  690. }
  691. else{
  692. initData = JSON.parse(document.querySelector(".js-react--profile-page.osu-layout.osu-layout--full").dataset.initialData);
  693. lastLocationStr = window.location.toString();
  694. lastInitData = initData;
  695. }
  696. const userId = initData.user.id;
  697. const modestr = initData.current_mode;
  698. if(!(userId === message.userId && modestr === message.mode)) return;
  699. const ttscore = initData.user.statistics.total_score;
  700. const lvl = initData.user.statistics.level.current;
  701. const upgradescore = Math.round(OsuLevelToExp(lvl + 1) - OsuLevelToExp(lvl));
  702. const lvlscore = ttscore - Math.round(OsuLevelToExp(lvl));
  703. document.querySelector("div.bar__text").textContent = `${OsuExpValToStr(lvlscore)}/${OsuExpValToStr(upgradescore)} (${(lvlscore/upgradescore * 100).toPrecision(3)}%)`;
  704. let ppDiv;
  705. document.querySelectorAll("div.value-display.value-display--plain").forEach((ele) => {
  706. if(ele.children[0].childNodes[0].nodeValue === "pp") ppDiv = ele;
  707. });
  708. ppDiv.children[1].children[0].childNodes[0].nodeValue = initData.user.statistics.pp;
  709. const obcb = () => {
  710. ob.disconnect();
  711. let result = true;
  712. switch(message.type){
  713. case "top_ranks":
  714. result &&= TopRanksWorker(message.data.pinned.items, 0);
  715. result &&= TopRanksWorker(message.data.best.items, 1);
  716. result &&= TopRanksWorker(message.data.firsts.items, 2);
  717. break;
  718. case "firsts":
  719. result &&= TopRanksWorker(message.data, 2);
  720. break;
  721. case "pinned":
  722. result &&= TopRanksWorker(message.data, 0);
  723. break;
  724. case "best":
  725. result &&= TopRanksWorker(message.data, 1);
  726. break;
  727. case "historical":
  728. result &&= TopRanksWorker(message.data.recent.items, 0, "historical");
  729. break;
  730. case "recent":
  731. result &&= TopRanksWorker(message.data, 0, "historical");
  732. break;
  733. }
  734. if(!result) ob.observe(document, {subtree: true, childList: true});
  735. };
  736. const ob = new MutationObserver(obcb);
  737. ob.observe(document, {subtree: true, childList: true});
  738. obcb();
  739. }
  740. let wloc = "";
  741. const WindowLocationChanged = () => {
  742. if(window.location !== wloc){
  743. wloc = window.location;
  744. return true;
  745. }
  746. else return false;
  747. }
  748. const InsertStyleSheet = () => {
  749. //const sheetId = "osu-web-enhancement-general-stylesheet";
  750. const s = new CSSStyleSheet();
  751. s.replaceSync(inj_style);
  752. document.adoptedStyleSheets = [...document.adoptedStyleSheets, s];
  753. }
  754. const OnBeatmapsetDownload = (message) => {
  755. beatmapsets.add(message.beatmapsetId);
  756. }
  757. const OnMutation = (mulist) => {
  758. mut.disconnect();
  759. PlaceSelectOsuDbButton();
  760. FilterBeatmapSet();
  761. mut.observe(document, {childList: true, subtree: true});
  762. };
  763. const MessageFilter = (message) => {
  764. switch(message.type){
  765. case "beatmapset_download_complete": OnBeatmapsetDownload(message); break;
  766. }
  767. }
  768. const WindowMessageFilter = (event) => {
  769. if(event.source === window && event?.data?.id === "osu!web enhancement"){
  770. ImproveProfile(event.data);
  771. }
  772. }
  773. window.addEventListener("message", WindowMessageFilter);
  774. const mut = new MutationObserver(OnMutation);
  775. mut.observe(document, {childList: true, subtree: true});
  776. InsertStyleSheet();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址