osu!web enhancement

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

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

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

QingJ © 2025

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