osu!web enhancement

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

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

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