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

QingJ © 2025

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