osu!web enhancement

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

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

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