osu!web enhancement

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

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

  1. // ==UserScript==
  2. // @name osu!web enhancement
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.5.6
  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-reverse;
  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. .beatmap-playcount__background{
  193. width: 100%;
  194. border-radius: 6px;
  195. mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
  196. -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));
  197. }
  198. .beatmap-playcount__info, .beatmap-playcount__detail-count{
  199. z-index: 1;
  200. }
  201. .play-detail__group.play-detail__group--top *{
  202. z-index: 3;
  203. }
  204. div.play-detail-list time.js-timeago, span.beatmap-playcount__mapper, span.beatmap-playcount__mapper > a{
  205. color: #ccc;
  206. }
  207. button.show-more-link{
  208. z-index: 4;
  209. }
  210. a.beatmap-download-link{
  211. margin: 0px 5px;
  212. color: hsl(var(--hsl-l1));
  213. }
  214. a.beatmap-download-link:hover, a.beatmap-pack-item-download-link span:hover{
  215. color: #fff;
  216. }
  217. a.beatmap-pack-item-download-link span{
  218. color: hsl(var(--hsl-l1));
  219. }
  220. `;
  221. let scriptContent =
  222. String.raw`console.log("page script injected from osu!web enhancement");
  223. let oldXHROpen = window.XMLHttpRequest.prototype.open;
  224. window.XMLHttpRequest.prototype.open = function() {
  225. this.addEventListener("load", function() {
  226. const url = this.responseURL;
  227. 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);
  228. 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);
  229. let reg = trreg ?? (adreg ?? null);
  230. if(!reg) return;
  231. let info = {
  232. type: reg.groups.type,
  233. userId: Number(reg.groups.id),
  234. mode: reg.groups.mode,
  235. subdomain: reg.groups.subdomain,
  236. };
  237. const responseBody = this.responseText;
  238. info.data = JSON.parse(responseBody);
  239. info.id = "osu!web enhancement";
  240. window.postMessage(info, "*");
  241. });
  242. return oldXHROpen.apply(this, arguments);
  243. };`;
  244. const scriptId = "osu-web-enhancement-XHR-script";
  245. if(!document.querySelector(`script#${scriptId}`)){
  246. const script = document.createElement("script");
  247. script.textContent = scriptContent;
  248. document.body.appendChild(script);
  249. }
  250. const HTML = (tagname, attrs, ...children) => {
  251. if(attrs === undefined) return document.createTextNode(tagname);
  252. const ele = document.createElement(tagname);
  253. if(attrs) for(let [key, value] of Object.entries(attrs)){
  254. if(key === "eventListener"){
  255. for(let listener of value){
  256. ele.addEventListener(listener.type, listener.listener, listener.options);
  257. }
  258. }
  259. else ele.setAttribute(key, value);
  260. }
  261. for(let child of children) if(child) ele.append(child);
  262. return ele;
  263. };
  264. const html = (html) => {
  265. const t = document.createElement("template");
  266. t.innerHTML = html;
  267. return t.content.firstElementChild;
  268. };
  269. const PostMessage = (msg) => { console.error(msg); };
  270. const OsuMod = {
  271. NoFail: 1 << 0,
  272. Easy: 1 << 1,
  273. TouchDevice: 1 << 2,
  274. NoVideo: 1 << 2,
  275. Hidden: 1 << 3,
  276. HardRock: 1 << 4,
  277. SuddenDeath: 1 << 5,
  278. DoubleTime: 1 << 6,
  279. Relax: 1 << 7,
  280. HalfTime: 1 << 8,
  281. Nightcore: 1 << 9, // always with DT
  282. Flashlight: 1 << 10,
  283. Autoplay: 1 << 11,
  284. SpunOut: 1 << 12,
  285. Autopilot: 1 << 13,
  286. Perfect: 1 << 14,
  287. Key4: 1 << 15,
  288. Key5: 1 << 16,
  289. Key6: 1 << 17,
  290. Key7: 1 << 18,
  291. Key8: 1 << 19,
  292. KeyMod: 1 << 19 | 1 << 18 | 1 << 17 | 1 << 16 | 1 << 15,
  293. FadeIn: 1 << 20,
  294. Random: 1 << 21,
  295. Cinema: 1 << 22,
  296. TargetPractice: 1 << 23,
  297. Key9: 1 << 24,
  298. Coop: 1 << 25,
  299. Key1: 1 << 26,
  300. Key3: 1 << 27,
  301. Key2: 1 << 28,
  302. ScoreV2: 1 << 29,
  303. Mirror: 1 << 30,
  304. };
  305. class Byte{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++]; } };
  306. class RankedStatus extends Byte{
  307. constructor(arr, iter){
  308. super(arr, iter);
  309. switch(this.value){
  310. case 1: this.description = "unsubmitted"; break;
  311. case 2: this.description = "pending/wip/graveyard"; break;
  312. case 3: this.description = "unused"; break;
  313. case 4: this.description = "ranked"; break;
  314. case 5: this.description = "approved"; break;
  315. case 6: this.description = "qualified"; break;
  316. case 7: this.description = "loved"; break;
  317. default: this.description = "unknown"; this.value = 0;
  318. }
  319. }
  320. };
  321. class OsuMode extends Byte{
  322. constructor(arr, iter){
  323. super(arr, iter);
  324. switch(this.value){
  325. case 1: this.description = "taiko"; break;
  326. case 2: this.description = "catch"; break;
  327. case 3: this.description = "mania"; break;
  328. default: this.value = 0; this.description = "osu";
  329. }
  330. }
  331. };
  332. class Grade extends Byte{
  333. constructor(arr, iter){
  334. super(arr, iter);
  335. switch(this.value){
  336. case 0: this.description = "SSH"; break;
  337. case 1: this.description = "SH"; break;
  338. case 2: this.description = "SS"; break;
  339. case 3: this.description = "S"; break;
  340. case 4: this.description = "A"; break;
  341. case 5: this.description = "B"; break;
  342. case 6: this.description = "C"; break;
  343. case 7: this.description = "D"; break;
  344. default: this.description = "not played";
  345. }
  346. }
  347. };
  348. class Short{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8; } };
  349. class Int{ value = 0; constructor(arr, iter){ this.value = arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8 | arr[iter.nxtpos++] << 16 | arr[iter.nxtpos++] << 24; } };
  350. class Long{ value = 0n; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 8).getBigUint64(0, true); iter.nxtpos += 8; } };
  351. class ULEB128{
  352. value = 0n;
  353. constructor(arr, iter){
  354. let shift = 0n;
  355. while(true){
  356. let peek = BigInt(arr[iter.nxtpos++]);
  357. this.value |= (peek & 0x7Fn) << shift;
  358. if((peek & 0x80n) === 0n) break;
  359. shift += 7n;
  360. }
  361. }
  362. };
  363. class Single{ value = 0; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 4).getFloat32(0, true); iter.nxtpos += 4; } };
  364. class Double{ value = 0; constructor(arr, iter){ this.value = new DataView(arr.buffer, iter.nxtpos, 8).getFloat64(0, true); iter.nxtpos += 8; } };
  365. class Boolean{ value = false; constructor(arr, iter){ this.value = arr[iter.nxtpos++] !== 0x00; } };
  366. class OString{
  367. value = "";
  368. constructor(arr, iter){
  369. switch(arr[iter.nxtpos++]){
  370. case 0: break;
  371. case 0x0b: {
  372. const l = new ULEB128(arr, iter).value;
  373. const bv = new Uint8Array(arr.buffer, iter.nxtpos, Number(l));
  374. this.value = new TextDecoder().decode(bv);
  375. iter.nxtpos += Number(l);
  376. break;
  377. }
  378. default: console.assert(false, `error occurred while parsing osu string with the first byte.`);
  379. }
  380. }
  381. };
  382. class IntDouble{
  383. int = 0;
  384. double = 0;
  385. constructor(arr, iter){
  386. const m1 = arr[iter.nxtpos++];
  387. console.assert(m1 === 0x08, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  388. this.int = new Int(arr, iter).value;
  389. const m2 = arr[iter.nxtpos++];
  390. console.assert(m2 === 0x0d, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  391. this.double = new Double(arr, iter).value;
  392. }
  393. };
  394. class IntDoubleArray extends Array{
  395. constructor(arr, iter){
  396. super(new Int(arr, iter).value);
  397. for(let i = 0; i < this.length; i++) this[i] = new IntDouble(arr, iter);
  398. }
  399. };
  400. class TimingPoint{
  401. BPM = 0;
  402. offset = 0;
  403. notInherited = false;
  404. constructor(arr, iter){
  405. this.BPM = new Double(arr, iter).value;
  406. this.offset = new Double(arr, iter).value;
  407. this.notInherited = new Boolean(arr, iter).value;
  408. }
  409. };
  410. class TimingPointArray extends Array{
  411. constructor(arr, iter){
  412. super(new Int(arr, iter).value);
  413. for(let i = 0; i < this.length; i++) this[i] = new TimingPoint(arr, iter);
  414. }
  415. };
  416. class DateTime extends Long{};
  417. class Beatmap{
  418. constructor(arr, iter){
  419. if(iter.osuVersion < 20191106) this.bytes = new Int(arr, iter);
  420. this.artistName = new OString(arr, iter);
  421. this.artistNameUnicode = new OString(arr, iter);
  422. this.songTitle = new OString(arr, iter);
  423. this.songTitleUnicode = new OString(arr, iter);
  424. this.creatorName = new OString(arr, iter);
  425. this.difficultyName = new OString(arr, iter);
  426. this.audioFilename = new OString(arr, iter);
  427. this.MD5Hash = new OString(arr, iter);
  428. this.beatmapFilename = new OString(arr, iter);
  429. this.rankedStatus = new RankedStatus(arr, iter);
  430. this.hitcircleCount = new Short(arr, iter);
  431. this.sliderCount = new Short(arr, iter);
  432. this.spinnerCount = new Short(arr, iter);
  433. this.lastModified = new Long(arr, iter);
  434. this.AR = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  435. this.CS = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  436. this.HP = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  437. this.OD = iter.osuVersion < 20140609 ? new Byte(arr, iter) : new Single(arr, iter);
  438. this.sliderVelocity = new Double(arr, iter);
  439. if(iter.osuVersion >= 20140609) this.osuSRInfoArr = new IntDoubleArray(arr, iter);
  440. if(iter.osuVersion >= 20140609) this.taikoSRInfoArr = new IntDoubleArray(arr, iter);
  441. if(iter.osuVersion >= 20140609) this.catchSRInfoArr = new IntDoubleArray(arr, iter);
  442. if(iter.osuVersion >= 20140609) this.maniaSRInfoArr = new IntDoubleArray(arr, iter);
  443. this.drainTime = new Int(arr, iter);
  444. this.totalTime = new Int(arr, iter);
  445. this.audioPreviewTime = new Int(arr, iter);
  446. this.timingPointArr = new TimingPointArray(arr, iter);
  447. this.difficultyID = new Int(arr, iter);
  448. this.beatmapID = new Int(arr, iter);
  449. this.threadID = new Int(arr, iter);
  450. this.osuGrade = new Grade(arr, iter);
  451. this.taikoGrade = new Grade(arr, iter);
  452. this.catchGrade = new Grade(arr, iter);
  453. this.maniaGrade = new Grade(arr, iter);
  454. this.offsetLocal = new Short(arr, iter);
  455. this.stackLeniency = new Single(arr, iter);
  456. this.mode = new OsuMode(arr, iter);
  457. this.sourceStr = new OString(arr, iter);
  458. this.tagStr = new OString(arr, iter);
  459. this.offsetOnline = new Short(arr, iter);
  460. this.titleFont = new OString(arr, iter);
  461. this.unplayed = new Boolean(arr, iter);
  462. this.lastTimePlayed = new Long(arr, iter);
  463. this.isOsz2 = new Boolean(arr, iter);
  464. this.folderName = new OString(arr, iter);
  465. this.lastTimeChecked = new Long(arr, iter);
  466. this.ignoreBeatmapSound = new Boolean(arr, iter);
  467. this.ignoreBeatmapSkin = new Boolean(arr, iter);
  468. this.disableStoryboard = new Boolean(arr, iter);
  469. this.disableVideo = new Boolean(arr, iter);
  470. this.visualOverride = new Boolean(arr, iter);
  471. if(iter.osuVersion < 20140609) this.uselessShort = new Short(arr, iter);
  472. this.lastModified = new Int(arr, iter);
  473. this.scrollSpeedMania = new Byte(arr, iter);
  474. }
  475. };
  476. class BeatmapArray extends Array{
  477. constructor(arr, iter){
  478. super(new Int(arr, iter).value);
  479. for(let i = 0; i < this.length; i++) this[i] = new Beatmap(arr, iter);
  480. }
  481. };
  482. class OsuDb{
  483. constructor(arr, iter){
  484. this.version = new Int(arr, iter);
  485. iter.osuVersion = this.version.value;
  486. this.folderCount = new Int(arr, iter);
  487. this.accountUnlocked = new Boolean(arr, iter);
  488. this.timeTillUnlock = new DateTime(arr, iter);
  489. this.playerName = new OString(arr, iter);
  490. this.beatmapArray = new BeatmapArray(arr, iter);
  491. this.permission = new Int(arr, iter);
  492. }
  493. };
  494. const beatmapsets = new Set();
  495. const beatmaps = new Set();
  496. const bmsReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/([0-9]+)/;
  497. const bmsdlReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/([0-9]+)\/download/;
  498. const bmReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/(?:[0-9]+)#(?:mania|osu|fruits|taiko)\/([0-9]+)/;
  499. const BeatmapsetRefresh = () => {
  500. for(const bm of window.osudb.beatmapArray){
  501. beatmaps.add(bm.difficultyID.value);
  502. beatmapsets.add(bm.beatmapID.value);
  503. }
  504. OnMutation();
  505. };
  506. const NewOsuDb = (r) => {
  507. return new Promise((resolve, reject) => {
  508. const start = performance.now();
  509. const result = new Uint8Array(r.result);
  510. const length = result.length;
  511. console.log(`start reading osu!.db(${length} Bytes).`);
  512. const iter = {
  513. nxtpos: 0,
  514. };
  515. window.osudb = new OsuDb(result, iter);
  516. if(iter.nxtpos !== length) ShowPopup("There are still remaining unread bytes, something may be wrong.", "danger");
  517. ShowPopup(`Finished reading osu!.db in ${performance.now() - start} ms.`);
  518. resolve();
  519. })
  520. };
  521. const ReadOsuDb = (file) => {
  522. if(file.name !== "osu!.db"){ console.assert( false, "filename should be 'osu!.db'."); return; }
  523. const r = new FileReader();
  524. r.onload = () => {
  525. NewOsuDb(r);
  526. BeatmapsetRefresh();
  527. };
  528. r.onerror = () => console.assert(false, "error occurred while reading file.");
  529. r.readAsArrayBuffer(file);
  530. };
  531. const SelectOsuDb = (event) => {
  532. const t = event.target;
  533. const l = t.files;
  534. console.assert(l && l.length === 1, "No file or multiple files are selected.");
  535. ReadOsuDb(l[0]);
  536. };
  537. const CheckForUpdate = () => {
  538. const verReg = /<dd class="script-show-version"><span>([0-9\.]+)<\/span><\/dd>/;
  539. fetch("https://gf.qytechs.cn/en/scripts/475417-osu-web-enhancement", {
  540. credentials: "omit"
  541. }).then(response => response.text()).then((html) => {
  542. const ver = verReg.exec(html);
  543. if(ver){
  544. const result = (() => {
  545. const verList = ver[1].split(".");
  546. const thisVer = GM_info.script.version;
  547. console.log(`latest version is: ${ver[1]}, current version is: ${thisVer}`);
  548. const thisVerList = thisVer.split(".");
  549. for(let i = 0; i < verList.length; i++){
  550. if(Number(verList[i]) > Number(thisVerList[i] ?? 0)) return true;
  551. else if(Number(verList[i]) < Number(thisVerList[i] ?? 0)) return false;
  552. }
  553. return false;
  554. })();
  555. if(result){
  556. const a = HTML("a", {href: "https://gf.qytechs.cn/scripts/475417-osu-web-enhancement/code/osu!web%20enhancement.user.js", download: "", style: "display:none;"});
  557. a.click();
  558. }
  559. else{
  560. ShowPopup("The lastest version is already installed!")
  561. }
  562. }
  563. });
  564. };
  565. const AddMenu = () => {
  566. const menuId = "osu-web-enhancement-toolbar";
  567. if(document.getElementById(menuId)) return;
  568. const anc = document.querySelector("div.nav2__col.nav2__col--menu.js-react--quick-search-button");
  569. const i = HTML("input", {type: "file", id: "osu-db-input", accept: ".db", eventListener: [{
  570. type: "change",
  571. listener: SelectOsuDb,
  572. options: false,
  573. }]});
  574. const menuClass = "simple-menu simple-menu--nav2 simple-menu--nav2-left-aligned simple-menu--nav2-transparent js-menu";
  575. const menuItemClass = "simple-menu__item u-section-community--before-bg-normal";
  576. const menuTgtId = "osu-web-enhancement";
  577. anc.insertAdjacentElement("beforebegin",
  578. HTML("div", {class: "nav2__col nav2__col--menu", id: menuId},
  579. 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;"},
  580. HTML("span", {style: "flex-grow: 1;"}),
  581. HTML("span", {style: "font-size: 10px;"}, HTML("osu!web")),
  582. HTML("span", {style: "font-size: 10px;"}, HTML("enhancement")),
  583. HTML("span", {style: "flex-grow: 1;"}),
  584. ),
  585. HTML("div", {class: "nav2__menu-popup"},
  586. HTML("div", {class: `${menuClass}`, "data-menu-id": `nav2-menu-popup-${menuTgtId}`, "data-visibility": "hidden"},
  587. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", id: "import-osu-db-button", eventListener: [{
  588. type: "click",
  589. listener: () => {if(i) i.click();},
  590. options: false,
  591. }]}, HTML("Import osu!.db")),
  592. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", eventListener: [{
  593. type: "click",
  594. listener: CheckForUpdate,
  595. }]}, HTML("Check for update")),
  596. ),
  597. )
  598. )
  599. );
  600. const mobMenuItmCls = "navbar-mobile-item__submenu-item js-click-menu--close";
  601. const mob = document.querySelector(`div.mobile-menu__item.js-click-menu[data-click-menu-id="mobile-nav"]`);
  602. mob.insertAdjacentElement("beforeend",
  603. HTML("div", {class: "navbar-mobile-item"},
  604. HTML("div", {class: "navbar-mobile-item__main js-click-menu", "data-click-menu-target": `nav-mobile-${menuTgtId}`, style: "cursor: pointer;"},
  605. HTML("span", {class: "navbar-mobile-item__icon navbar-mobile-item__icon--closed"},
  606. HTML("i", {class: "fas fa-chevron-right"})
  607. ),
  608. HTML("span", {class: "navbar-mobile-item__icon navbar-mobile-item__icon--opened"},
  609. HTML("i", {class: "fas fa-chevron-down"})
  610. ),
  611. HTML("osu!web enhancement"),
  612. ),
  613. HTML("ul", {class: "navbar-mobile-item__submenu js-click-menu", "data-click-menu-id": `nav-mobile-${menuTgtId}`, "data-visibility": "hidden"},
  614. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", eventListener: [{
  615. type: "click",
  616. listener: () => {if(i) i.click();},
  617. }]}, HTML("Import osu!.db"))),
  618. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", eventListener: [{
  619. type: "click",
  620. listener: CheckForUpdate,
  621. }]}, HTML("Check for update"))),
  622. )
  623. )
  624. );
  625. document.body.appendChild(i);
  626. };
  627. const FilterBeatmapSet = () => {
  628. document.querySelectorAll(".beatmapsets__item").forEach((item) => {
  629. const bmsID = Number(bmsReg.exec(item.innerHTML)?.[1]);
  630. if(bmsID && beatmapsets.has(bmsID)){
  631. item.classList.add("owned-beatmapset");
  632. }
  633. });
  634. document.querySelectorAll("div.bbcode a, a.osu-md__link").forEach(item => {
  635. if(item.classList.contains("owned-beatmap-link") || item.classList.contains("beatmap-download-link")) return;
  636. const e = bmsReg.exec(item.href);
  637. if(e && beatmapsets.has(Number(e[1]))){
  638. item.classList.add("owned-beatmap-link");
  639. if(item.nextElementSibling?.classList?.contains("beatmap-download-link")) item.nextElementSibling.remove();
  640. const box = item.getBoundingClientRect();
  641. const size = Math.round(box.height / 16 * 14);
  642. const vert = Math.round(size * 4 / 14) / 2;
  643. 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;`}));
  644. }else if(e && !item.nextElementSibling?.classList?.contains("beatmap-download-link")){
  645. item.after(
  646. HTML("a", {class: "beatmap-download-link", href: `https://osu.ppy.sh/beatmapsets/${e[1]}/download`, download: ""},
  647. HTML("span", {class: "fas fa-file-download", title: "Download"})
  648. )
  649. );
  650. }
  651. });
  652. document.querySelectorAll("li.beatmap-pack-items__set").forEach(item => {
  653. if(item.classList.contains("owned-beatmap-pack-item")) return;
  654. const a = item.querySelector("a.beatmap-pack-items__link");
  655. const e = bmsReg.exec(a.href);
  656. if(e && beatmapsets.has(Number(e[1]))){
  657. item.classList.add("owned-beatmap-pack-item");
  658. const span = item.querySelector("span.fal");
  659. span.setAttribute("title", "Owned");
  660. span.dataset.origTitle = "owned";
  661. span.setAttribute("class", "");
  662. span.append(HTML("img", {src: svg_green_tick, alt: "owned beatmap", style: `width: 16px; height: 16px; vertical-align: -2px;`}));
  663. const parent = item.querySelector(".beatmap-pack-item-download-link");
  664. if(parent){
  665. console.assert(parent.parentElement === item, "unexpected error occurred!");
  666. item.insertBefore(span, parent);
  667. parent.remove();
  668. }
  669. }else if(e){
  670. const icon = item.querySelector(".beatmap-pack-items__icon");
  671. icon.setAttribute("title", "Download");
  672. icon.setAttribute("class", "fas fa-file-download beatmap-pack-items__icon");
  673. if(icon.parentElement === item){
  674. const dl = HTML("a", {class: "beatmap-pack-item-download-link", href: `https://osu.ppy.sh/beatmapsets/${e[1]}/download`, download: ""});
  675. item.insertBefore(dl, icon);
  676. dl.append(icon);
  677. }
  678. }
  679. })
  680. };
  681. const AdjustStyle = (modeId, sectionName) => {
  682. const styleSheetId = `userscript-generated-stylesheet-${sectionName}`;
  683. let e = document.getElementById(styleSheetId);
  684. if(!e){
  685. e = document.createElement("style");
  686. e.id = styleSheetId;
  687. document.head.appendChild(e);
  688. }
  689. const s = e.sheet;
  690. while(s.cssRules.length) s.deleteRule(0);
  691. const sectionSelector = `div.js-sortable--page[data-page-id="${sectionName}"]`;
  692. let ll = [];
  693. switch(modeId){
  694. case 3: ll = [".mania-300", ".mania-200", ".mania-100", ".mania-50", ".mania-miss"]; break;
  695. case 2: ll = [".fruits-300", ".fruits-100", ".fruits-50-miss", ".fruits-miss"]; break;
  696. case 1: ll = [".taiko-300", ".taiko-150", ".taiko-miss"]; break;
  697. case 0: ll = [".osu-300", ".osu-100", ".osu-50", ".osu-miss"]; break;
  698. }
  699. ll.forEach((str) =>
  700. s.insertRule(
  701. `${sectionSelector} ${str} + .score-detail-data-text {
  702. width: ${[...document.querySelectorAll(`${sectionSelector} ${str} + .score-detail-data-text`)].reduce((max, ele) => ele.clientWidth > max ? ele.clientWidth : max, 0) + 2}px;
  703. }` ,0
  704. )
  705. );
  706. [".play-detail__pp", ".play-detail__combo", ".play-detail__Accuracy", ".play-detail__Accuracy2"].forEach((str) =>
  707. s.insertRule(
  708. `${sectionSelector} ${str}{
  709. min-width: ${Math.ceil([...document.querySelectorAll(`${sectionSelector} ${str}`)].reduce((max, ele) => {const w = ele.getBoundingClientRect().width; return w > max ? w : max;}, 0)) + 1}px;
  710. }`
  711. ,0
  712. )
  713. );
  714. };
  715. const TopRanksWorker = (dataList, tabId, sectionName = "top_ranks") => {
  716. if(!dataList.length) return true;
  717. const tabEle = document.querySelector(`div.js-sortable--page[data-page-id="${sectionName}"]`);
  718. if(!tabEle) return false;
  719. const listEle = tabEle.querySelectorAll(`.title.title--page-extra-small + div.play-detail-list.u-relative`)?.[tabId];
  720. if(!listEle) return false;
  721. const rid = dataList[0].ruleset_id;
  722. let s = 0, e = 0;
  723. for(const ele of [...listEle.querySelectorAll(".play-detail.play-detail--highlightable")]){
  724. const a = ele.querySelector("time.js-timeago");
  725. const t = a.getAttribute("datetime");
  726. const i = dataList.findIndex(item => item.ended_at === t);
  727. if(i !== -1){
  728. ListItemWorker(ele, dataList[i]);
  729. if(i === e) e++;
  730. else if(i === s - 1) s--;
  731. else if(i < s){
  732. dataList.splice(s, e - s);
  733. s = i, e = i + 1;
  734. }
  735. else if(i > e){
  736. dataList.splice(s, e - s);
  737. s = i - (e - s);
  738. e = s + 1;
  739. }
  740. }
  741. }
  742. dataList.splice(s, e - s);
  743. if(dataList.length) return false;
  744. AdjustStyle(rid, sectionName, tabId);
  745. return true;
  746. };
  747. 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']) => {
  748. const len = stops.length;
  749. diff = Math.min(Math.max(diff, stops[0]), stops[len - 1]);
  750. let r = stops.findIndex(stop => stop > diff);
  751. if(r === -1) r = len - 1;
  752. const d = stops[r] - stops[r - 1];
  753. return `#${[[1, 3], [3, 5], [5, 7]]
  754. .map(_ => [Number.parseInt(vals[r].slice(..._), 16), Number.parseInt(vals[r-1].slice(..._), 16)])
  755. .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"))
  756. .join("")
  757. }`;
  758. }
  759. const ListItemWorker = (ele, data) => {
  760. const isLazer = window.location.hostname.split(".")[0] === "lazer"; // assume that hostname can only be osu.ppy.sh or lazer.ppy.sh
  761. if(ele.getAttribute("improved") !== null) return;
  762. ele.setAttribute("improved", "");
  763. if(data.pp){
  764. const pptext = ele.querySelector(".play-detail__pp > span").childNodes[0];
  765. pptext.nodeValue = Number(data.pp).toPrecision(5);
  766. }
  767. const left = ele.querySelector("div.play-detail__group.play-detail__group--top");
  768. 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);`});
  769. left.parentElement.insertBefore(leftc, left);
  770. const detail= ele.querySelector("div.play-detail__score-detail-top-right");
  771. const du = detail.children[0];
  772. if(!detail.children[1]) detail.append(HTML("div", {classList: "play-detail__pp-weight"}));
  773. const db = detail.children[1];
  774. data.statistics.perfect ??= 0, data.statistics.great ??= 0, data.statistics.good ??= 0, data.statistics.ok ??= 0, data.statistics.meh ??= 0, data.statistics.miss ??= 0;
  775. const bmName = ele.querySelector("span.play-detail__beatmap");
  776. const sr = HTML("div", {class: `difficulty-badge ${data.beatmap.difficulty_rating >= 6.7 ? "difficulty-badge--expert-plus" : ""}`, style: `--bg: ${DiffToColour(data.beatmap.difficulty_rating)}`},
  777. HTML("span", {class: "difficulty-badge__icon"}, HTML("span", {class: "fas fa-star"})),
  778. HTML("span", {class: "difficulty-badge__rating"}, HTML(`${data.beatmap.difficulty_rating.toFixed(2)}`))
  779. );
  780. bmName.parentElement.insertBefore(sr, bmName);
  781. switch(data.ruleset_id){
  782. case 0:{
  783. du.replaceChildren(
  784. HTML("span", {class: "play-detail__combo", title: `Combo${isLazer ? "/Max Combo" : ""}`},
  785. 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}`)),
  786. isLazer ? HTML("/") : null,
  787. isLazer ? HTML("span", {class: "max-combo"}, HTML(`${(data.maximum_statistics.great ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0)}`)) : null,
  788. HTML("x"),
  789. ),
  790. HTML("span", {class: "play-detail__Accuracy", title: `${isLazer ? "V2" : "V1"} Accuracy`}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  791. );
  792. const m_300 = HTML("span", {class: "score-detail score-detail-osu-300"},
  793. HTML("span", {class: "osu-300"},
  794. HTML("300")
  795. ),
  796. HTML("span", {class: "score-detail-data-text"},
  797. HTML(`${data.statistics.great + data.statistics.perfect}`)
  798. )
  799. );
  800. const s100 = HTML("span", {class: "score-detail score-detail-osu-100"},
  801. HTML("span", {class: "osu-100"},
  802. HTML("100")
  803. ),
  804. HTML("span", {class: "score-detail-data-text"},
  805. HTML(`${data.statistics.ok + data.statistics.good}`)
  806. )
  807. );
  808. const s50 = HTML("span", {class: "score-detail score-detail-osu-50"},
  809. HTML("span", {class: "osu-50"},
  810. HTML("50")
  811. ),
  812. HTML("span", {class: "score-detail-data-text"},
  813. HTML(`${data.statistics.meh}`)
  814. )
  815. );
  816. const s0 = HTML("span", {class: "score-detail score-detail-osu-miss"},
  817. HTML("span", {class: "osu-miss"},
  818. HTML("img", {src: svg_osu_miss, alt: "miss"})
  819. ),
  820. HTML("span", {class: "score-detail-data-text"},
  821. HTML(`${data.statistics.miss}`)
  822. )
  823. );
  824. db.replaceChildren(m_300, s100, s50, s0);
  825. break;
  826. }
  827. case 1:{
  828. const cur = [data.statistics.great ?? 0, data.statistics.ok ?? 0, data.statistics.miss ?? 0];
  829. const mx = cur[0] + cur[1] + cur[2];
  830. du.replaceChildren(
  831. HTML("span", {class: "play-detail__combo", title: `Combo/Max Combo`},
  832. HTML("span", {class: `combo ${(data.max_combo === mx ? "legacy-perfect-combo" : "")}`}, HTML(`${data.max_combo}`)),
  833. HTML("/"),
  834. HTML("span", {class: "max-combo"}, HTML(`${mx}`)),
  835. HTML("x"),
  836. ),
  837. HTML("span", {class: "play-detail__Accuracy"}, HTML(`Acc: ${(data.accuracy * 100).toFixed(2)}%`)),
  838. );
  839. db.replaceChildren(
  840. HTML("span", {class: "score-detail score-detail-taiko-300"},
  841. HTML("span", {class: "taiko-300"}, HTML("300")),
  842. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.great ?? 0))
  843. ),
  844. HTML("span", {class: "score-detail score-detail-taiko-150"},
  845. HTML("span", {class: "taiko-150"}, HTML("150")),
  846. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.ok ?? 0))
  847. ),
  848. HTML("span", {class: "score-detail score-detail-fruits-combo"},
  849. HTML("span", {class: "taiko-miss"}, HTML("miss")),
  850. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss ?? 0))
  851. )
  852. );
  853. break;
  854. }
  855. case 2:{
  856. if (isLazer) {
  857. const cur = [data.statistics.great ?? 0, data.statistics.large_tick_hit ?? 0, data.statistics.small_tick_hit ?? 0];
  858. const mx = [data.maximum_statistics.great ?? 0, data.maximum_statistics.large_tick_hit ?? 0, data.maximum_statistics.small_tick_hit ?? 0];
  859. du.replaceChildren(
  860. HTML("span", {class: "play-detail__combo", title: `Combo/Max Combo`},
  861. HTML("span", {class: `combo ${(data.max_combo === mx[0] + mx[1] ? "legacy-perfect-combo" : "")}`}, HTML(`${data.max_combo}`)),
  862. isLazer ? HTML("/") : null,
  863. isLazer ? HTML("span", {class: "max-combo"}, HTML(`${mx[0] + mx[1]}`)) : null,
  864. HTML("x"),
  865. ),
  866. HTML("span", {class: "play-detail__Accuracy"}, HTML(`Acc: ${(data.accuracy * 100).toFixed(2)}%`)),
  867. );
  868. db.replaceChildren(
  869. HTML("span", {class: "score-detail score-detail-fruits-300"},
  870. HTML("span", {class: "fruits-300"}, HTML("fruits")),
  871. HTML("span", {class: "score-detail-data-text"}, HTML(cur[0] + "/" + mx[0]))
  872. ),
  873. HTML("span", {class: "score-detail score-detail-fruits-100"},
  874. HTML("span", {class: "fruits-100"}, HTML("ticks")),
  875. HTML("span", {class: "score-detail-data-text"}, HTML(cur[1] + "/" + mx[1]))
  876. ),
  877. HTML("span", {class: "score-detail score-detail-fruits-50-miss"},
  878. HTML("span", {class: "fruits-50-miss"}, HTML("drops")),
  879. HTML("span", {class: "score-detail-data-text"}, HTML(cur[2] + "/" + mx[2]))
  880. )
  881. );
  882. } else {
  883. du.replaceChildren(
  884. HTML("span", {class: "play-detail__Accuracy"}, HTML(`Acc: ${(data.accuracy * 100).toFixed(2)}%`)),
  885. );
  886. db.replaceChildren(
  887. HTML("span", {class: "score-detail score-detail-fruits-300"},
  888. HTML("span", {class: "fruits-300"}, HTML("FRUIT")),
  889. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.great ?? 0))
  890. ),
  891. HTML("span", {class: "score-detail score-detail-fruits-100"},
  892. HTML("span", {class: "fruits-100"}, HTML("tick")),
  893. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.large_tick_hit ?? 0))
  894. ),
  895. HTML("span", {class: "score-detail score-detail-fruits-50-miss"},
  896. HTML("span", {class: "fruits-50-miss"}, HTML("miss")),
  897. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.small_tick_miss ?? 0))
  898. ),
  899. HTML("span", {class: "score-detail score-detail-fruits-miss"},
  900. HTML("span", {class: "fruits-miss"}, HTML("MISS")),
  901. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss ?? 0))
  902. )
  903. );
  904. }
  905. break;
  906. }
  907. case 3:{
  908. 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));
  909. const MCombo = (data.maximum_statistics.perfect ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0);
  910. const isMCombo = isLazer ? data.max_combo >= MCombo : data.legacy_perfect;
  911. du.replaceChildren(
  912. HTML("span", {class: "play-detail__combo", title: `Combo${isLazer ? "/Max Combo" : ""}`},
  913. HTML("span", {class: `combo ${isMCombo ? "legacy-perfect-combo" : ""}`}, HTML(`${data.max_combo}`)),
  914. isLazer ? HTML("/") : null,
  915. isLazer ? HTML("span", {class: "max-combo"}, HTML(MCombo)) : null,
  916. HTML("x"),
  917. ),
  918. HTML("span", {class: "play-detail__Accuracy", title: `Score${isLazer ? "V2" : "V1"} Accuracy`}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  919. HTML("span", {class: "play-detail__Accuracy2", title: `pp Accuracy`}, HTML(`${(v2acc * 100).toFixed(2)}%`)),
  920. );
  921. if(data.pp){
  922. const lostpp = data.pp * (0.2 / (Math.min(Math.max(v2acc, 0.8), 1) - 0.8) - 1);
  923. ele.querySelector(".play-detail__pp").appendChild(HTML("span", {class: "lost-pp"}, HTML(`-${lostpp.toPrecision(4)}`)));
  924. }
  925. const M_300 = Number(data.statistics.perfect) / Math.max(Number(data.statistics.great), 1);
  926. db.replaceChildren(
  927. HTML("span", {class: "score-detail score-detail-mania-max-300"},
  928. HTML("span", {class: "mania-max"}, HTML("M")),
  929. HTML("/"),
  930. HTML("span", {class: "mania-300"}, HTML("300")),
  931. 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))}`))
  932. ),
  933. HTML("span", {class: "score-detail score-detail-mania-max-200"},
  934. HTML("span", {class: "mania-200"}, HTML("200")),
  935. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.good))
  936. ),
  937. HTML("span", {class: "score-detail score-detail-mania-max-100"},
  938. HTML("span", {class: "mania-100"}, HTML("100")),
  939. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.ok))
  940. ),
  941. HTML("span", {class: "score-detail score-detail-mania-max-50"},
  942. HTML("span", {class: "mania-50"}, HTML("50")),
  943. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.meh))
  944. ),
  945. HTML("span", {class: "score-detail score-detail-mania-max-0"},
  946. HTML("span", {class: "mania-miss"}, HTML("miss")),
  947. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss))
  948. )
  949. );
  950. break;
  951. }
  952. }
  953. }
  954. let lastLocationStr = "";
  955. let lastInitData;
  956. const OsuLevelToExp = (n) => {
  957. if(n <= 100) return 5000 / 3 * (4 * n ** 3 - 3 * n ** 2 - n) + 1.25 * 1.8 ** (n - 60);
  958. else return 26_931_190_827 + 99_999_999_999 * (n - 100);
  959. }
  960. const OsuExpValToStr = (num) => {
  961. let exp = Math.log10(num);
  962. if(exp >= 12){
  963. return `${(num / 10 ** 12).toPrecision(4)}T`;
  964. }
  965. else if(exp >= 9){
  966. return `${(num / 10 ** 9).toPrecision(4)}B`;
  967. }
  968. else if(exp >= 6){
  969. return `${(num / 10 ** 6).toPrecision(4)}M`;
  970. }
  971. else if(exp >= 4){
  972. return `${(num / 10 ** 3).toPrecision(4)}K`;
  973. }
  974. else return `${num}`;
  975. }
  976. const messageCache = [];
  977. let lastUserId, lastModestr;
  978. const ImproveProfile = (message) => {
  979. let initData;
  980. if(window.location.toString() === lastLocationStr){
  981. initData = lastInitData;
  982. }
  983. else{
  984. initData = JSON.parse(document.querySelector(".js-react--profile-page.osu-layout.osu-layout--full").dataset.initialData);
  985. lastLocationStr = window.location.toString();
  986. lastInitData = initData;
  987. }
  988. const userId = initData.user.id;
  989. const modestr = initData.current_mode;
  990. if(!(userId === message.userId && modestr === message.mode)) return;
  991. const ttscore = initData.user.statistics.total_score;
  992. const lvl = initData.user.statistics.level.current;
  993. const upgradescore = Math.round(OsuLevelToExp(lvl + 1) - OsuLevelToExp(lvl));
  994. const lvlscore = ttscore - Math.round(OsuLevelToExp(lvl));
  995. document.querySelector("div.bar__text").textContent = `${OsuExpValToStr(lvlscore)}/${OsuExpValToStr(upgradescore)} (${(lvlscore/upgradescore * 100).toPrecision(3)}%)`;
  996. let ppDiv;
  997. document.querySelectorAll("div.value-display.value-display--plain").forEach((ele) => {
  998. if(ele.querySelector("div.value-display__label").textContent === "pp") ppDiv = ele;
  999. });
  1000. ppDiv.querySelector(".value-display__value > div").textContent = Number(initData.user.statistics.pp).toPrecision(6);
  1001. //document.querySelector(".value-display.value-display--plain.value-display--plain-wide").textContent =
  1002. const obcb = () => {
  1003. ob.disconnect();
  1004. let result = true;
  1005. switch(message.type){
  1006. case "top_ranks":
  1007. result &&= TopRanksWorker(message.data.pinned.items, 0);
  1008. result &&= TopRanksWorker(message.data.best.items, 1);
  1009. result &&= TopRanksWorker(message.data.firsts.items, 2);
  1010. break;
  1011. case "firsts":
  1012. result &&= TopRanksWorker(message.data, 2);
  1013. break;
  1014. case "pinned":
  1015. result &&= TopRanksWorker(message.data, 0);
  1016. break;
  1017. case "best":
  1018. result &&= TopRanksWorker(message.data, 1);
  1019. break;
  1020. case "historical":
  1021. result &&= TopRanksWorker(message.data.recent.items, 0, "historical");
  1022. break;
  1023. case "recent":
  1024. result &&= TopRanksWorker(message.data, 0, "historical");
  1025. break;
  1026. }
  1027. if(!result) ob.observe(document, {subtree: true, childList: true});
  1028. };
  1029. const ob = new MutationObserver(obcb);
  1030. ob.observe(document, {subtree: true, childList: true});
  1031. obcb();
  1032. }
  1033. let wloc = "";
  1034. const WindowLocationChanged = () => {
  1035. if(window.location !== wloc){
  1036. wloc = window.location;
  1037. return true;
  1038. }
  1039. else return false;
  1040. }
  1041. const InsertStyleSheet = () => {
  1042. //const sheetId = "osu-web-enhancement-general-stylesheet";
  1043. const s = new CSSStyleSheet();
  1044. s.replaceSync(inj_style);
  1045. document.adoptedStyleSheets = [...document.adoptedStyleSheets, s];
  1046. }
  1047. const OnBeatmapsetDownload = (message) => {
  1048. beatmapsets.add(message.beatmapsetId);
  1049. }
  1050. const ImproveBeatmapPlaycountItems = () => {
  1051. for(const item of [...document.querySelectorAll("div.beatmap-playcount")]){
  1052. if(item.getAttribute("improved") !== null) continue;
  1053. item.setAttribute("improved", "");
  1054. const a = item.querySelector("a");
  1055. const bms = bmsReg.exec(a.href);
  1056. if(!bms?.[1]) continue;
  1057. const d = item.querySelector("div.beatmap-playcount__detail");
  1058. const b = HTML("div", {class: "beatmap-playcount__background", style: `background-image: url(https://assets.ppy.sh/beatmaps/${bms[1]}/covers/card@2x.jpg)`});
  1059. if(d.childElementCount > 0) d.insertBefore(b, d.children[0]);
  1060. else d.append(b);
  1061. }
  1062. }
  1063. const CloseScoreCardPopup = () => {
  1064. document.querySelector("div.score-card-popup-window").remove();
  1065. }
  1066. const ShowScoreCardPopup = () => {
  1067. const p = document.querySelector("div.js-portal");
  1068. if(!p) return;
  1069. const bmsId =
  1070. document.body.append(
  1071. HTML("div", {class: "score-card-popup-window"},
  1072. HTML("div", {class: "score-card-popup-menu"},
  1073. HTML("button", {class: "score-card-close-button", eventListener: {type: "click", listener: CloseScoreCardPopup}}),
  1074. HTML("button", {class: "score-card-copy-to-clipboard-button", ev}),
  1075. ),
  1076. HTML("div", {class: "score-card"},
  1077. )
  1078. )
  1079. );
  1080. };
  1081. const AddPopupButton = () => {
  1082. const p = document.querySelector("div.js-portal")?.querySelector("div.simple-menu");
  1083. if(!p || p.querySelector("button.score-card-popup-button")) return;
  1084. p.append(HTML("button", {class: "score-card-popup-button simple-menu__item", type: "button", eventListener: [{type: "click", listener: ShowScoreCardPopup}]}, HTML("Popup")));
  1085. };
  1086. const OnMutation = (mulist) => {
  1087. mut.disconnect();
  1088. AddMenu();
  1089. FilterBeatmapSet();
  1090. ImproveBeatmapPlaycountItems();
  1091. //AddPopupButton();
  1092. mut.observe(document, {childList: true, subtree: true});
  1093. };
  1094. const MessageFilter = (message) => {
  1095. switch(message.type){
  1096. case "beatmapset_download_complete": OnBeatmapsetDownload(message); break;
  1097. }
  1098. }
  1099. const WindowMessageFilter = (event) => {
  1100. if(event.source === window && event?.data?.id === "osu!web enhancement"){
  1101. ImproveProfile(event.data);
  1102. }
  1103. }
  1104. const OnClick = (event) => {
  1105. let t = event.target;
  1106. while(t){
  1107. if(t.tagName === "A"){
  1108. const e = bmsdlReg.exec(t.href);
  1109. if(!e) continue;
  1110. beatmapsets.add(Number(e[1]));
  1111. FilterBeatmapSet();
  1112. break;
  1113. }
  1114. t = t.parentElement;
  1115. }
  1116. }
  1117. //document.addEventListener("click", OnClick);
  1118. window.addEventListener("message", WindowMessageFilter);
  1119. const mut = new MutationObserver(OnMutation);
  1120. mut.observe(document, {childList: true, subtree: true});
  1121. InsertStyleSheet();
  1122. //{id, mode} -> (bmid -> record)
  1123. console.log("osu!web enhancement loaded");

QingJ © 2025

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