osu!web enhancement

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

当前为 2023-10-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name osu!web enhancement
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6.4
  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. // @match https://lazer.ppy.sh/*
  9. // @icon http://ppy.sh/favicon.ico
  10. // @grant none
  11. // @run-at document-end
  12. // ==/UserScript==
  13. const ShowPopup = (m, t = "info") => {
  14. window.popup(m, t);
  15. [["info", console.log], ["warning", console.warn], ["danger", console.error]].find(g => g[0] === t)[1](m);
  16. }
  17. const svg_osu_miss = URL.createObjectURL(new Blob(
  18. [`<svg viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
  19. <filter id="blur">
  20. <feFlood flood-color="red" flood-opacity="0.5" in="SourceGraphic" />
  21. <feComposite operator="in" in2="SourceGraphic" />
  22. <feGaussianBlur stdDeviation="6" />
  23. <feComponentTransfer result="glow1"> <feFuncA type="linear" slope="10" intercept="0" /> </feComponentTransfer>
  24. <feGaussianBlur in="glow1" stdDeviation="1" result="glow2" />
  25. <feMerge> <feMergeNode in="SourceGraphic" /> <feMergeNode in="glow2" /> </feMerge>
  26. </filter>
  27. <filter id="blur2"> <feGaussianBlur stdDeviation="0.2"/> </filter>
  28. <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" />
  29. <use href="#cross" stroke="red" stroke-width="2" fill="transparent" filter="url(#blur)"/>
  30. <use href="#cross" fill="white" stroke="transparent" filter="url(#blur2)"/>
  31. </svg>`], {type: "image/svg+xml"}));
  32. const svg_green_tick = URL.createObjectURL(new Blob([
  33. `<svg viewBox="0 0 18 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
  34. <polyline points="2,8 7,14 16,2" stroke="#62ee56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
  35. </svg>`], {type: "image/svg+xml"}));
  36. const inj_style =
  37. `#osu-db-input{
  38. display: none;
  39. }
  40. .osu-db-button{
  41. align-items: center;
  42. padding: 10px;
  43. }
  44. .osu-db-button:hover{
  45. cursor: pointer;
  46. }
  47. .beatmapsets__item.owned-beatmapset{
  48. opacity: 1.0;
  49. }
  50. .beatmapsets__item.owned-beatmapset .beatmapset-panel__menu-container{
  51. background-color: #87dda8;
  52. }
  53. .beatmapsets__item.owned-beatmapset .fas, .beatmapsets__item.owned-beatmapset .far{
  54. color: #5c9170;
  55. }
  56. .owned-beatmap-link{
  57. color: #87dda8;
  58. }
  59. .play-detail__accuracy{
  60. margin: 0px 12px;
  61. }
  62. .play-detail__accuracy.ppAcc{
  63. color: #8ef9f1;
  64. padding: 0;
  65. }
  66. .play-detail__weighted-pp{
  67. margin: 0px;
  68. }
  69. .play-detail__pp{
  70. flex-direction: column;
  71. }
  72. .lost-pp{
  73. font-size: 10px;
  74. position: relative;
  75. right: 7px;
  76. font-weight: 600;
  77. }
  78. .score-detail{
  79. display: inline-block;
  80. }
  81. .score-detail-data-text{
  82. margin-left: 5px;
  83. margin-right: 10px;
  84. width: auto;
  85. display: inline-block;
  86. }
  87. @keyframes rainbow{
  88. 0%{
  89. color: #be19ff;
  90. }
  91. 25%{
  92. color: #0075ff;
  93. }
  94. 50%{
  95. color: #4ddf86;
  96. }
  97. 75%{
  98. color: #e9ea00;
  99. }
  100. 100%{
  101. color: #ff7800;
  102. }
  103. }
  104. .play-detail__accuracy-and-weighted-pp{
  105. display: flex;
  106. flex-direction: row;
  107. }
  108. .play-detail__before{
  109. flex-grow: 1;
  110. }
  111. .mania-max{
  112. animation: 0.16s infinite alternate rainbow;
  113. }
  114. .mania-300{
  115. color: #fbff00;
  116. }
  117. .osu-100, .fruits-100, .taiko-150{
  118. color: #67ff5b;
  119. }
  120. .mania-200{
  121. color: #6cd800;
  122. }
  123. .osu-300, .fruits-300, .taiko-300{
  124. color: #7dfbff;
  125. }
  126. .mania-100{
  127. color: #257aea;
  128. }
  129. .mania-50{
  130. color: #d2d2d2;
  131. }
  132. .osu-50, .fruits-50-miss{
  133. color: #ffbf00;
  134. }
  135. .mania-miss, .taiko-miss, .fruits-miss{
  136. color: #cc2626;
  137. }
  138. .mania-max, .mania-300, .mania-200, .mania-100, .mania-50, .mania-miss, .osu-300, .osu-100, .osu-50, .osu-miss{
  139. font-weight: 600;
  140. }
  141. .score-detail-data-text{
  142. font-weight: 500;
  143. }
  144. .osu-miss{
  145. display: inline-block;
  146. }
  147. .osu-miss > img{
  148. width: 14px;
  149. height: 14px;
  150. bottom: 1px;
  151. position: relative;
  152. }
  153. .play-detail__Accuracy, .play-detail__Accuracy2, .combo, .max-combo, .play-detail__combo{
  154. display: inline-block;
  155. width: auto;
  156. }
  157. .play-detail__Accuracy{
  158. text-align: left;
  159. color: #fc2;
  160. }
  161. .play-detail__Accuracy2{
  162. text-align: left;
  163. color: rgb(142, 249, 241);
  164. }
  165. .play-detail__combo, .play-detail__Accuracy2, .play-detail__Accuracy{
  166. margin-right: 13px;
  167. }
  168. .play-detail__combo{
  169. text-align: right;
  170. }
  171. .combo, .max-combo{
  172. margin: 0px 1px;
  173. }
  174. .max-combo, .legacy-perfect-combo{
  175. color: hsl(var(--hsl-lime-1));
  176. }
  177. div.bar__exp-info{
  178. position: relative;
  179. bottom: 100%;
  180. }
  181. .play-detail__group--background, .beatmap-playcount__background{
  182. position: absolute;
  183. width: 90%;
  184. height: 100%;
  185. left: 0px;
  186. margin: 0px;
  187. pointer-events: none;
  188. z-index: 1;
  189. border-radius: 10px 0px 0px 10px;
  190. background-size: cover;
  191. background-position-y: -100%;
  192. mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));
  193. -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));
  194. }
  195. @media(max-width: 900px){
  196. .play-detail__group--background, .beatmap-playcount__background{
  197. background-position-y: 0%;
  198. mask-image: linear-gradient(to bottom, #0007, #0004);
  199. -webkit-mask-image: linear-gradient(to bottom, #0007, #0004);
  200. width: 100%;
  201. }
  202. .lost-pp{
  203. left: 3px;
  204. }
  205. .play-detail__group.play-detail__group--bottom{
  206. z-index: 1;
  207. }
  208. .play-detail__before{
  209. flex-grow: 0;
  210. }
  211. }
  212. .play-detail.play-detail--highlightable.play-detail--pin-sortable.js-score-pin-sortable .play-detail__group--background{
  213. left: 20px;
  214. }
  215. .beatmap-playcount__background{
  216. width: 100%;
  217. border-radius: 6px;
  218. mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
  219. -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
  220. }
  221. .beatmap-playcount__info, .beatmap-playcount__detail-count, .play-detail__group.play-detail__group--top *{
  222. z-index: 1;
  223. }
  224. div.play-detail-list time.js-timeago, span.beatmap-playcount__mapper, span.beatmap-playcount__mapper > a{
  225. color: #ccc;
  226. }
  227. button.show-more-link{
  228. z-index: 4;
  229. }
  230. a.beatmap-download-link{
  231. margin: 0px 5px;
  232. color: hsl(var(--hsl-l1));
  233. }
  234. a.beatmap-download-link:hover, a.beatmap-pack-item-download-link span:hover{
  235. color: #fff;
  236. }
  237. a.beatmap-pack-item-download-link span{
  238. color: hsl(var(--hsl-l1));
  239. }
  240. .play-detail.play-detail--highlightable.audio-player{
  241. max-width: none;
  242. height: unset;
  243. padding: unset;
  244. align-items: unset;
  245. }
  246. .play-detail.play-detail--highlightable.audio-player__button{
  247. align-items: unset;
  248. padding: unset;
  249. }
  250. .play-detail.play-detail--highlightable.audio-player__button:hover{
  251. color: unset;
  252. }
  253. `;
  254. let scriptContent =
  255. String.raw`console.log("page script injected from osu!web enhancement");
  256. if(window.oldXHROpen === undefined){
  257. window.oldXHROpen = window.XMLHttpRequest.prototype.open;
  258. window.XMLHttpRequest.prototype.open = function() {
  259. this.addEventListener("load", function() {
  260. const url = this.responseURL;
  261. const trreg = /https:\/\/(?<subdomain>osu|lazer)\.ppy\.sh\/users\/(?<id>[0-9]+)\/extra-pages\/(?<type>top_ranks|historical)\?mode=(?<mode>osu|taiko|fruits|mania)/.exec(url);
  262. const adreg = /https:\/\/(?<subdomain>osu|lazer)\.ppy\.sh\/users\/(?<id>[0-9]+)\/scores\/(?<type>firsts|best|recent|pinned)\?mode=(?<mode>osu|taiko|fruits|mania)&limit=[0-9]*&offset=[0-9]*/.exec(url);
  263. let reg = trreg ?? (adreg ?? null);
  264. if(!reg){
  265. const bmsreg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/search\?/;
  266. return;
  267. }
  268. let info = {
  269. type: reg.groups.type,
  270. userId: Number(reg.groups.id),
  271. mode: reg.groups.mode,
  272. subdomain: reg.groups.subdomain,
  273. };
  274. const responseBody = this.responseText;
  275. info.data = JSON.parse(responseBody);
  276. info.id = "osu!web enhancement";
  277. window.postMessage(info, "*");
  278. });
  279. return oldXHROpen.apply(this, arguments);
  280. };
  281. }`;
  282. const scriptId = "osu-web-enhancement-XHR-script";
  283. if(!document.querySelector(`script#${scriptId}`)){
  284. const script = document.createElement("script");
  285. script.textContent = scriptContent;
  286. document.body.appendChild(script);
  287. }
  288. const HTML = (tagname, attrs, ...children) => {
  289. if(attrs === undefined) return document.createTextNode(tagname);
  290. const ele = document.createElement(tagname);
  291. if(attrs) for(let [key, value] of Object.entries(attrs)){
  292. if(key === "eventListener"){
  293. for(let listener of value){
  294. ele.addEventListener(listener.type, listener.listener, listener.options);
  295. }
  296. }
  297. else ele.setAttribute(key, value);
  298. }
  299. for(let child of children) if(child) ele.append(child);
  300. return ele;
  301. };
  302. const html = (html) => {
  303. const t = document.createElement("template");
  304. t.innerHTML = html;
  305. return t.content.firstElementChild;
  306. };
  307. const PostMessage = (msg) => { console.error(msg); };
  308. const OsuMod = {
  309. NoFail: 1 << 0,
  310. Easy: 1 << 1,
  311. TouchDevice: 1 << 2,
  312. NoVideo: 1 << 2,
  313. Hidden: 1 << 3,
  314. HardRock: 1 << 4,
  315. SuddenDeath: 1 << 5,
  316. DoubleTime: 1 << 6,
  317. Relax: 1 << 7,
  318. HalfTime: 1 << 8,
  319. Nightcore: 1 << 9, // always with DT
  320. Flashlight: 1 << 10,
  321. Autoplay: 1 << 11,
  322. SpunOut: 1 << 12,
  323. Autopilot: 1 << 13,
  324. Perfect: 1 << 14,
  325. Key4: 1 << 15,
  326. Key5: 1 << 16,
  327. Key6: 1 << 17,
  328. Key7: 1 << 18,
  329. Key8: 1 << 19,
  330. KeyMod: 1 << 19 | 1 << 18 | 1 << 17 | 1 << 16 | 1 << 15,
  331. FadeIn: 1 << 20,
  332. Random: 1 << 21,
  333. Cinema: 1 << 22,
  334. TargetPractice: 1 << 23,
  335. Key9: 1 << 24,
  336. Coop: 1 << 25,
  337. Key1: 1 << 26,
  338. Key3: 1 << 27,
  339. Key2: 1 << 28,
  340. ScoreV2: 1 << 29,
  341. Mirror: 1 << 30,
  342. };
  343. class Byte{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++]; } };
  344. class RankedStatus extends Byte{
  345. constructor(arr, iter){
  346. super(arr, iter);
  347. switch(this.value){
  348. case 1: this.description = "unsubmitted"; break;
  349. case 2: this.description = "pending/wip/graveyard"; break;
  350. case 3: this.description = "unused"; break;
  351. case 4: this.description = "ranked"; break;
  352. case 5: this.description = "approved"; break;
  353. case 6: this.description = "qualified"; break;
  354. case 7: this.description = "loved"; break;
  355. default: this.description = "unknown"; this.value = 0;
  356. }
  357. }
  358. };
  359. class OsuMode extends Byte{
  360. constructor(arr, iter){
  361. super(arr, iter);
  362. switch(this.value){
  363. case 1: this.description = "taiko"; break;
  364. case 2: this.description = "catch"; break;
  365. case 3: this.description = "mania"; break;
  366. default: this.value = 0; this.description = "osu";
  367. }
  368. }
  369. };
  370. class Grade extends Byte{
  371. constructor(arr, iter){
  372. super(arr, iter);
  373. switch(this.value){
  374. case 0: this.description = "SSH"; break;
  375. case 1: this.description = "SH"; break;
  376. case 2: this.description = "SS"; break;
  377. case 3: this.description = "S"; break;
  378. case 4: this.description = "A"; break;
  379. case 5: this.description = "B"; break;
  380. case 6: this.description = "C"; break;
  381. case 7: this.description = "D"; break;
  382. default: this.description = "not played";
  383. }
  384. }
  385. };
  386. class Short{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8; } };
  387. class Int{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8 | arr[iter.nxtpos++] << 16 | arr[iter.nxtpos++] << 24; } };
  388. class Long{ value = 0n; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 8).getBigUint64(0, true); iter.nxtpos += 8; } };
  389. class ULEB128{
  390. value = 0n;
  391. constructor(arr, iter){
  392. let shift = 0n;
  393. while(true){
  394. let peek = BigInt(arr[iter.nxtpos++]);
  395. this.value |= (peek & 0x7Fn) << shift;
  396. if((peek & 0x80n) === 0n) break;
  397. shift += 7n;
  398. }
  399. }
  400. };
  401. class Single{ value = 0; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 4).getFloat32(0, true); iter.nxtpos += 4; } };
  402. class Double{ value = 0; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 8).getFloat64(0, true); iter.nxtpos += 8; } };
  403. class Boolean{ value = false; constructor(arr, iter){ this.value = arr[iter.nxtpos++] !== 0x00; } };
  404. class OString{
  405. value = "";
  406. constructor(arr, iter){
  407. switch(arr[iter.nxtpos++]){
  408. case 0: break;
  409. case 0x0b: {
  410. const l = new ULEB128(arr, iter).value;
  411. const bv = new Uint8Array(arr.buffer, iter.nxtpos, Number(l));
  412. this.value = new TextDecoder().decode(bv);
  413. iter.nxtpos += Number(l);
  414. break;
  415. }
  416. default: console.assert(false, `error occurred while parsing osu string with the first byte.`);
  417. }
  418. }
  419. };
  420. class IntDouble{
  421. int = 0;
  422. double = 0;
  423. constructor(arr, iter){
  424. const m1 = arr[iter.nxtpos++];
  425. console.assert(m1 === 0x08, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  426. this.int = new Int(arr, iter).value;
  427. const m2 = arr[iter.nxtpos++];
  428. console.assert(m2 === 0x0d, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  429. this.double = new Double(arr, iter).value;
  430. }
  431. };
  432. class IntDoubleArray extends Array{
  433. constructor(arr, iter){
  434. super(new Int(arr, iter).value);
  435. for(let i = 0; i < this.length; i++) this[i] = new IntDouble(arr, iter);
  436. }
  437. };
  438. class TimingPoint{
  439. BPM = 0;
  440. offset = 0;
  441. notInherited = false;
  442. constructor(arr, iter){
  443. this.BPM = new Double(arr, iter).value;
  444. this.offset = new Double(arr, iter).value;
  445. this.notInherited = new Boolean(arr, iter).value;
  446. }
  447. };
  448. class TimingPointArray extends Array{
  449. constructor(arr, iter){
  450. super(new Int(arr, iter).value);
  451. for(let i = 0; i < this.length; i++) this[i] = new TimingPoint(arr, iter);
  452. }
  453. };
  454. class DateTime extends Long{};
  455. class Beatmap{
  456. constructor(arr, iter){
  457. if(iter.osuVersion < 20191106) this.bytes = new Int(arr, iter);
  458. this.artistName = new OString(arr, iter);
  459. this.artistNameUnicode = new OString(arr, iter);
  460. this.songTitle = new OString(arr, iter);
  461. this.songTitleUnicode = new OString(arr, iter);
  462. this.creatorName = new OString(arr, iter);
  463. this.difficultyName = new OString(arr, iter);
  464. this.audioFilename = new OString(arr, iter);
  465. this.MD5Hash = new OString(arr, iter);
  466. this.beatmapFilename = new OString(arr, iter);
  467. this.rankedStatus = new RankedStatus(arr, iter);
  468. this.hitcircleCount = new Short(arr, iter);
  469. this.sliderCount = new Short(arr, iter);
  470. this.spinnerCount = new Short(arr, iter);
  471. this.lastModified = new Long(arr, iter);
  472. this.AR = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  473. this.CS = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  474. this.HP = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  475. this.OD = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  476. this.sliderVelocity = new Double(arr, iter);
  477. if(iter.osuVersion >= 20140609) this.osuSRInfoArr = new IntDoubleArray(arr, iter);
  478. if(iter.osuVersion >= 20140609) this.taikoSRInfoArr = new IntDoubleArray(arr, iter);
  479. if(iter.osuVersion >= 20140609) this.catchSRInfoArr = new IntDoubleArray(arr, iter);
  480. if(iter.osuVersion >= 20140609) this.maniaSRInfoArr = new IntDoubleArray(arr, iter);
  481. this.drainTime = new Int(arr, iter);
  482. this.totalTime = new Int(arr, iter);
  483. this.audioPreviewTime = new Int(arr, iter);
  484. this.timingPointArr = new TimingPointArray(arr, iter);
  485. this.difficultyID = new Int(arr, iter);
  486. this.beatmapID = new Int(arr, iter);
  487. this.threadID = new Int(arr, iter);
  488. this.osuGrade = new Grade(arr, iter);
  489. this.taikoGrade = new Grade(arr, iter);
  490. this.catchGrade = new Grade(arr, iter);
  491. this.maniaGrade = new Grade(arr, iter);
  492. this.offsetLocal = new Short(arr, iter);
  493. this.stackLeniency = new Single(arr, iter);
  494. this.mode = new OsuMode(arr, iter);
  495. this.sourceStr = new OString(arr, iter);
  496. this.tagStr = new OString(arr, iter);
  497. this.offsetOnline = new Short(arr, iter);
  498. this.titleFont = new OString(arr, iter);
  499. this.unplayed = new Boolean(arr, iter);
  500. this.lastTimePlayed = new Long(arr, iter);
  501. this.isOsz2 = new Boolean(arr, iter);
  502. this.folderName = new OString(arr, iter);
  503. this.lastTimeChecked = new Long(arr, iter);
  504. this.ignoreBeatmapSound = new Boolean(arr, iter);
  505. this.ignoreBeatmapSkin = new Boolean(arr, iter);
  506. this.disableStoryboard = new Boolean(arr, iter);
  507. this.disableVideo = new Boolean(arr, iter);
  508. this.visualOverride = new Boolean(arr, iter);
  509. if(iter.osuVersion < 20140609) this.uselessShort = new Short(arr, iter);
  510. this.lastModified = new Int(arr, iter);
  511. this.scrollSpeedMania = new Byte(arr, iter);
  512. }
  513. };
  514. class BeatmapArray extends Array{
  515. constructor(arr, iter){
  516. super(new Int(arr, iter).value);
  517. for(let i = 0; i < this.length; i++) this[i] = new Beatmap(arr, iter);
  518. }
  519. };
  520. class OsuDb{
  521. constructor(arr, iter){
  522. this.version = new Int(arr, iter);
  523. iter.osuVersion = this.version.value;
  524. this.folderCount = new Int(arr, iter);
  525. this.accountUnlocked = new Boolean(arr, iter);
  526. this.timeTillUnlock = new DateTime(arr, iter);
  527. this.playerName = new OString(arr, iter);
  528. this.beatmapArray = new BeatmapArray(arr, iter);
  529. this.permission = new Int(arr, iter);
  530. }
  531. };
  532. const beatmapsets = new Set();
  533. const beatmaps = new Set();
  534. const bmsReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/([0-9]+)/;
  535. const bmsdlReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/([0-9]+)\/download/;
  536. const bmReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/(?:[0-9]+)#(?:mania|osu|fruits|taiko)\/([0-9]+)/;
  537. const BeatmapsetRefresh = () => {
  538. for(const bm of window.osudb.beatmapArray){
  539. beatmaps.add(bm.difficultyID.value);
  540. beatmapsets.add(bm.beatmapID.value);
  541. }
  542. OnMutation();
  543. };
  544. const NewOsuDb = (r) => {
  545. return new Promise((resolve, reject) => {
  546. const start = performance.now();
  547. const result = new Uint8Array(r.result);
  548. const length = result.length;
  549. console.log(`start reading osu!.db(${length} Bytes).`);
  550. const iter = {
  551. nxtpos: 0,
  552. };
  553. window.osudb = new OsuDb(result, iter);
  554. if(iter.nxtpos !== length) ShowPopup("There are still remaining unread bytes, something may be wrong.", "danger");
  555. ShowPopup(`Finished reading osu!.db in ${performance.now() - start} ms.`);
  556. resolve();
  557. })
  558. };
  559. const ReadOsuDb = (file) => {
  560. if(file.name !== "osu!.db"){ console.assert( false, "filename should be 'osu!.db'."); return; }
  561. const r = new FileReader();
  562. r.onload = () => {
  563. NewOsuDb(r);
  564. BeatmapsetRefresh();
  565. };
  566. r.onerror = () => console.assert(false, "error occurred while reading file.");
  567. r.readAsArrayBuffer(file);
  568. };
  569. const SelectOsuDb = (event) => {
  570. const t = event.target;
  571. const l = t.files;
  572. console.assert(l && l.length === 1, "No file or multiple files are selected.");
  573. ReadOsuDb(l[0]);
  574. };
  575. const CheckForUpdate = () => {
  576. const verReg = /<dd class="script-show-version"><span>([0-9\.]+)<\/span><\/dd>/;
  577. fetch("https://gf.qytechs.cn/en/scripts/475417-osu-web-enhancement", {
  578. credentials: "omit"
  579. }).then(response => response.text()).then((html) => {
  580. const ver = verReg.exec(html);
  581. if(ver){
  582. const result = (() => {
  583. const verList = ver[1].split(".");
  584. const thisVer = GM_info.script.version;
  585. console.log(`latest version is: ${ver[1]}, current version is: ${thisVer}`);
  586. const thisVerList = thisVer.split(".");
  587. for(let i = 0; i < verList.length; i++){
  588. if(Number(verList[i]) > Number(thisVerList[i] ?? 0)) return true;
  589. else if(Number(verList[i]) < Number(thisVerList[i] ?? 0)) return false;
  590. }
  591. return false;
  592. })();
  593. if(result){
  594. const a = HTML("a", {href: "https://gf.qytechs.cn/scripts/475417-osu-web-enhancement/code/osu!web%20enhancement.user.js", download: "", style: "display:none;"});
  595. a.click();
  596. }
  597. else{
  598. ShowPopup("The lastest version is already installed!")
  599. }
  600. }
  601. });
  602. };
  603. const AddMenu = () => {
  604. const menuId = "osu-web-enhancement-toolbar";
  605. if(!window.menuEventListener){
  606. window.addEventListener("click", (ev) => {
  607. const fid = ev.target?.dataset?.functionId;
  608. if(fid) switch(fid){
  609. case "import-osu-db-button": document.getElementById("osu-db-input")?.click(); break;
  610. case "check-for-update-button": CheckForUpdate(); break;
  611. case "pp-gini-index-calculator": PPGiniIndex(); break;
  612. }
  613. });
  614. window.menuEventListener = true;
  615. }
  616. if(document.getElementById(menuId)) return;
  617. const anc = document.querySelector("div.nav2__col.nav2__col--menu.js-react--quick-search-button");
  618. const i = HTML("input", {type: "file", id: "osu-db-input", accept: ".db", eventListener: [{
  619. type: "change",
  620. listener: SelectOsuDb,
  621. options: false,
  622. }]});
  623. const menuClass = "simple-menu simple-menu--nav2 simple-menu--nav2-left-aligned simple-menu--nav2-transparent js-menu";
  624. const menuItemClass = "simple-menu__item u-section-community--before-bg-normal";
  625. const menuTgtId = "osu-web-enhancement";
  626. anc.insertAdjacentElement("beforebegin",
  627. HTML("div", {class: "nav2__col nav2__col--menu", id: menuId},
  628. HTML("div", {class: "nav2__menu-link-main js-menu", "data-menu-target": `nav2-menu-popup-${menuTgtId}`, "data-menu-show-delay":"0", style:"flex-direction: column; cursor: default;"},
  629. HTML("span", {style: "flex-grow: 1;"}),
  630. HTML("span", {style: "font-size: 10px;"}, HTML("osu!web")),
  631. HTML("span", {style: "font-size: 10px;"}, HTML("enhancement")),
  632. HTML("span", {style: "flex-grow: 1;"}),
  633. ),
  634. HTML("div", {class: "nav2__menu-popup"},
  635. HTML("div", {class: `${menuClass}`, "data-menu-id": `nav2-menu-popup-${menuTgtId}`, "data-visibility": "hidden"},
  636. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", "data-function-id": "import-osu-db-button", }, HTML("Import osu!.db")),
  637. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", "data-function-id": "check-for-update-button"}, HTML("Check for update")),
  638. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", "data-function-id": "pp-gini-index-calculator"}, HTML("Calculate pp Gini index")),
  639. HTML("a", {class: `${menuItemClass}`, style: "cursor: pointer;", href: "https://gf.qytechs.cn/en/scripts/475417-osu-web-enhancement", target: "_blank"}, HTML("Go to GreasyFork page"))
  640. ),
  641. )
  642. )
  643. );
  644. const mobMenuItmCls = "navbar-mobile-item__submenu-item js-click-menu--close";
  645. const mob = document.querySelector(`div.mobile-menu__item.js-click-menu[data-click-menu-id="mobile-nav"]`);
  646. mob.insertAdjacentElement("beforeend",
  647. HTML("div", {class: "navbar-mobile-item"},
  648. HTML("div", {class: "navbar-mobile-item__main js-click-menu", "data-click-menu-target": `nav-mobile-${menuTgtId}`, style: "cursor: pointer;"},
  649. HTML("span", {class: "navbar-mobile-item__icon navbar-mobile-item__icon--closed"},
  650. HTML("i", {class: "fas fa-chevron-right"})
  651. ),
  652. HTML("span", {class: "navbar-mobile-item__icon navbar-mobile-item__icon--opened"},
  653. HTML("i", {class: "fas fa-chevron-down"})
  654. ),
  655. HTML("osu!web enhancement"),
  656. ),
  657. HTML("ul", {class: "navbar-mobile-item__submenu js-click-menu", "data-click-menu-id": `nav-mobile-${menuTgtId}`, "data-visibility": "hidden"},
  658. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", "data-function-id": "import-osu-db-button",}, HTML("Import osu!.db"))),
  659. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", "data-function-id": "check-for-update-button"}, HTML("Check for update"))),
  660. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", "data-function-id": "pp-gini-index-calculator"}, HTML("Calculate pp Gini index"))),
  661. HTML("a", {class: `${mobMenuItmCls}`, style: "cursor: pointer;", href: "https://gf.qytechs.cn/en/scripts/475417-osu-web-enhancement", target: "_blank"}, HTML("Go to GreasyFork page"))
  662. )
  663. )
  664. );
  665. document.body.appendChild(i);
  666. };
  667. const FilterBeatmapSet = () => {
  668. document.querySelectorAll(".beatmapsets__item").forEach((item) => {
  669. const bmsID = Number(bmsReg.exec(item.innerHTML)?.[1]);
  670. if(bmsID && beatmapsets.has(bmsID)){
  671. item.classList.add("owned-beatmapset");
  672. }
  673. });
  674. document.querySelectorAll("div.bbcode a, a.osu-md__link").forEach(item => {
  675. if(item.classList.contains("owned-beatmap-link") || item.classList.contains("beatmap-download-link")) return;
  676. const e = bmsReg.exec(item.href);
  677. if(e && beatmapsets.has(Number(e[1]))){
  678. item.classList.add("owned-beatmap-link");
  679. if(item.nextElementSibling?.classList?.contains("beatmap-download-link")) item.nextElementSibling.remove();
  680. const box = item.getBoundingClientRect();
  681. const size = Math.round(box.height / 16 * 14);
  682. const vert = Math.round(size * 4 / 14) / 2;
  683. item.after(HTML("img", {src: svg_green_tick, title: "Owned", alt: "owned beatmap", style: `margin: 0px 5px; width: ${size}px; height: ${size}px; vertical-align: -${vert}px;`}));
  684. }else if(e && !item.nextElementSibling?.classList?.contains("beatmap-download-link")){
  685. item.after(
  686. HTML("a", {class: "beatmap-download-link", href: `https://osu.ppy.sh/beatmapsets/${e[1]}/download`, download: ""},
  687. HTML("span", {class: "fas fa-file-download", title: "Download"})
  688. )
  689. );
  690. }
  691. });
  692. document.querySelectorAll("li.beatmap-pack-items__set").forEach(item => {
  693. if(item.classList.contains("owned-beatmap-pack-item")) return;
  694. const a = item.querySelector("a.beatmap-pack-items__link");
  695. const e = bmsReg.exec(a.href);
  696. if(e && beatmapsets.has(Number(e[1]))){
  697. item.classList.add("owned-beatmap-pack-item");
  698. const span = item.querySelector("span.fal");
  699. span.setAttribute("title", "Owned");
  700. span.dataset.origTitle = "owned";
  701. span.setAttribute("class", "");
  702. span.append(HTML("img", {src: svg_green_tick, alt: "owned beatmap", style: `width: 16px; height: 16px; vertical-align: -2px;`}));
  703. const parent = item.querySelector(".beatmap-pack-item-download-link");
  704. if(parent){
  705. console.assert(parent.parentElement === item, "unexpected error occurred!");
  706. item.insertBefore(span, parent);
  707. parent.remove();
  708. }
  709. }else if(e){
  710. const icon = item.querySelector(".beatmap-pack-items__icon");
  711. icon.setAttribute("title", "Download");
  712. icon.setAttribute("class", "fas fa-file-download beatmap-pack-items__icon");
  713. if(icon.parentElement === item){
  714. const dl = HTML("a", {class: "beatmap-pack-item-download-link", href: `https://osu.ppy.sh/beatmapsets/${e[1]}/download`, download: ""});
  715. item.insertBefore(dl, icon);
  716. dl.append(icon);
  717. }
  718. }
  719. })
  720. };
  721. const AdjustStyle = (modestr, sectionName) => {
  722. const styleSheetId = `userscript-generated-stylesheet-${sectionName}`;
  723. let e = document.getElementById(styleSheetId);
  724. if(!e){
  725. e = document.createElement("style");
  726. e.id = styleSheetId;
  727. document.head.appendChild(e);
  728. }
  729. const s = e.sheet;
  730. while(s.cssRules.length) s.deleteRule(0);
  731. const sectionSelector = `div.js-sortable--page[data-page-id="${sectionName}"]`;
  732. let ll = [];
  733. switch(modestr){
  734. case "mania": ll = [".mania-300", ".mania-200", ".mania-100", ".mania-50", ".mania-miss"]; break;
  735. case "fruits": ll = [".fruits-300", ".fruits-100", ".fruits-50-miss", ".fruits-miss"]; break;
  736. case "taiko": ll = [".taiko-300", ".taiko-150", ".taiko-miss"]; break;
  737. case "osu": ll = [".osu-300", ".osu-100", ".osu-50", ".osu-miss"]; break;
  738. }
  739. class FasterCalc{
  740. _map = new Map();
  741. Calculate = (ele) => {
  742. const t = ele.textContent;
  743. let w = 0, changed = false;
  744. for(const c of t){
  745. let wc = this._map.get(c);
  746. if(!wc){
  747. if(!changed) changed = ele.cloneNode(true);
  748. ele.textContent = c;
  749. wc = ele.clientWidth;
  750. this._map.set(c, wc);
  751. }
  752. w += wc;
  753. }
  754. if(changed){
  755. ele.insertAdjacentElement("afterend", changed);
  756. ele.remove();
  757. }
  758. return w;
  759. };
  760. };
  761. let past = performance.now(), curr;
  762. let fc = new FasterCalc();
  763. ll.forEach((str) =>
  764. s.insertRule(
  765. `${sectionSelector} ${str} + .score-detail-data-text {
  766. width: ${[...document.querySelectorAll(`${sectionSelector} ${str} + .score-detail-data-text`)].reduce((max, ele) => { const w = fc.Calculate(ele); return w > max ? w : max }, 0) + 2}px;
  767. }` ,0
  768. )
  769. );
  770. curr = performance.now();
  771. console.log(`AdjustStyle Stage 1: ${curr - past}ms`);
  772. past = performance.now();
  773. fc = new FasterCalc();
  774. [".play-detail__combo", ".play-detail__Accuracy", ".play-detail__Accuracy2"].forEach((str) =>
  775. s.insertRule(
  776. `${sectionSelector} ${str}{
  777. min-width: ${Math.ceil([...document.querySelectorAll(`${sectionSelector} ${str}`)].reduce((max, ele) => {const w = fc.Calculate(ele); return w > max ? w : max;}, 0)) + 1}px;
  778. }`
  779. ,0
  780. )
  781. );
  782. curr = performance.now();
  783. console.log(`AdjustStyle Stage 2: ${curr - past}ms`);
  784. past = performance.now();
  785. [".play-detail__pp"].forEach((str) =>
  786. s.insertRule(
  787. `${sectionSelector} ${str}{
  788. min-width: ${Math.ceil([...document.querySelectorAll(`${sectionSelector} ${str}`)].reduce((max, ele) => {const w = ele.clientWidth; return w > max ? w : max;}, 0)) + 1}px;
  789. }`
  790. ,0
  791. )
  792. );
  793. curr = performance.now();
  794. console.log(`AdjustStyle Stage 3: ${curr - past}ms`);
  795. past = performance.now();
  796. };
  797. const PPGiniIndex = () => {
  798. let vals = [...document.querySelectorAll(`div.js-sortable--page[data-page-id="top_ranks"] div.play-detail-list:nth-child(4) div.play-detail.play-detail--highlightable`)]
  799. .map((ele) => {const ppele = ele.querySelector("div.play-detail__pp span"); return Number((ppele.title ? ppele.title : ppele.dataset.origTitle).replaceAll(",", ""))})
  800. .sort((a, b) => b - a);
  801. if(vals.length === 0) ShowPopup("Could not find best play data", "danger");
  802. const min = vals[vals.length - 1];
  803. let _ = 0; for(let i = vals.length - 1; i >= 0; i--) {
  804. _ += vals[i] - min;
  805. vals[i] = _;
  806. }
  807. const SB = vals.reduce((sum, val) => sum + val, -(vals[0] / 2));
  808. const SAB = vals[0] / 2 * vals.length;
  809. ShowPopup(`Your pp Gini index of bp${vals.length} is ${(1 - SB/SAB).toPrecision(6)}.`);
  810. }
  811. const TopRanksWorker = (userId, modestr, addedNodes = [document.body]) => {
  812. const isLazer = window.location.hostname.split(".")[0] === "lazer"; // assume that hostname can only be osu.ppy.sh or lazer.ppy.sh
  813. const subdomain = isLazer ? "lazer": "osu";
  814. let sectionNames = new Set();
  815. const GetSection = (ele) => {
  816. let count = 0;
  817. while(ele){
  818. if(ele.tagName === "DIV" && ele.className === "js-sortable--page") return ele.dataset.pageId;
  819. ele = ele.parentElement;
  820. count++;
  821. if(count > 50) console.log(ele);
  822. }
  823. };
  824. addedNodes.forEach((eles) => {
  825. if(eles instanceof Element) eles.querySelectorAll(":scope div.play-detail.play-detail--highlightable").forEach((ele) => {
  826. if(ele.getAttribute("improved") !== null) return;
  827. const a = ele.querySelector(":scope time.js-timeago");
  828. const t = a.getAttribute("datetime");
  829. const data = messageCache.get(`${userId},${modestr},${subdomain},${t}`);
  830. if(data){
  831. sectionNames.add(GetSection(ele));
  832. ListItemWorker(ele, data, isLazer);
  833. }
  834. });
  835. });
  836. sectionNames.forEach(sectionName => AdjustStyle(modestr, sectionName));
  837. };
  838. const DiffToColour = (diff, stops = [0.1, 1.25, 2, 2.5, 3.3, 4.2, 4.9, 5.8, 6.7, 7.7, 9], vals = ['#4290FB', '#4FC0FF', '#4FFFD5', '#7CFF4F', '#F6F05C', '#FF8068', '#FF4E6F', '#C645B8', '#6563DE', '#18158E', '#000000']) => {
  839. const len = stops.length;
  840. diff = Math.min(Math.max(diff, stops[0]), stops[len - 1]);
  841. let r = stops.findIndex(stop => stop > diff);
  842. if(r === -1) r = len - 1;
  843. const d = stops[r] - stops[r - 1];
  844. return `#${[[1, 3], [3, 5], [5, 7]]
  845. .map(_ => [Number.parseInt(vals[r].slice(..._), 16), Number.parseInt(vals[r-1].slice(..._), 16)])
  846. .map(_ => Math.round((_[0] ** 2.2 * (diff - stops[r-1]) / d + _[1] ** 2.2 * (stops[r] - diff) / d) ** (1 / 2.2)).toString(16).padStart(2, "0"))
  847. .join("")
  848. }`;
  849. };
  850. let scr = {};
  851. const ListItemWorker = (ele, data, isLazer) => {
  852. if(ele.getAttribute("improved") !== null) return;
  853. ele.setAttribute("improved", "");
  854. if(data.pp){
  855. data.pp = Number(data.pp);
  856. const pptext = ele.querySelector(".play-detail__pp > span").childNodes[0];
  857. pptext.nodeValue = data.pp >= 1 ? data.pp.toPrecision(5) : (data.pp < 0.00005 ? 0 : data.pp.toFixed(4));
  858. if(data.weight) pptext.title = `${data.weight.pp >= 1 ? data.weight.pp.toPrecision(5) : (data.weight.pp < 0.00005 ? 0 : data.weight.pp.toFixed(4))} of total pp`;
  859. }
  860. const left = ele.querySelector("div.play-detail__group.play-detail__group--top");
  861. 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);`});
  862. left.insertAdjacentElement("beforebegin", leftc);
  863. const detail = ele.querySelector("div.play-detail__score-detail-top-right");
  864. const du = detail.children[0];
  865. if(!detail.children[1]) detail.append(HTML("div", {classList: "play-detail__pp-weight"}));
  866. const db = detail.children[1];
  867. data.statistics.perfect ??= 0, data.statistics.great ??= 0, data.statistics.good ??= 0, data.statistics.ok ??= 0, data.statistics.meh ??= 0, data.statistics.miss ??= 0;
  868. const bmName = ele.querySelector("span.play-detail__beatmap");
  869. const sr = HTML("div", {class: `difficulty-badge ${data.beatmap.difficulty_rating >= 6.7 ? "difficulty-badge--expert-plus" : ""}`, style: `--bg: ${DiffToColour(data.beatmap.difficulty_rating)}`},
  870. HTML("span", {class: "difficulty-badge__icon"}, HTML("span", {class: "fas fa-star"})),
  871. HTML("span", {class: "difficulty-badge__rating"}, HTML(`${data.beatmap.difficulty_rating.toFixed(2)}`))
  872. );
  873. /*
  874. const ic = ele;
  875. ic.classList.add("audio-player", "js-audio--player");
  876. ic.setAttribute("data-audio-url", `https://b.ppy.sh/preview/${data.beatmap.beatmapset_id}.mp3`)
  877. ic.setAttribute("data-audio-state", "paused");
  878. const gr = ele;
  879. gr.classList.add("audio-player__button", "audio-player__button--play", "js-audio--play");
  880. */
  881. bmName.parentElement.insertBefore(sr, bmName);
  882. const bma = ele.querySelector("a.play-detail__title");
  883. const cnt = [data.beatmap.count_circles, data.beatmap.count_sliders, data.beatmap.count_spinners];
  884. // const modeName = ["STD", "TAIKO", "CTB", "MANIA"];
  885. const secToMin = (s) => `${Math.floor(s/60)}:${String(s%60).padStart(2, '0')}`;
  886. // let scrMsg = `${modeName[data.ruleset_id]} ${data.beatmapset.title}\n[${data.beatmap.version}] ${secToMin(data.beatmap.total_length)}\n${data.total_score} ${data.rank} ${data.pp ? (data.pp >= 1 ? data.pp.toPrecision(5) : (data.pp < 0.00005 ? 0 : data.pp.toFixed(4))) : "-"}pp\n`;
  887. let scrMsg = `${data.beatmapset.title}\n [${data.beatmap.version}] ${secToMin(data.beatmap.total_length)}\n${data.total_score} ${data.rank} ${data.pp ? (data.pp >= 1 ? data.pp.toPrecision(5) : (data.pp < 0.00005 ? 0 : data.pp.toFixed(4))) : "-"}pp\n`;
  888. bma.onclick = (e) => {e.stopPropagation();};
  889. switch(data.ruleset_id){
  890. case 0:{
  891. du.replaceChildren(
  892. HTML("span", {class: "play-detail__before"}),
  893. HTML("span", {class: "play-detail__Accuracy", title: `${isLazer ? "V2" : "V1"} Accuracy`}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  894. HTML("span", {class: "play-detail__combo", title: `Combo${isLazer ? "/Max Combo" : ""}`},
  895. HTML("span", {class: `combo ${isLazer ?(data.max_combo === (data.maximum_statistics.great ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0) ? "legacy-perfect-combo" : ""):(data.legacy_perfect ? "legacy-perfect-combo" : "")}`}, HTML(`${data.max_combo}`)),
  896. isLazer ? HTML("/") : null,
  897. isLazer ? HTML("span", {class: "max-combo"}, HTML(`${(data.maximum_statistics.great ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0)}`)) : null,
  898. HTML("x"),
  899. ),
  900. );
  901. const m_300 = HTML("span", {class: "score-detail score-detail-osu-300"},
  902. HTML("span", {class: "osu-300"},
  903. HTML("300")
  904. ),
  905. HTML("span", {class: "score-detail-data-text"},
  906. HTML(`${data.statistics.great + data.statistics.perfect}`)
  907. )
  908. );
  909. const s100 = HTML("span", {class: "score-detail score-detail-osu-100"},
  910. HTML("span", {class: "osu-100"},
  911. HTML("100")
  912. ),
  913. HTML("span", {class: "score-detail-data-text"},
  914. HTML(`${data.statistics.ok + data.statistics.good}`)
  915. )
  916. );
  917. const s50 = HTML("span", {class: "score-detail score-detail-osu-50"},
  918. HTML("span", {class: "osu-50"},
  919. HTML("50")
  920. ),
  921. HTML("span", {class: "score-detail-data-text"},
  922. HTML(`${data.statistics.meh}`)
  923. )
  924. );
  925. const s0 = HTML("span", {class: "score-detail score-detail-osu-miss"},
  926. HTML("span", {class: "osu-miss"},
  927. HTML("img", {src: svg_osu_miss, alt: "miss"})
  928. ),
  929. HTML("span", {class: "score-detail-data-text"},
  930. HTML(`${data.statistics.miss}`)
  931. )
  932. );
  933. db.replaceChildren(m_300, s100, s50, s0);
  934. scrMsg += `${data.statistics.great + data.statistics.perfect}-${data.statistics.ok + data.statistics.good}-${data.statistics.meh}-${data.statistics.miss} ${data.max_combo}`;
  935. if (isLazer) {
  936. scrMsg += `${(data.maximum_statistics.great ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0)}`;
  937. }
  938. scrMsg += "x\n";
  939. scrMsg += `⭕ ${cnt[0]} 🌡️ ${cnt[1]} 🔄 ${cnt[2]}\n`;
  940. break;
  941. }
  942. case 1:{
  943. const cur = [data.statistics.great ?? 0, data.statistics.ok ?? 0, data.statistics.miss ?? 0];
  944. const mx = cur[0] + cur[1] + cur[2];
  945. du.replaceChildren(
  946. HTML("span", {class: "play-detail__before"}),
  947. HTML("span", {class: "play-detail__Accuracy"}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  948. HTML("span", {class: "play-detail__combo", title: `Combo/Max Combo`},
  949. HTML("span", {class: `combo ${(data.max_combo === mx ? "legacy-perfect-combo" : "")}`}, HTML(`${data.max_combo}`)),
  950. HTML("/"),
  951. HTML("span", {class: "max-combo"}, HTML(`${mx}`)),
  952. HTML("x"),
  953. ),
  954. );
  955. db.replaceChildren(
  956. HTML("span", {class: "score-detail score-detail-taiko-300"},
  957. HTML("span", {class: "taiko-300"}, HTML("300")),
  958. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.great ?? 0))
  959. ),
  960. HTML("span", {class: "score-detail score-detail-taiko-150"},
  961. HTML("span", {class: "taiko-150"}, HTML("150")),
  962. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.ok ?? 0))
  963. ),
  964. HTML("span", {class: "score-detail score-detail-fruits-combo"},
  965. HTML("span", {class: "taiko-miss"}, HTML("miss")),
  966. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss ?? 0))
  967. ),
  968. );
  969. scrMsg += `${data.statistics.great}-${data.statistics.ok}-${data.statistics.miss} ${data.max_combo}/${mx}x\n`;
  970. scrMsg += `🥁 ${cnt[0]} 🌡️ ${cnt[1]} 🍥 ${cnt[2]}\n`;
  971. break;
  972. }
  973. case 2:{
  974. if (isLazer) {
  975. const cur = [data.statistics.great ?? 0, data.statistics.large_tick_hit ?? 0, data.statistics.small_tick_hit ?? 0];
  976. const mx = [data.maximum_statistics.great ?? 0, data.maximum_statistics.large_tick_hit ?? 0, data.maximum_statistics.small_tick_hit ?? 0];
  977. du.replaceChildren(
  978. HTML("span", {class: "play-detail__before"}),
  979. HTML("span", {class: "play-detail__Accuracy"}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  980. HTML("span", {class: "play-detail__combo", title: `Combo/Max Combo`},
  981. HTML("span", {class: `combo ${(data.max_combo === mx[0] + mx[1] ? "legacy-perfect-combo" : "")}`}, HTML(`${data.max_combo}`)),
  982. isLazer ? HTML("/") : null,
  983. isLazer ? HTML("span", {class: "max-combo"}, HTML(`${mx[0] + mx[1]}`)) : null,
  984. HTML("x"),
  985. ),
  986. );
  987. db.replaceChildren(
  988. HTML("span", {class: "score-detail score-detail-fruits-300"},
  989. HTML("span", {class: "fruits-300"}, HTML("fruits")),
  990. HTML("span", {class: "score-detail-data-text"}, HTML(cur[0] + "/" + mx[0]))
  991. ),
  992. HTML("span", {class: "score-detail score-detail-fruits-100"},
  993. HTML("span", {class: "fruits-100"}, HTML("ticks")),
  994. HTML("span", {class: "score-detail-data-text"}, HTML(cur[1] + "/" + mx[1]))
  995. ),
  996. HTML("span", {class: "score-detail score-detail-fruits-50-miss"},
  997. HTML("span", {class: "fruits-50-miss"}, HTML("drops")),
  998. HTML("span", {class: "score-detail-data-text"}, HTML(cur[2] + "/" + mx[2]))
  999. )
  1000. );
  1001. scrMsg += `${cur[0]}/${mx[0]}-${cur[1]}/${mx[1]}-${cur[2]}/${mx[2]} ${data.max_combo}/${mx[0] + mx[1]}x\n`;
  1002. scrMsg += `🍎 ${cnt[0]} 💧 ${cnt[1]} 🍌 ${cnt[2]}\n`;
  1003. } else {
  1004. du.replaceChildren(
  1005. HTML("span", {class: "play-detail__before"}),
  1006. HTML("span", {class: "play-detail__Accuracy"}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  1007. );
  1008. db.replaceChildren(
  1009. HTML("span", {class: "score-detail score-detail-fruits-300"},
  1010. HTML("span", {class: "fruits-300"}, HTML("FRUIT")),
  1011. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.great ?? 0))
  1012. ),
  1013. HTML("span", {class: "score-detail score-detail-fruits-100"},
  1014. HTML("span", {class: "fruits-100"}, HTML("tick")),
  1015. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.large_tick_hit ?? 0))
  1016. ),
  1017. HTML("span", {class: "score-detail score-detail-fruits-50-miss"},
  1018. HTML("span", {class: "fruits-50-miss"}, HTML("miss")),
  1019. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.small_tick_miss ?? 0))
  1020. ),
  1021. HTML("span", {class: "score-detail score-detail-fruits-miss"},
  1022. HTML("span", {class: "fruits-miss"}, HTML("MISS")),
  1023. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss ?? 0))
  1024. )
  1025. );
  1026. scrMsg += `${data.statistics.great ?? 0}-${data.statistics.large_tick_hit ?? 0}-${data.statistics.small_tick_miss ?? 0}-${data.statistics.miss ?? 0} ${data.max_combo}\n`;
  1027. scrMsg += `🍎 ${cnt[0]} 💧 ${cnt[1]} 🍌 ${cnt[2]}\n`;
  1028. }
  1029. break;
  1030. }
  1031. case 3:{
  1032. 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));
  1033. const MCombo = (data.maximum_statistics.perfect ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0);
  1034. const isMCombo = isLazer ? data.max_combo >= MCombo : data.legacy_perfect;
  1035. du.replaceChildren(
  1036. HTML("span", {class: "play-detail__before"}),
  1037. HTML("span", {class: "play-detail__Accuracy2", title: `pp Accuracy`}, HTML(`${(v2acc * 100).toFixed(2)}%`)),
  1038. HTML("span", {class: "play-detail__Accuracy", title: `Score${isLazer ? "V2" : "V1"} Accuracy`}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  1039. HTML("span", {class: "play-detail__combo", title: `Combo${isLazer ? "/Max Combo" : ""}`},
  1040. HTML("span", {class: `combo ${isMCombo ? "legacy-perfect-combo" : ""}`}, HTML(`${data.max_combo}`)),
  1041. isLazer ? HTML("/") : null,
  1042. isLazer ? HTML("span", {class: "max-combo"}, HTML(MCombo)) : null,
  1043. HTML("x"),
  1044. ),
  1045. );
  1046. if(data.pp){
  1047. const lostpp = data.pp * (0.2 / (Math.min(Math.max(v2acc, 0.8), 1) - 0.8) - 1);
  1048. ele.querySelector(".play-detail__pp").appendChild(HTML("span", {class: "lost-pp"}, HTML(`-${lostpp.toPrecision(4)}`)));
  1049. }
  1050. const M_300 = Number(data.statistics.perfect) / Math.max(Number(data.statistics.great), 1);
  1051. db.replaceChildren(
  1052. HTML("span", {class: "score-detail score-detail-mania-max-300"},
  1053. HTML("span", {class: "mania-max"}, HTML("M")),
  1054. HTML("/"),
  1055. HTML("span", {class: "mania-300"}, HTML("300")),
  1056. HTML("span", {class: "score-detail-data-text"}, HTML(`${M_300 >= 1000 ? Math.round(M_300) : (M_300 < 1 ? M_300.toFixed(2): M_300.toPrecision(3))}`))
  1057. ),
  1058. HTML("span", {class: "score-detail score-detail-mania-max-200"},
  1059. HTML("span", {class: "mania-200"}, HTML("200")),
  1060. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.good))
  1061. ),
  1062. HTML("span", {class: "score-detail score-detail-mania-max-100"},
  1063. HTML("span", {class: "mania-100"}, HTML("100")),
  1064. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.ok))
  1065. ),
  1066. HTML("span", {class: "score-detail score-detail-mania-max-50"},
  1067. HTML("span", {class: "mania-50"}, HTML("50")),
  1068. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.meh))
  1069. ),
  1070. HTML("span", {class: "score-detail score-detail-mania-max-0"},
  1071. HTML("span", {class: "mania-miss"}, HTML("miss")),
  1072. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss))
  1073. )
  1074. );
  1075. scrMsg += `${data.statistics.perfect}-${data.statistics.great}-${data.statistics.good}-${data.statistics.ok}-${data.statistics.meh}-${data.statistics.miss} ${data.max_combo}`;
  1076. if(isLazer){
  1077. scrMsg += `/${MCombo}`;
  1078. }
  1079. scrMsg += "x\n";
  1080. scrMsg += `🍚 ${cnt[0]} 🍜 ${cnt[1]}\n`;
  1081. break;
  1082. }
  1083. }
  1084. scr[data.id] = scrMsg;
  1085. }
  1086. let lastInitData;
  1087. const OsuLevelToExp = (n) => {
  1088. if(n <= 100) return 5000 / 3 * (4 * n ** 3 - 3 * n ** 2 - n) + 1.25 * 1.8 ** (n - 60);
  1089. else return 26_931_190_827 + 99_999_999_999 * (n - 100);
  1090. }
  1091. const OsuExpValToStr = (num) => {
  1092. let exp = Math.log10(num);
  1093. if(exp >= 12){
  1094. return `${(num / 10 ** 12).toPrecision(4)}T`;
  1095. }
  1096. else if(exp >= 9){
  1097. return `${(num / 10 ** 9).toPrecision(4)}B`;
  1098. }
  1099. else if(exp >= 6){
  1100. return `${(num / 10 ** 6).toPrecision(4)}M`;
  1101. }
  1102. else if(exp >= 4){
  1103. return `${(num / 10 ** 3).toPrecision(4)}K`;
  1104. }
  1105. else return `${num}`;
  1106. }
  1107. const messageCache = new Map();
  1108. window.messageCache = messageCache;
  1109. const profUrlReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/users\/[0-9]+(?:|\/osu|\/taiko|\/fruits|\/mania)/;
  1110. const ImproveProfile = (mulist) => {
  1111. let initData, wloc = window.location.toString();
  1112. if(!profUrlReg.exec(wloc)) return;
  1113. const initDataEle = document.querySelector(".js-react--profile-page.osu-layout.osu-layout--full");
  1114. if(!initDataEle) return;
  1115. initData = JSON.parse(initDataEle.dataset.initialData);
  1116. const userId = initData.user.id, modestr = initData.current_mode;
  1117. if(initData !== lastInitData){
  1118. let ppDiv;
  1119. document.querySelectorAll("div.value-display.value-display--plain").forEach((ele) => {
  1120. if(ele.querySelector("div.value-display__label").textContent === "pp") ppDiv = ele;
  1121. });
  1122. if(ppDiv){
  1123. const ttscore = initData.user.statistics.total_score;
  1124. const lvl = initData.user.statistics.level.current;
  1125. const upgradescore = Math.round(OsuLevelToExp(lvl + 1) - OsuLevelToExp(lvl));
  1126. const lvlscore = ttscore - Math.round(OsuLevelToExp(lvl));
  1127. lastInitData = initData;
  1128. document.querySelector("div.bar__text").textContent = `${OsuExpValToStr(lvlscore)}/${OsuExpValToStr(upgradescore)} (${(lvlscore/upgradescore * 100).toPrecision(3)}%)`;
  1129. const _pp = initData.user.statistics.pp;
  1130. ppDiv.querySelector(".value-display__value > div").textContent = _pp >= 1 ? _pp.toPrecision(6) : (_pp < 0.000005 ? 0 : _pp.toFixed(5));
  1131. }
  1132. }
  1133. if(mulist !== undefined) mulist.forEach((record) => {
  1134. if(record.type === "childList" && record.addedNodes) TopRanksWorker(userId, modestr, record.addedNodes);
  1135. });
  1136. }
  1137. let wloc = "";
  1138. const WindowLocationChanged = () => {
  1139. if(window.location !== wloc){
  1140. wloc = window.location;
  1141. return true;
  1142. }
  1143. else return false;
  1144. }
  1145. const InsertStyleSheet = () => {
  1146. //const sheetId = "osu-web-enhancement-general-stylesheet";
  1147. const s = new CSSStyleSheet();
  1148. s.replaceSync(inj_style);
  1149. document.adoptedStyleSheets = [...document.adoptedStyleSheets, s];
  1150. }
  1151. const OnBeatmapsetDownload = (message) => {
  1152. beatmapsets.add(message.beatmapsetId);
  1153. }
  1154. const ImproveBeatmapPlaycountItems = () => {
  1155. for(const item of [...document.querySelectorAll("div.beatmap-playcount")]){
  1156. if(item.getAttribute("improved") !== null) continue;
  1157. item.setAttribute("improved", "");
  1158. const a = item.querySelector("a");
  1159. const bms = bmsReg.exec(a.href);
  1160. if(!bms?.[1]) continue;
  1161. const d = item.querySelector("div.beatmap-playcount__detail");
  1162. const b = HTML("div", {class: "beatmap-playcount__background", style: `background-image: url(https://assets.ppy.sh/beatmaps/${bms[1]}/covers/card@2x.jpg)`});
  1163. if(d.childElementCount > 0) d.insertBefore(b, d.children[0]);
  1164. else d.append(b);
  1165. }
  1166. }
  1167. const CloseScoreCardPopup = () => {
  1168. document.querySelector("div.score-card-popup-window").remove();
  1169. }
  1170. const CopyToClipboard = (txt) => {
  1171. const t = document.createElement('textarea');
  1172. t.value = txt;
  1173. document.body.appendChild(t);
  1174. t.select();
  1175. document.execCommand('copy');
  1176. document.body.removeChild(t);
  1177. }
  1178. const ShowScoreCardPopup = () => {
  1179. const p = document.querySelector("div.js-portal");
  1180. if(!p) return;
  1181. document.body.append(
  1182. HTML("div", {class: "score-card-popup-window"},
  1183. HTML("div", {class: "score-card-popup-menu"},
  1184. HTML("button", {class: "score-card-close-button", eventListener: {type: "click", listener: CloseScoreCardPopup}}),
  1185. HTML("button", {class: "score-card-copy-to-clipboard-button", ev}),
  1186. ),
  1187. HTML("div", {class: "score-card"},
  1188. )
  1189. )
  1190. );
  1191. };
  1192. const CopyDetailsPopup = (id) => {
  1193. let msg = scr[document.querySelector("div.js-portal")?.querySelector("div.simple-menu").querySelector("a").href.split("/").pop()];
  1194. console.log(msg);
  1195. CopyToClipboard(msg);
  1196. ShowPopup("Score details copied to clipboard!");
  1197. };
  1198. const AddPopupButton = () => {
  1199. const p = document.querySelector("div.js-portal")?.querySelector("div.simple-menu");
  1200. if(!p || p.querySelector("button.score-card-popup-button")) return;
  1201. // p.append(HTML("button", {class: "score-card-popup-button simple-menu__item", type: "button", eventListener: [{type: "click", listener: ShowScoreCardPopup}]}, HTML("Popup")));
  1202. p.append(HTML("button", {class: "score-card-popup-button simple-menu__item", type: "button", eventListener: [{type: "click", listener: CopyDetailsPopup}]}, HTML("Copy Text Details")));
  1203. };
  1204. const OnMutation = (mulist) => {
  1205. mut.disconnect();
  1206. AddMenu();
  1207. FilterBeatmapSet();
  1208. ImproveBeatmapPlaycountItems();
  1209. ImproveProfile(mulist);
  1210. AddPopupButton();
  1211. mut.observe(document, {childList: true, subtree: true});
  1212. };
  1213. const MessageFilter = (message) => {
  1214. info = `${message.userId},${message.mode},${message.subdomain}`;
  1215. switch(message.type){
  1216. case "beatmapset_download_complete": OnBeatmapsetDownload(message); break;
  1217. case "top_ranks":
  1218. [message.data.pinned.items, message.data.best.items, message.data.firsts.items].forEach(items => items.forEach(item => {
  1219. messageCache.set(`${info},${item.ended_at}`, item);
  1220. }));
  1221. TopRanksWorker(message.userId, message.mode);
  1222. break;
  1223. case "firsts": case "pinned": case "best": case "recent":
  1224. message.data.forEach(item => { messageCache.set(`${info},${item.ended_at}`, item); });
  1225. TopRanksWorker(message.userId, message.mode);
  1226. break;
  1227. case "historical":
  1228. message.data.recent.items.forEach(item => { messageCache.set(`${info},${item.ended_at}`, item); });
  1229. TopRanksWorker(message.userId, message.mode);
  1230. break;
  1231. }
  1232. }
  1233. const WindowMessageFilter = (event) => {
  1234. if(event.source === window && event?.data?.id === "osu!web enhancement"){
  1235. MessageFilter(event.data);
  1236. }
  1237. }
  1238. const OnClick = (event) => {
  1239. let t = event.target;
  1240. while(t){
  1241. if(t.tagName === "A"){
  1242. const e = bmsdlReg.exec(t.href);
  1243. if(!e) continue;
  1244. beatmapsets.add(Number(e[1]));
  1245. FilterBeatmapSet();
  1246. break;
  1247. }
  1248. t = t.parentElement;
  1249. }
  1250. }
  1251. //document.addEventListener("click", OnClick);
  1252. window.addEventListener("message", WindowMessageFilter);
  1253. const mut = new MutationObserver(OnMutation);
  1254. mut.observe(document, {childList: true, subtree: true});
  1255. InsertStyleSheet();
  1256. //{id, mode} -> (bmid -> record)
  1257. console.log("osu!web enhancement loaded");
  1258.  
  1259.  
  1260. // below are test code
  1261. /*
  1262. const osusrc = "https://i.ppy.sh/bde5906f8f985126f4ea624d3eb14c8702883aa2/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d6f73752e706e67";
  1263. const taikosrc = "https://i.ppy.sh/c1a9502ea05c9fcde03a375ebf528a12ff30cae7/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d7461696b6f2e706e67";
  1264. const fruitsrc = "https://i.ppy.sh/e7cad0470810a868df06d597e3441812659c0bfa/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d6672756974732e706e67";
  1265. const maniasrc = "https://i.ppy.sh/55d9494fcf7c3ef2d614695a9a951977a21f23f6/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d6d616e69612e706e67";
  1266. const pngsrc = [osusrc, taikosrc, fruitsrc, maniasrc];
  1267. const png = [null, null, null, null];
  1268. let canvas, ctx, cw, ch;
  1269. const ToggleSnow = async (modeid) => {
  1270. if(canvas) {canvas.remove(); return;}
  1271. canvas = HTML("canvas", {style: `position: fixed; bottom: 0px; left: 0px;`, width: window.innerWidth, height: window.innerHeight});
  1272. document.body.append(canvas);
  1273. ctx = canvas.getContext("webgl2");
  1274. if(!png[modeid]){
  1275. const response = await fetch(pngsrc[modeid]);
  1276. png[modeid] = await response.blob();
  1277. }
  1278. }
  1279. */

QingJ © 2025

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