osu!web enhancement

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

  1. // ==UserScript==
  2. // @name osu!web enhancement
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.7.0
  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. // below are source code from http://i18njs.com/js/i18n.js
  14. (function() {
  15. var Translator, i18n, translator,
  16. __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
  17. Translator = (function() {
  18. function Translator() {
  19. this.translate = __bind(this.translate, this); this.data = {
  20. values: {},
  21. contexts: []
  22. };
  23. this.globalContext = {};
  24. }
  25. Translator.prototype.translate = function(text, defaultNumOrFormatting, numOrFormattingOrContext, formattingOrContext, context) {
  26. var defaultText, formatting, isObject, num;
  27. if (context == null) {
  28. context = this.globalContext;
  29. }
  30. isObject = function(obj) {
  31. var type;
  32. type = typeof obj;
  33. return type === "function" || type === "object" && !!obj;
  34. };
  35. if (isObject(defaultNumOrFormatting)) {
  36. defaultText = null;
  37. num = null;
  38. formatting = defaultNumOrFormatting;
  39. context = numOrFormattingOrContext || this.globalContext;
  40. } else {
  41. if (typeof defaultNumOrFormatting === "number") {
  42. defaultText = null;
  43. num = defaultNumOrFormatting;
  44. formatting = numOrFormattingOrContext;
  45. context = formattingOrContext || this.globalContext;
  46. } else {
  47. defaultText = defaultNumOrFormatting;
  48. if (typeof numOrFormattingOrContext === "number") {
  49. num = numOrFormattingOrContext;
  50. formatting = formattingOrContext;
  51. context = context;
  52. } else {
  53. num = null;
  54. formatting = numOrFormattingOrContext;
  55. context = formattingOrContext || this.globalContext;
  56. }
  57. }
  58. }
  59. if (isObject(text)) {
  60. if (isObject(text['i18n'])) {
  61. text = text['i18n'];
  62. }
  63. return this.translateHash(text, context);
  64. } else {
  65. return this.translateText(text, num, formatting, context, defaultText);
  66. }
  67. };
  68. Translator.prototype.add = function(d) {
  69. var c, k, v, _i, _len, _ref, _ref1, _results;
  70. if ((d.values != null)) {
  71. _ref = d.values;
  72. for (k in _ref) {
  73. v = _ref[k];
  74. this.data.values[k] = v;
  75. }
  76. }
  77. if ((d.contexts != null)) {
  78. _ref1 = d.contexts;
  79. _results = [];
  80. for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
  81. c = _ref1[_i];
  82. _results.push(this.data.contexts.push(c));
  83. }
  84. return _results;
  85. }
  86. };
  87. Translator.prototype.setContext = function(key, value) {
  88. return this.globalContext[key] = value;
  89. };
  90. Translator.prototype.clearContext = function(key) {
  91. return this.lobalContext[key] = null;
  92. };
  93. Translator.prototype.reset = function() {
  94. this.data = {
  95. values: {},
  96. contexts: []
  97. };
  98. return this.globalContext = {};
  99. };
  100. Translator.prototype.resetData = function() {
  101. return this.data = {
  102. values: {},
  103. contexts: []
  104. };
  105. };
  106. Translator.prototype.resetContext = function() {
  107. return this.globalContext = {};
  108. };
  109. Translator.prototype.translateHash = function(hash, context) {
  110. var k, v;
  111.  
  112. for (k in hash) {
  113. v = hash[k];
  114. if (typeof v === "string") {
  115. hash[k] = this.translateText(v, null, null, context);
  116. }
  117. }
  118. return hash;
  119. };
  120. Translator.prototype.translateText = function(text, num, formatting, context, defaultText) {
  121. var contextData, result;
  122. if (context == null) { context = this.globalContext; }
  123. if (this.data == null) { return this.useOriginalText(defaultText || text, num, formatting); }
  124. contextData = this.getContextData(this.data, context);
  125. if (contextData != null) { result = this.findTranslation(text, num, formatting, contextData.values, defaultText); }
  126. if (result == null) { result = this.findTranslation(text, num, formatting, this.data.values, defaultText); }
  127. if (result == null) { return this.useOriginalText(defaultText || text, num, formatting); }
  128. return result;
  129. };
  130. Translator.prototype.findTranslation = function(text, num, formatting, data) {
  131. var result, triple, value, _i, _len;
  132. value = data[text];
  133. if (value == null) { return null; }
  134. if (num == null) {
  135. if (typeof value === "string") { return this.applyFormatting(value, num, formatting); }
  136. } else {
  137. if (value instanceof Array || value.length) {
  138. for (_i = 0, _len = value.length; _i < _len; _i++) {
  139. triple = value[_i];
  140. if ((num >= triple[0] || triple[0] === null) && (num <= triple[1] || triple[1] === null)) {
  141. result = this.applyFormatting(triple[2].replace("-%n", String(-num)), num, formatting);
  142. return this.applyFormatting(result.replace("%n", String(num)), num, formatting);
  143. }
  144. }
  145. }
  146. }
  147. return null;
  148. };
  149. Translator.prototype.getContextData = function(data, context) {
  150. var c, equal, key, value, _i, _len, _ref, _ref1;
  151. if (data.contexts == null) { return null; }
  152. _ref = data.contexts;
  153. for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  154. c = _ref[_i];
  155. equal = true;
  156. _ref1 = c.matches;
  157. for (key in _ref1) {
  158. value = _ref1[key];
  159. equal = equal && value === context[key];
  160. }
  161. if (equal) { return c; }
  162. }
  163. return null;
  164. };
  165. Translator.prototype.useOriginalText = function(text, num, formatting) {
  166. if (num == null) {
  167. return this.applyFormatting(text, num, formatting);
  168. }
  169. return this.applyFormatting(text.replace("%n", String(num)), num, formatting);
  170. };
  171. Translator.prototype.applyFormatting = function(text, num, formatting) {
  172. var ind, regex;
  173. for (ind in formatting) {
  174. regex = new RegExp("%{" + ind + "}", "g");
  175. text = text.replace(regex, formatting[ind]);
  176. }
  177. return text;
  178. };
  179. return Translator;
  180. })();
  181. translator = new Translator();
  182. i18n = translator.translate;
  183. i18n.translator = translator;
  184. i18n.create = function(data) {
  185. var trans;
  186. trans = new Translator();
  187. if (data != null) {
  188. trans.add(data);
  189. }
  190. trans.translate.create = i18n.create;
  191. return trans.translate;
  192. };
  193. (typeof module !== "undefined" && module !== null ? module.exports = i18n : void 0) || (this.i18n = i18n);
  194. }).call(this);
  195. // end of code from http://i18njs.com/js/i18n.js
  196. const ShowPopup = (m, t = "info") => {
  197. window.popup(m, t);
  198. [["info", console.log], ["warning", console.warn], ["danger", console.error]].find(g => g[0] === t)[1](m);
  199. }
  200. const svg_osu_miss = URL.createObjectURL(new Blob(
  201. [`<svg viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
  202. <filter id="blur">
  203. <feFlood flood-color="red" flood-opacity="0.5" in="SourceGraphic" />
  204. <feComposite operator="in" in2="SourceGraphic" />
  205. <feGaussianBlur stdDeviation="6" />
  206. <feComponentTransfer result="glow1"> <feFuncA type="linear" slope="10" intercept="0" /> </feComponentTransfer>
  207. <feGaussianBlur in="glow1" stdDeviation="1" result="glow2" />
  208. <feMerge> <feMergeNode in="SourceGraphic" /> <feMergeNode in="glow2" /> </feMerge>
  209. </filter>
  210. <filter id="blur2"> <feGaussianBlur stdDeviation="0.2"/> </filter>
  211. <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" />
  212. <use href="#cross" stroke="red" stroke-width="2" fill="transparent" filter="url(#blur)"/>
  213. <use href="#cross" fill="white" stroke="transparent" filter="url(#blur2)"/>
  214. </svg>`], {type: "image/svg+xml"}));
  215. const svg_green_tick = URL.createObjectURL(new Blob([
  216. `<svg viewBox="0 0 18 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
  217. <polyline points="2,8 7,14 16,2" stroke="#62ee56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
  218. </svg>`], {type: "image/svg+xml"}));
  219. const inj_style =
  220. `#osu-db-input{
  221. display: none;
  222. }
  223. .osu-db-button{
  224. align-items: center;
  225. padding: 10px;
  226. }
  227. .osu-db-button:hover{
  228. cursor: pointer;
  229. }
  230. .beatmapset-panel[owned-beatmapset] .beatmapset-panel__menu-container{
  231. background-color: #87dda8;
  232. }
  233. .beatmapset-panel[owned-beatmapset] .beatmapset-panel__menu .fa-file-download, .beatmapset-panel[owned-beatmapset] .beatmapset-panel__menu .fa-heart{
  234. color: #5c9170;
  235. }
  236. .owned-beatmap-link{
  237. color: #87dda8;
  238. }
  239. .play-detail__accuracy{
  240. margin: 0px 12px;
  241. }
  242. .play-detail__accuracy.ppAcc{
  243. color: #8ef9f1;
  244. padding: 0;
  245. }
  246. .play-detail__weighted-pp{
  247. margin: 0px;
  248. }
  249. .play-detail__pp{
  250. flex-direction: column;
  251. }
  252. .lost-pp{
  253. font-size: 10px;
  254. position: relative;
  255. right: 7px;
  256. font-weight: 600;
  257. }
  258. .score-detail{
  259. display: inline-block;
  260. }
  261. .score-detail-data-text{
  262. margin-left: 5px;
  263. margin-right: 10px;
  264. width: auto;
  265. display: inline-block;
  266. }
  267. @keyframes rainbow{
  268. 0%{
  269. color: #be19ff;
  270. }
  271. 25%{
  272. color: #0075ff;
  273. }
  274. 50%{
  275. color: #4ddf86;
  276. }
  277. 75%{
  278. color: #e9ea00;
  279. }
  280. 100%{
  281. color: #ff7800;
  282. }
  283. }
  284. .play-detail__accuracy-and-weighted-pp{
  285. display: flex;
  286. flex-direction: row;
  287. }
  288. .play-detail__before{
  289. flex-grow: 1;
  290. }
  291. .mania-max{
  292. animation: 0.16s infinite alternate rainbow;
  293. }
  294. .mania-300{
  295. color: #fbff00;
  296. }
  297. .osu-100, .fruits-100, .taiko-150{
  298. color: #67ff5b;
  299. }
  300. .mania-200{
  301. color: #6cd800;
  302. }
  303. .osu-300, .fruits-300, .taiko-300{
  304. color: #7dfbff;
  305. }
  306. .mania-100{
  307. color: #257aea;
  308. }
  309. .mania-50{
  310. color: #d2d2d2;
  311. }
  312. .osu-50, .fruits-50-miss{
  313. color: #ffbf00;
  314. }
  315. .mania-miss, .taiko-miss, .fruits-miss{
  316. color: #cc2626;
  317. }
  318. .mania-max, .mania-300, .mania-200, .mania-100, .mania-50, .mania-miss, .osu-300, .osu-100, .osu-50, .osu-miss{
  319. font-weight: 600;
  320. }
  321. .score-detail-data-text{
  322. font-weight: 500;
  323. }
  324. .osu-miss{
  325. display: inline-block;
  326. }
  327. .osu-miss > img{
  328. width: 14px;
  329. height: 14px;
  330. bottom: 1px;
  331. position: relative;
  332. }
  333. .play-detail__Accuracy, .play-detail__Accuracy2, .combo, .max-combo, .play-detail__combo{
  334. display: inline-block;
  335. width: auto;
  336. }
  337. .play-detail__Accuracy{
  338. text-align: left;
  339. color: #fc2;
  340. }
  341. .play-detail__Accuracy2{
  342. text-align: left;
  343. color: rgb(142, 249, 241);
  344. }
  345. .play-detail__combo, .play-detail__Accuracy2, .play-detail__Accuracy{
  346. margin-right: 13px;
  347. }
  348. .play-detail__combo{
  349. text-align: right;
  350. }
  351. .combo, .max-combo{
  352. margin: 0px 1px;
  353. }
  354. .max-combo, .legacy-perfect-combo{
  355. color: hsl(var(--hsl-lime-1));
  356. }
  357. div.bar__exp-info{
  358. position: relative;
  359. bottom: 100%;
  360. }
  361. .play-detail__group--background, .beatmap-playcount__background{
  362. position: absolute;
  363. width: 90%;
  364. height: 100%;
  365. left: 0px;
  366. margin: 0px;
  367. pointer-events: none;
  368. z-index: 1;
  369. border-radius: 10px 0px 0px 10px;
  370. background-size: cover;
  371. background-position-y: -100%;
  372. mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));
  373. -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));
  374. }
  375. @media(max-width: 900px){
  376. .play-detail__group--background, .beatmap-playcount__background{
  377. background-position-y: 0%;
  378. mask-image: linear-gradient(to bottom, #0007, #0004);
  379. -webkit-mask-image: linear-gradient(to bottom, #0007, #0004);
  380. width: 100%;
  381. }
  382. .lost-pp{
  383. left: 3px;
  384. }
  385. .play-detail__group.play-detail__group--bottom{
  386. z-index: 1;
  387. }
  388. .play-detail__before{
  389. flex-grow: 0;
  390. }
  391. }
  392. .play-detail.play-detail--highlightable.play-detail--pin-sortable.js-score-pin-sortable .play-detail__group--background{
  393. left: 20px;
  394. }
  395. .beatmap-playcount__background{
  396. width: 100%;
  397. border-radius: 6px;
  398. mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
  399. -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));
  400. }
  401. .beatmap-playcount__info, .beatmap-playcount__detail-count, .play-detail__group.play-detail__group--top *{
  402. z-index: 1;
  403. }
  404. div.play-detail-list time.js-timeago, span.beatmap-playcount__mapper, span.beatmap-playcount__mapper > a{
  405. color: #ccc;
  406. }
  407. button.show-more-link{
  408. z-index: 4;
  409. }
  410. a.beatmap-download-link{
  411. margin: 0px 5px;
  412. color: hsl(var(--hsl-l1));
  413. }
  414. a.beatmap-download-link:hover, a.beatmap-pack-item-download-link span:hover{
  415. color: #fff;
  416. }
  417. a.beatmap-pack-item-download-link span{
  418. color: hsl(var(--hsl-l1));
  419. }
  420. .play-detail.play-detail--highlightable.audio-player{
  421. max-width: none;
  422. height: unset;
  423. padding: unset;
  424. align-items: unset;
  425. }
  426. .play-detail.play-detail--highlightable.audio-player__button{
  427. align-items: unset;
  428. padding: unset;
  429. }
  430. .play-detail.play-detail--highlightable.audio-player__button:hover{
  431. color: unset;
  432. }
  433. .sort-detail__items{
  434. display: flex;
  435. align-items: center;
  436. flex-wrap: wrap;
  437. }
  438. .sort-detail__item{
  439. border-radius: 4px;
  440. margin: 5px;
  441. }
  442. `;
  443. const scriptContent =
  444. String.raw`console.log("page script injected from osu!web enhancement");
  445. if(window.oldXHROpen === undefined){
  446. window.oldXHROpen = window.XMLHttpRequest.prototype.open;
  447. window.XMLHttpRequest.prototype.open = function() {
  448. this.addEventListener("load", function() {
  449. const url = this.responseURL;
  450. 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);
  451. 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);
  452. let reg = trreg ?? (adreg ?? null);
  453. if(!reg){
  454. const bmsreg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/search\?/;
  455. return;
  456. }
  457. let info = {
  458. type: reg.groups.type,
  459. userId: Number(reg.groups.id),
  460. mode: reg.groups.mode,
  461. subdomain: reg.groups.subdomain,
  462. };
  463. const responseBody = this.responseText;
  464. info.data = JSON.parse(responseBody);
  465. info.id = "osu!web enhancement";
  466. window.postMessage(info, "*");
  467. });
  468. return oldXHROpen.apply(this, arguments);
  469. };
  470. }`;
  471. const locales = {
  472. "en": {
  473. "values": {
  474. "Owned": "Owned",
  475. "Download": "Download",
  476. "pp Accuracy": "pp Accuracy",
  477. "V1 Accuracy": "V1 Accuracy",
  478. "V2 Accuracy": "V2 Accuracy",
  479. "Lazer Mode Accuracy": "Lazer Mode Accuracy",
  480. "Stable Mode Accuracy": "Stable Mode Accuracy",
  481. "Combo": "Combo",
  482. "Combo/Max Combo": "Combo/Max Combo",
  483. "Import osu!.db": "Import osu!.db",
  484. "Check for update": "Check for update",
  485. "Calculate pp Gini index": "Calculate pp Gini index",
  486. "Go to GreasyFork page": "Go to GreasyFork page",
  487. "Copy Text Details": "Copy Text Details",
  488. "Could not find best play data": "Could not find best play data",
  489. "The latest version is already installed!": "The latest version is already installed!",
  490. "Script is already busy reading a osu!.db file.": "Script is already busy reading a osu!.db file.",
  491. "There are still remaining unread bytes, something may be wrong.": "There are still remaining unread bytes, something may be wrong.",
  492. "Score details copied to clipboard!": "Score details copied to clipboard!",
  493. "%{pc} of total pp": "%{pc} of total pp",
  494. "Your pp Gini index of bp%{bp} is %{val}.": "Your pp Gini index of bp%{bp} is %{val}.",
  495. "Finished reading osu!.db in %{time} ms.": "Finished reading osu!.db in %{time} ms.",
  496. "MAX: %{MAX} 300: %{MAX}": "MAX: %{MAX} 300: %{MAX}",
  497. "Unable to copy score detail to clipboard, check console for more info.": "Unable to copy score detail to clipboard, check console for more info.",
  498. "Show bp analytic": "Show bp analytic",
  499. "Close bp analytic": "Close bp analytic",
  500. }
  501. },
  502. "zh": {
  503. "values": {
  504. "Owned": "已获得",
  505. "Download": "下载",
  506. "pp Accuracy": "pp-准确度",
  507. "V1 Accuracy": "V1-准确度",
  508. "V2 Accuracy": "V2-准确度",
  509. "Lazer Accuracy": "Lazer-准确度",
  510. "Lazer Mode Accuracy": "Lazer准确度",
  511. "Stable Mode Accuracy": "Stable准确度",
  512. "Combo": "连击数",
  513. "Combo/Max Combo": "连击数/最大连击数",
  514. "Import osu!.db": "读取 osu!.db",
  515. "Check for update": "检查更新",
  516. "Calculate pp Gini index": "计算 pp 基尼指数",
  517. "Go to GreasyFork page": "前往 GreasyFork 页面",
  518. "Copy Text Details": "复制文本信息",
  519. "Could not find best play data": "无法获取 BP 数据",
  520. "The latest version is already installed!": "已安装最新版本!",
  521. "Script is already busy reading a osu!.db file.": "脚本已经开始读取 osu!.db 文件。",
  522. "There are still remaining unread bytes, something may be wrong.": "部分数据未能读取,可能发生错误。",
  523. "Score details copied to clipboard!": "分数信息已复制到剪贴板!",
  524. "%{pc} of total pp": "占总 pp 的 %{pc}",
  525. "Your pp Gini index of bp%{bp} is %{val}.": "BP%{bp} 的 pp 基尼指数为 %{val}。",
  526. "Finished reading osu!.db in %{time} ms.": "osu!.db 读取完毕,用时 %{time}ms。",
  527. "MAX: %{MAX} 300: %{MAX}": "MAX: %{MAX} 300: %{MAX}",
  528. }
  529. },
  530. "zh-tw": {
  531. "values": {
  532. "Owned": "已獲得",
  533. "Download": "下載",
  534. "pp Accuracy": "pp-準確度",
  535. "V1 Accuracy": "V1-準確度",
  536. "V2 Accuracy": "V2-準確度",
  537. "Lazer Accuracy": "Lazer-準確度",
  538. "Combo": "連擊數",
  539. "Combo/Max Combo": "連擊數/最大連擊數",
  540. "Import osu!.db": "讀取 osu!.db",
  541. "Check for update": "檢查更新",
  542. "Calculate pp Gini index": "計算 pp 基尼指數",
  543. "Go to GreasyFork page": "前往 GreasyFork 頁面",
  544. "Copy Text Details": "複製文字訊息",
  545. "Could not find best play data": "无法獲取 BP 數據",
  546. "The latest version is already installed!": "已安裝最新版本!",
  547. "Script is already busy reading a osu!.db file.": "腳本已經開始讀取 osu!.db 文件。",
  548. "There are still remaining unread bytes, something may be wrong.": "部分數據未能讀取,可能發生錯誤。",
  549. "Score details copied to clipboard!": "分數訊息已複製到剪貼簿!",
  550. "%{pc} of total pp": "佔總 pp 的 %{pc}",
  551. "Your pp Gini index of bp%{bp} is %{val}.": "BP%{bp} 的 pp 基尼指數為 %{val}。",
  552. "Finished reading osu!.db in %{time} ms.": "osu!.db 讀取完畢,用時 %{time}ms。",
  553. "MAX: %{MAX} 300: %{MAX}": "MAX: %{MAX} 300: %{MAX}",
  554. }
  555. },
  556. "ja": {
  557. "values": {
  558. "Owned": "取得済み",
  559. "Download": "ダウンロード",
  560. "pp Accuracy": "pp-精度",
  561. "V1 Accuracy": "V1-精度",
  562. "V2 Accuracy": "V2-精度",
  563. "Lazer Accuracy": "Lazer-精度",
  564. "Combo": "コンボ数",
  565. "Combo/Max Combo": "コンボ数/最大コンボ数",
  566. "Import osu!.db": "osu!.db を読み取る",
  567. "Check for update": "更新を確認する",
  568. "Calculate pp Gini index": "pp のジニ指数の計算",
  569. "Go to GreasyFork page": "GreasyFork のページへ",
  570. "Copy Text Details": "詳細をテキストにコピー",
  571. "Could not find best play data": "BP データが見つからない",
  572. "The latest version is already installed!": "最新版は既にインストールされている!",
  573. "Script is already busy reading a osu!.db file.": "スクリプトは osu!.db ファイルの読み取りを既に始める。",
  574. "There are still remaining unread bytes, something may be wrong.": "一部のデータを読み取れません、多分何かの間違いだ。",
  575. "Score details copied to clipboard!": "スコア詳細をクリップボードにコピー!",
  576. "%{pc} of total pp": "全 pp の %{pc}",
  577. "Your pp Gini index of bp%{bp} is %{val}.": "BP%{bp} の pp ジニ指数は %{val} です。",
  578. "Finished reading osu!.db in %{time} ms.": "osu!.db の読み取りを %{time}ms で完了しました。",
  579. "MAX: %{MAX} 300: %{MAX}": "MAX: %{MAX} 300: %{MAX}",
  580. }
  581. }
  582. };
  583. const scriptId = "osu-web-enhancement-XHR-script";
  584. if(!document.querySelector(`script#${scriptId}`)){
  585. const script = document.createElement("script");
  586. script.textContent = scriptContent;
  587. document.body.appendChild(script);
  588. }
  589. const persistentEventListeners = new Map();
  590. const HTML = (tagname, attrs, ...children) => {
  591. if(attrs === undefined) return document.createTextNode(tagname);
  592. const ele = document.createElement(tagname);
  593. if(attrs) for(const [key, value] of Object.entries(attrs)){
  594. if(value === null || value === undefined) continue;
  595. if(key.charAt(0) === "_"){
  596. const type = key.slice(1);
  597. ele.addEventListener(type, value);
  598. }
  599. else if(key.charAt(0) === "#" && ele.getAttribute("id") !== null){
  600. const type = key.slice(1);
  601. persistentEventListeners.set(ele.getAttribute("id"), {type: type, value: value});
  602. ele.addEventListener(type, value);
  603. }
  604. else if(key === "eventListener"){
  605. for(const listener of value){
  606. ele.addEventListener(listener.type, listener.listener, listener.options);
  607. }
  608. }
  609. else ele.setAttribute(key, value);
  610. }
  611. for(const child of children) if(child) ele.append(child);
  612. return ele;
  613. };
  614. const html = (html) => {
  615. const t = document.createElement("template");
  616. t.innerHTML = html;
  617. return t.content.firstElementChild;
  618. };
  619. const OsuMod = {
  620. NoFail: 1 << 0,
  621. Easy: 1 << 1,
  622. TouchDevice: 1 << 2,
  623. NoVideo: 1 << 2,
  624. Hidden: 1 << 3,
  625. HardRock: 1 << 4,
  626. SuddenDeath: 1 << 5,
  627. DoubleTime: 1 << 6,
  628. Relax: 1 << 7,
  629. HalfTime: 1 << 8,
  630. Nightcore: 1 << 9, // always with DT
  631. Flashlight: 1 << 10,
  632. Autoplay: 1 << 11,
  633. SpunOut: 1 << 12,
  634. Autopilot: 1 << 13,
  635. Perfect: 1 << 14,
  636. Key4: 1 << 15,
  637. Key5: 1 << 16,
  638. Key6: 1 << 17,
  639. Key7: 1 << 18,
  640. Key8: 1 << 19,
  641. KeyMod: 1 << 19 | 1 << 18 | 1 << 17 | 1 << 16 | 1 << 15,
  642. FadeIn: 1 << 20,
  643. Random: 1 << 21,
  644. Cinema: 1 << 22,
  645. TargetPractice: 1 << 23,
  646. Key9: 1 << 24,
  647. Coop: 1 << 25,
  648. Key1: 1 << 26,
  649. Key3: 1 << 27,
  650. Key2: 1 << 28,
  651. ScoreV2: 1 << 29,
  652. Mirror: 1 << 30,
  653. };
  654. const Byte = (arr, iter) => {
  655. return arr[iter.nxtpos++];
  656. }
  657. const RankedStatus = (arr, iter) => {
  658. const r = {value: Byte(arr, iter), description: ""};
  659. switch(r.value){
  660. case 1: r.description = "unsubmitted"; break;
  661. case 2: r.description = "pending/wip/graveyard"; break;
  662. case 3: r.description = "unused"; break;
  663. case 4: r.description = "ranked"; break;
  664. case 5: r.description = "approved"; break;
  665. case 6: r.description = "qualified"; break;
  666. case 7: r.description = "loved"; break;
  667. default: r.description = "unknown"; r.value = 0;
  668. }
  669. return r;
  670. };
  671. const OsuMode = (arr, iter) => {
  672. const r = {value: Byte(arr, iter), description: ""};
  673. switch(r.value){
  674. case 1: r.description = "taiko"; break;
  675. case 2: r.description = "catch"; break;
  676. case 3: r.description = "mania"; break;
  677. default: r.value = 0; r.description = "osu";
  678. }
  679. return r;
  680. };
  681. const Grade = (arr, iter) => {
  682. const r = {value: Byte(arr, iter), description: ""};
  683. switch(r.value){
  684. case 0: r.description = "SSH"; break;
  685. case 1: r.description = "SH"; break;
  686. case 2: r.description = "SS"; break;
  687. case 3: r.description = "S"; break;
  688. case 4: r.description = "A"; break;
  689. case 5: r.description = "B"; break;
  690. case 6: r.description = "C"; break;
  691. case 7: r.description = "D"; break;
  692. default: r.description = "not played";
  693. }
  694. return r;
  695. };
  696. const Short = (arr, iter) => (arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8);
  697. const Int = (arr, iter) => { return arr[iter.nxtpos++] | arr[iter.nxtpos++] << 8 | arr[iter.nxtpos++] << 16 | arr[iter.nxtpos++] << 24; };
  698. const Long = (arr, iter) => { const r = new DataView(arr.buffer, iter.nxtpos, 8).getBigUint64(0, true); iter.nxtpos += 8; return r; };
  699. const ULEB128 = (arr, iter) => {
  700. let value = 0n, shift = 0n, peek = 0n;
  701. do{
  702. peek = BigInt(arr[iter.nxtpos++]);
  703. value |= (peek & 0x7Fn) << shift;
  704. shift += 7n;
  705. }while((peek & 0x80n) !== 0n)
  706. return value;
  707. };
  708. const Single = (arr, iter) => { const r = new DataView(arr.buffer, iter.nxtpos, 4).getFloat32(0, true); iter.nxtpos += 4; return r; };
  709. const Double = (arr, iter) => { const r = new DataView(arr.buffer, iter.nxtpos, 8).getFloat64(0, true); iter.nxtpos += 8; return r; };
  710. const Boolean = (arr, iter) => { return arr[iter.nxtpos++] !== 0x00; };
  711. const OString = (arr, iter) => {
  712. let value = "";
  713. switch(arr[iter.nxtpos++]){
  714. case 0: break;
  715. case 0x0b: {
  716. const l = ULEB128(arr, iter);
  717. const bv = new Uint8Array(arr.buffer, iter.nxtpos, Number(l));
  718. value = new TextDecoder().decode(bv);
  719. iter.nxtpos += Number(l);
  720. break;
  721. }
  722. default: console.assert(false, `error occurred while parsing osu string with the first byte.`);
  723. }
  724. return value;
  725. };
  726. const IntDouble = (arr, iter) => {
  727. const r = {int: 0, double: 0};
  728. const m1 = arr[iter.nxtpos++];
  729. console.assert(m1 === 0x08, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  730. r.int = Int(arr, iter);
  731. const m2 = arr[iter.nxtpos++];
  732. console.assert(m2 === 0x0d, `error occurred while parsing Int-Double pair at ${iter.nxtpos - 1} with value 0x${m1.toString(16)}: should be 0x8.`);
  733. r.double = Double(arr, iter);
  734. return r;
  735. };
  736. const IntDoubleArray = (arr, iter) => {
  737. const r = new Array(Int(arr, iter));
  738. for(let i = 0; i < r.length; i++) r[i] = IntDouble(arr, iter);
  739. return r;
  740. };
  741. const TimingPoint = (arr, iter) => {
  742. return {
  743. BPM: Double(arr, iter),
  744. offset: Double(arr, iter),
  745. notInherited: Boolean(arr, iter),
  746. };
  747. };
  748. const TimingPointArray = (arr, iter) => {
  749. const r = new Array(Int(arr, iter));
  750. for(let i = 0; i < r.length; i++) r[i] = TimingPoint(arr, iter);
  751. return r;
  752. };
  753. const DateTime = Long;
  754. const Beatmap = (arr, iter) => {
  755. return {
  756. bytes: (iter.osuVersion < 20191106) ? Int(arr, iter) : undefined,
  757. artistName: OString(arr, iter),
  758. artistNameUnicode: OString(arr, iter),
  759. songTitle: OString(arr, iter),
  760. songTitleUnicode: OString(arr, iter),
  761. creatorName: OString(arr, iter),
  762. difficultyName: OString(arr, iter),
  763. audioFilename: OString(arr, iter),
  764. MD5Hash: OString(arr, iter),
  765. beatmapFilename: OString(arr, iter),
  766. rankedStatus: RankedStatus(arr, iter),
  767. hitcircleCount: Short(arr, iter),
  768. sliderCount: Short(arr, iter),
  769. spinnerCount: Short(arr, iter),
  770. lastModified: Long(arr, iter),
  771. AR: iter.osuVersion < 20140609 ? Byte(arr, iter) : Single(arr, iter),
  772. CS: iter.osuVersion < 20140609 ? Byte(arr, iter) : Single(arr, iter),
  773. HP: iter.osuVersion < 20140609 ? Byte(arr, iter) : Single(arr, iter),
  774. OD: iter.osuVersion < 20140609 ? Byte(arr, iter) : Single(arr, iter),
  775. sliderVelocity: Double(arr, iter),
  776. osuSRInfoArr: (iter.osuVersion >= 20140609) ? IntDoubleArray(arr, iter) : undefined,
  777. taikoSRInfoArr: (iter.osuVersion >= 20140609) ? IntDoubleArray(arr, iter) : undefined,
  778. catchSRInfoArr: (iter.osuVersion >= 20140609) ? IntDoubleArray(arr, iter) : undefined,
  779. maniaSRInfoArr: (iter.osuVersion >= 20140609) ? IntDoubleArray(arr, iter) : undefined,
  780. drainTime: Int(arr, iter),
  781. totalTime: Int(arr, iter),
  782. audioPreviewTime: Int(arr, iter),
  783. timingPointArr: TimingPointArray(arr, iter),
  784. difficultyID: Int(arr, iter),
  785. beatmapID: Int(arr, iter),
  786. threadID: Int(arr, iter),
  787. osuGrade: Grade(arr, iter),
  788. taikoGrade: Grade(arr, iter),
  789. catchGrade: Grade(arr, iter),
  790. maniaGrade: Grade(arr, iter),
  791. offsetLocal: Short(arr, iter),
  792. stackLeniency: Single(arr, iter),
  793. mode: OsuMode(arr, iter),
  794. sourceStr: OString(arr, iter),
  795. tagStr: OString(arr, iter),
  796. offsetOnline: Short(arr, iter),
  797. titleFont: OString(arr, iter),
  798. unplayed: Boolean(arr, iter),
  799. lastTimePlayed: Long(arr, iter),
  800. isOsz2: Boolean(arr, iter),
  801. folderName: OString(arr, iter),
  802. lastTimeChecked: Long(arr, iter),
  803. ignoreBeatmapSound: Boolean(arr, iter),
  804. ignoreBeatmapSkin: Boolean(arr, iter),
  805. disableStoryboard: Boolean(arr, iter),
  806. disableVideo: Boolean(arr, iter),
  807. visualOverride: Boolean(arr, iter),
  808. uselessShort: (iter.osuVersion < 20140609) ? Short(arr, iter) : undefined,
  809. lastModified2: Int(arr, iter),
  810. scrollSpeedMania: Byte(arr, iter),
  811. };
  812. };
  813. class _ProgressBar{
  814. barEle = null;
  815. Show(){
  816. if(this.barEle) { this.barEle.style.setProperty("opacity", "1"); return; }
  817. this.barEle = HTML("div", {class: "owenhancement-progress-bar", style: "position: fixed; left: 0px; top: 0px; width: 0%; height: 3px; background-color: #fc2; opacity: 1; z-index: 999;"});
  818. document.body.insertAdjacentElement("beforebegin", this.barEle);
  819. }
  820. Progress(prog){
  821. if(this.barEle) this.barEle.style.setProperty("width", `${prog * 100}%`);
  822. if(prog >= 1) this.Hide();
  823. }
  824. Hide(){ this.barEle.style.setProperty("opacity", "0"); }
  825. };
  826. const ProgressBar = new _ProgressBar();
  827. const BeatmapArray = async (arr, iter) => {
  828. const t = 200;
  829. const r = new Array(Int(arr, iter));
  830. let l = performance.now();
  831. for(let i = 0; i < r.length; i++){
  832. r[i] = Beatmap(arr, iter);
  833. if(performance.now() - l > t){
  834. l = performance.now();
  835. ProgressBar.Progress((i + 1) / (r.length));
  836. await new Promise((res, rej) => setTimeout(() => res(), 0));
  837. }
  838. }
  839. return r;
  840. };
  841. const OsuDb = async (arr, iter) => {
  842. ProgressBar.Show();
  843. const r = {};
  844. r.version = Int(arr, iter);
  845. iter.osuVersion = r.version;
  846. r.folderCount = Int(arr, iter);
  847. r.accountUnlocked = Boolean(arr, iter);
  848. r.timeTillUnlock = DateTime(arr, iter);
  849. r.playerName = OString(arr, iter);
  850. r.beatmapArray = await BeatmapArray(arr, iter);
  851. r.permission = Int(arr, iter);
  852. ProgressBar.Hide();
  853. return r;
  854. };
  855. class ScoreDb{
  856. constructor(arr, iter){
  857.  
  858. }
  859. }
  860. const beatmapsets = new Set();
  861. const beatmaps = new Set();
  862. const bmsReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/([0-9]+)/;
  863. const bmsdlReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/([0-9]+)\/download/;
  864. const bmReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/beatmapsets\/(?:[0-9]+)#(?:mania|osu|fruits|taiko)\/([0-9]+)/;
  865. const BeatmapsetRefresh = () => {
  866. for(const bm of window.osudb.beatmapArray){
  867. beatmaps.add(bm.difficultyID);
  868. beatmapsets.add(bm.beatmapID);
  869. }
  870. OnMutation();
  871. };
  872. const NewOsuDb = async (r) => {
  873. const start = performance.now();
  874. const result = new Uint8Array(r.result);
  875. const length = result.length;
  876. console.log(`start reading osu!.db(${length} Bytes).`);
  877. const iter = {
  878. nxtpos: 0,
  879. };
  880. window.osudb = await OsuDb(result, iter);
  881. if(iter.nxtpos !== length) ShowPopup(i18n("There are still remaining unread bytes, something may be wrong."), "danger");
  882. ShowPopup(i18n("Finished reading osu!.db in %{time} ms.", {time: performance.now() - start}));
  883. };
  884. let ReadOsuDbWorking = false;
  885. const ReadOsuDb = async (file) => {
  886. if(ReadOsuDbWorking){
  887. ShowPopup(i18n("Script is already busy reading a osu!.db file."), "warning");
  888. return;
  889. }
  890. ReadOsuDbWorking = true;
  891. if(file.name !== "osu!.db"){ console.assert( false, "filename should be 'osu!.db'."); return; }
  892. const r = new FileReader();
  893. r.onload = async () => {
  894. await NewOsuDb(r);
  895. BeatmapsetRefresh();
  896. ReadOsuDbWorking = false;
  897. };
  898. r.onerror = () => console.assert(false, "error occurred while reading file.");
  899. r.readAsArrayBuffer(file);
  900. };
  901. const SelectOsuDb = (event) => {
  902. const t = event.target;
  903. const l = t.files;
  904. console.assert(l && l.length === 1, "No file or multiple files are selected.");
  905. ReadOsuDb(l[0]);
  906. };
  907. let osuAccessToken = "";
  908. let osuAccessTokenExpireTime = 0;
  909. let lock = false;
  910. let queue = [];
  911. const clientID = 34956;
  912. const clientSecret = "PKT6PQoydMhjFq9jNRCJsIUV9hSXfQ7PPEiWmg7J";
  913. const GetToken = async () => {
  914. if(!lock){
  915. lock = true;
  916. if(osuAccessToken === "" || new Date().getTime() > osuAccessTokenExpireTime){
  917. const response = await fetch("https://osu.ppy.sh/oauth/token", {
  918. method: "POST",
  919. body: new URLSearchParams([
  920. ["client_id", clientID],
  921. ["client_secret", clientSecret],
  922. ["grant_type", "client_credentials"],
  923. ["scope", "public"],
  924. ]),
  925. });
  926. const responseData = await response.json();
  927. osuAccessToken = responseData.access_token;
  928. osuAccessTokenExpireTime = new Date().getTime() + responseData.expires_in * 1000;
  929. }
  930. lock = false;
  931. let resolve;
  932. while(resolve = queue.shift()) resolve();
  933. }
  934. else{
  935. const {promise, resolve, reject} = Promise.withResolvers();
  936. queue.push(resolve);
  937. await promise;
  938. }
  939. return osuAccessToken;
  940. }
  941. const CheckForUpdate = () => {
  942. const verReg = /<dd class="script-show-version"><span>([0-9\.]+)<\/span><\/dd>/;
  943. fetch("https://greatest.deepsurf.us/en/scripts/475417-osu-web-enhancement", {
  944. credentials: "omit"
  945. }).then(response => response.text()).then((html) => {
  946. const ver = verReg.exec(html);
  947. if(ver){
  948. const result = (() => {
  949. const verList = ver[1].split(".");
  950. const thisVer = GM_info.script.version;
  951. console.log(`latest version is: ${ver[1]}, current version is: ${thisVer}`);
  952. const thisVerList = thisVer.split(".");
  953. for(let i = 0; i < verList.length; i++){
  954. if(Number(verList[i]) > Number(thisVerList[i] ?? 0)) return true;
  955. else if(Number(verList[i]) < Number(thisVerList[i] ?? 0)) return false;
  956. }
  957. return false;
  958. })();
  959. if(result){
  960. const a = HTML("a", {href: "https://greatest.deepsurf.us/scripts/475417-osu-web-enhancement/code/osu!web%20enhancement.user.js", download: "", style: "display:none;"});
  961. a.click();
  962. }
  963. else{
  964. ShowPopup(i18n("The latest version is already installed!"))
  965. }
  966. }
  967. });
  968. };
  969. const AddMenu = () => {
  970. const menuId = "osu-web-enhancement-toolbar";
  971. if(!window.menuEventListener){
  972. window.addEventListener("click", (ev) => {
  973. const fid = ev.target?.dataset?.functionId;
  974. if(fid) switch(fid){
  975. case "import-osu-db-button": document.getElementById("osu-db-input")?.click(); break;
  976. case "check-for-update-button": CheckForUpdate(); break;
  977. case "pp-gini-index-calculator": PPGiniIndex(); break;
  978. }
  979. });
  980. window.menuEventListener = true;
  981. }
  982. if(document.getElementById(menuId)) return;
  983. const anc = document.querySelector("div.nav2__col.nav2__col--menu.js-react--quick-search-button");
  984. const i = HTML("input", {type: "file", id: "osu-db-input", accept: ".db", eventListener: [{
  985. type: "change",
  986. listener: SelectOsuDb,
  987. options: false,
  988. }]});
  989. const menuClass = "simple-menu simple-menu--nav2 simple-menu--nav2-left-aligned simple-menu--nav2-transparent js-menu";
  990. const menuItemClass = "simple-menu__item u-section-community--before-bg-normal";
  991. const menuTgtId = "osu-web-enhancement";
  992. anc.insertAdjacentElement("beforebegin",
  993. HTML("div", {class: "nav2__col nav2__col--menu", id: menuId},
  994. 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;"},
  995. HTML("span", {style: "flex-grow: 1;"}),
  996. HTML("span", {style: "font-size: 10px;"}, HTML("osu!web")),
  997. HTML("span", {style: "font-size: 10px;"}, HTML("enhancement")),
  998. HTML("span", {style: "flex-grow: 1;"}),
  999. ),
  1000. HTML("div", {class: "nav2__menu-popup"},
  1001. HTML("div", {class: `${menuClass}`, "data-menu-id": `nav2-menu-popup-${menuTgtId}`, "data-visibility": "hidden"},
  1002. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", "data-function-id": "import-osu-db-button", }, HTML(i18n("Import osu!.db"))),
  1003. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", "data-function-id": "check-for-update-button"}, HTML(i18n("Check for update"))),
  1004. HTML("div", {class: `${menuItemClass}`, style: "cursor: pointer;", "data-function-id": "pp-gini-index-calculator"}, HTML(i18n("Calculate pp Gini index"))),
  1005. HTML("a", {class: `${menuItemClass}`, style: "cursor: pointer;", href: "https://greatest.deepsurf.us/en/scripts/475417-osu-web-enhancement", target: "_blank"}, HTML(i18n("Go to GreasyFork page")))
  1006. ),
  1007. )
  1008. )
  1009. );
  1010. const mobMenuItmCls = "navbar-mobile-item__submenu-item js-click-menu--close";
  1011. const mob = document.querySelector(`div.mobile-menu__item.js-click-menu[data-click-menu-id="mobile-nav"]`);
  1012. mob.insertAdjacentElement("beforeend",
  1013. HTML("div", {class: "navbar-mobile-item"},
  1014. HTML("div", {class: "navbar-mobile-item__main js-click-menu", "data-click-menu-target": `nav-mobile-${menuTgtId}`, style: "cursor: pointer;"},
  1015. HTML("span", {class: "navbar-mobile-item__icon navbar-mobile-item__icon--closed"},
  1016. HTML("i", {class: "fas fa-chevron-right"})
  1017. ),
  1018. HTML("span", {class: "navbar-mobile-item__icon navbar-mobile-item__icon--opened"},
  1019. HTML("i", {class: "fas fa-chevron-down"})
  1020. ),
  1021. HTML("osu!web enhancement"),
  1022. ),
  1023. HTML("ul", {class: "navbar-mobile-item__submenu js-click-menu", "data-click-menu-id": `nav-mobile-${menuTgtId}`, "data-visibility": "hidden"},
  1024. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", "data-function-id": "import-osu-db-button",}, HTML(i18n("Import osu!.db")))),
  1025. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", "data-function-id": "check-for-update-button"}, HTML(i18n("Check for update")))),
  1026. HTML("li", {}, HTML("div", {class: mobMenuItmCls, style: "cursor: pointer;", "data-function-id": "pp-gini-index-calculator"}, HTML(i18n("Calculate pp Gini index")))),
  1027. HTML("a", {class: `${mobMenuItmCls}`, style: "cursor: pointer;", href: "https://greatest.deepsurf.us/en/scripts/475417-osu-web-enhancement", target: "_blank"}, HTML(i18n("Go to GreasyFork page")))
  1028. )
  1029. )
  1030. );
  1031. document.body.appendChild(i);
  1032. };
  1033. const FilterBeatmapSet = () => {
  1034. document.querySelectorAll(".beatmapset-panel").forEach((item) => {
  1035. const bmsID = Number(bmsReg.exec(item.innerHTML)?.[1]);
  1036. if(bmsID && beatmapsets.has(bmsID)){
  1037. item.setAttribute("owned-beatmapset", "");
  1038. }
  1039. });
  1040. document.querySelectorAll("div.bbcode a, a.osu-md__link").forEach(item => {
  1041. if(item.classList.contains("owned-beatmap-link") || item.classList.contains("beatmap-download-link")) return;
  1042. const e = bmsReg.exec(item.href);
  1043. if(e && beatmapsets.has(Number(e[1]))){
  1044. item.classList.add("owned-beatmap-link");
  1045. if(item.nextElementSibling?.classList?.contains("beatmap-download-link")) item.nextElementSibling.remove();
  1046. const box = item.getBoundingClientRect();
  1047. const size = Math.round(box.height / 16 * 14);
  1048. const vert = Math.round(size * 4 / 14) / 2;
  1049. item.after(HTML("img", {src: svg_green_tick, title: i18n("Owned"), alt: "owned beatmap", style: `margin: 0px 5px; width: ${size}px; height: ${size}px; vertical-align: -${vert}px;`}));
  1050. }else if(e && !item.nextElementSibling?.classList?.contains("beatmap-download-link")){
  1051. item.after(
  1052. HTML("a", {class: "beatmap-download-link", href: `https://osu.ppy.sh/beatmapsets/${e[1]}/download`, download: ""},
  1053. HTML("span", {class: "fas fa-file-download", title: i18n("Download")})
  1054. )
  1055. );
  1056. }
  1057. });
  1058. document.querySelectorAll("li.beatmap-pack-items__set").forEach(item => {
  1059. if(item.classList.contains("owned-beatmap-pack-item")) return;
  1060. const a = item.querySelector("a.beatmap-pack-items__link");
  1061. const e = bmsReg.exec(a.href);
  1062. if(e && beatmapsets.has(Number(e[1]))){
  1063. item.classList.add("owned-beatmap-pack-item");
  1064. const span = item.querySelector("span.fal");
  1065. span.setAttribute("title", i18n("Owned"));
  1066. span.dataset.origTitle = "owned";
  1067. span.setAttribute("class", "");
  1068. span.append(HTML("img", {src: svg_green_tick, alt: "owned beatmap", style: `width: 16px; height: 16px; vertical-align: -2px;`}));
  1069. const parent = item.querySelector(".beatmap-pack-item-download-link");
  1070. if(parent){
  1071. console.assert(parent.parentElement === item, "unexpected error occurred!");
  1072. item.insertBefore(span, parent);
  1073. parent.remove();
  1074. }
  1075. }else if(e){
  1076. const icon = item.querySelector(".beatmap-pack-items__icon");
  1077. icon.setAttribute("title", i18n("Download"));
  1078. icon.setAttribute("class", "fas fa-file-download beatmap-pack-items__icon");
  1079. if(icon.parentElement === item){
  1080. const dl = HTML("a", {class: "beatmap-pack-item-download-link", href: `https://osu.ppy.sh/beatmapsets/${e[1]}/download`, download: ""});
  1081. item.insertBefore(dl, icon);
  1082. dl.append(icon);
  1083. }
  1084. }
  1085. })
  1086. };
  1087. const AdjustStyle = (modestr, sectionName) => {
  1088. const styleSheetId = `userscript-generated-stylesheet-${sectionName}`;
  1089. let e = document.getElementById(styleSheetId);
  1090. if(!e){
  1091. e = document.createElement("style");
  1092. e.id = styleSheetId;
  1093. document.head.appendChild(e);
  1094. }
  1095. const s = e.sheet;
  1096. while(s.cssRules.length) s.deleteRule(0);
  1097. const sectionSelector = `div.js-sortable--page[data-page-id="${sectionName}"]`;
  1098. let ll = [];
  1099. switch(modestr){
  1100. case "mania": ll = [".mania-300", ".mania-200", ".mania-100", ".mania-50", ".mania-miss"]; break;
  1101. case "fruits": ll = [".fruits-300", ".fruits-100", ".fruits-50-miss", ".fruits-miss"]; break;
  1102. case "taiko": ll = [".taiko-300", ".taiko-150", ".taiko-miss"]; break;
  1103. case "osu": ll = [".osu-300", ".osu-100", ".osu-50", ".osu-miss"]; break;
  1104. }
  1105. class FasterCalc{
  1106. _map = new Map();
  1107. Calculate = (ele) => {
  1108. const t = ele.textContent;
  1109. let w = 0, changed = false;
  1110. for(const c of t){
  1111. let wc = this._map.get(c);
  1112. if(!wc){
  1113. if(!changed) changed = ele.cloneNode(true);
  1114. ele.textContent = c;
  1115. wc = ele.clientWidth;
  1116. this._map.set(c, wc);
  1117. }
  1118. w += wc;
  1119. }
  1120. if(changed){
  1121. ele.insertAdjacentElement("afterend", changed);
  1122. ele.remove();
  1123. }
  1124. return w;
  1125. };
  1126. };
  1127. let fc = new FasterCalc();
  1128. ll.forEach((str) =>
  1129. s.insertRule(
  1130. `${sectionSelector} ${str} + .score-detail-data-text {
  1131. width: ${[...document.querySelectorAll(`${sectionSelector} ${str} + .score-detail-data-text`)].reduce((max, ele) => { const w = fc.Calculate(ele); return w > max ? w : max }, 0) + 2}px;
  1132. }` ,0
  1133. )
  1134. );
  1135. fc = new FasterCalc();
  1136. [".play-detail__combo", ".play-detail__Accuracy", ".play-detail__Accuracy2"].forEach((str) =>
  1137. s.insertRule(
  1138. `${sectionSelector} ${str}{
  1139. min-width: ${Math.ceil([...document.querySelectorAll(`${sectionSelector} ${str}`)].reduce((max, ele) => {const w = fc.Calculate(ele); return w > max ? w : max;}, 0)) + 1}px;
  1140. }`
  1141. ,0
  1142. )
  1143. );
  1144. [".play-detail__pp"].forEach((str) =>
  1145. s.insertRule(
  1146. `${sectionSelector} ${str}{
  1147. min-width: ${Math.ceil([...document.querySelectorAll(`${sectionSelector} ${str}`)].reduce((max, ele) => {const w = ele.clientWidth; return w > max ? w : max;}, 0)) + 1}px;
  1148. }`
  1149. ,0
  1150. )
  1151. );
  1152. };
  1153. const PPGiniIndex = () => {
  1154. const vals = [...document.querySelectorAll(`div.js-sortable--page[data-page-id="top_ranks"] div.play-detail-list:nth-child(4) div.play-detail.play-detail--highlightable`)]
  1155. .map((ele) => {const ppele = ele.querySelector("div.play-detail__pp span"); return Number((ppele.title ? ppele.title : ppele.dataset.origTitle).replaceAll(",", ""))})
  1156. .sort((a, b) => b - a);
  1157. if(vals.length === 0) {
  1158. ShowPopup(i18n("Could not find best play data"), "danger");
  1159. return;
  1160. }
  1161. const min = vals[vals.length - 1];
  1162. let _ = 0; for(let i = vals.length - 1; i >= 0; i--) {
  1163. _ += vals[i] - min;
  1164. vals[i] = _;
  1165. }
  1166. const SB = vals.reduce((sum, val) => sum + val, -(vals[0] / 2));
  1167. const SAB = vals[0] / 2 * vals.length;
  1168. ShowPopup(i18n("Your pp Gini index of bp%{bp} is %{val}.", {bp: vals.length, val: (1 - SB/SAB).toPrecision(6)}));
  1169. }
  1170. const IsLazer = () => {
  1171. if(document.querySelector("button[data-url=\"https://osu.ppy.sh/home/account/options?user_profile_customization%5Blegacy_score_only%5D=1\"] span.fas")) return true;
  1172. else return false;
  1173. }
  1174. const TopRanksWorker = (userId, modestr, addedNodes = [document.body]) => {
  1175. const isLazer = IsLazer();
  1176. const subdomain = "osu"; // This line was const subdomain = isLazer ? "lazer": "osu"; now lazer.ppy.sh is no longer in use.
  1177. const sectionNames = new Set();
  1178. const GetSection = (ele) => {
  1179. let count = 0;
  1180. while(ele){
  1181. if(ele.tagName === "DIV" && ele.className === "js-sortable--page") return ele.dataset.pageId;
  1182. ele = ele.parentElement;
  1183. count++;
  1184. if(count > 50) console.log(ele);
  1185. }
  1186. };
  1187. addedNodes.forEach((eles) => {
  1188. if(eles instanceof Element) eles.querySelectorAll(":scope div.play-detail.play-detail--highlightable").forEach((ele) => {
  1189. if(ele.getAttribute("improved") !== null) return;
  1190. let a = ele.querySelector(":scope time.js-timeago");
  1191. // add support for osu!plus by @RealStr1ke
  1192. if(a === null) a = ele.querySelector(":scope time.timeago")
  1193. const t = a.getAttribute("datetime");
  1194. const data = messageCache.get(`${userId},${modestr},${subdomain},${t}`);
  1195. if(data){
  1196. sectionNames.add(GetSection(ele));
  1197. ListItemWorker(ele, data, isLazer);
  1198. }
  1199. });
  1200. });
  1201. sectionNames.forEach(sectionName => AdjustStyle(modestr, sectionName));
  1202. };
  1203. const DiffToColour = (diff, stops = [0, 0.1, 1.25, 2, 2.5, 3.3, 4.2, 4.9, 5.8, 6.7, 7.7, 9], vals = ['#AAAAAA', '#4290FB', '#4FC0FF', '#4FFFD5', '#7CFF4F', '#F6F05C', '#FF8068', '#FF4E6F', '#C645B8', '#6563DE', '#18158E', '#000000']) => {
  1204. const len = stops.length;
  1205. diff = Math.min(Math.max(diff, stops[0]), stops[len - 1]);
  1206. let r = stops.findIndex(stop => stop > diff);
  1207. if(r === -1) r = len - 1;
  1208. const d = stops[r] - stops[r - 1];
  1209. return `#${[[1, 3], [3, 5], [5, 7]]
  1210. .map(_ => [Number.parseInt(vals[r].slice(..._), 16), Number.parseInt(vals[r-1].slice(..._), 16)])
  1211. .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"))
  1212. .join("")
  1213. }`;
  1214. };
  1215. const CustomToPrecision = (number, precision) => {
  1216. return number >= 1 ? number.toPrecision(precision) : (number < (10 ** (-precision + 1) / 2) ? 0 : number.toFixed(precision - 1));
  1217. }
  1218. const ListItemWorker = (ele, data, isLazer) => {
  1219. console.log(isLazer);
  1220. if(ele.getAttribute("improved") !== null) return;
  1221. ele.setAttribute("improved", "");
  1222. ele.setAttribute("data-replay-id", data.id);
  1223. if(data.pp){
  1224. data.pp = Number(data.pp);
  1225. const pptext = ele.querySelector(".play-detail__pp > span").childNodes[0];
  1226. pptext.nodeValue = CustomToPrecision(data.pp, 5);
  1227. if(data.weight) pptext.title = i18n("%{pc} of total pp", {pc: CustomToPrecision(data.weight.pp, 5)});
  1228. }
  1229. const left = ele.querySelector("div.play-detail__group.play-detail__group--top");
  1230. 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);`});
  1231. left.insertAdjacentElement("beforebegin", leftc);
  1232. const detail = ele.querySelector("div.play-detail__score-detail-top-right");
  1233. const du = detail.children[0];
  1234. if(!detail.children[1]) detail.append(HTML("div", {classList: "play-detail__pp-weight"}));
  1235. const db = detail.children[1];
  1236. data.statistics.perfect ??= 0, data.statistics.great ??= 0, data.statistics.good ??= 0, data.statistics.ok ??= 0, data.statistics.meh ??= 0, data.statistics.miss ??= 0;
  1237. const bmName = ele.querySelector("span.play-detail__beatmap");
  1238. GetToken().then(async (token) => {
  1239. const body = {
  1240. mods: data.mods.map((modObj) => modObj.acronym),
  1241. ruleset_id: data.ruleset_id,
  1242. };
  1243. const response = await fetch(`https://osu.ppy.sh/api/v2/beatmaps/${data.beatmap.id}/attributes`,{
  1244. method: "POST",
  1245. body: JSON.stringify(body),
  1246. headers: {
  1247. "Authorization": `Bearer ${token}`,
  1248. "Accept": "application/json",
  1249. "Content-Type": "application/json",
  1250. }
  1251. });
  1252. const attributes = (await response.json()).attributes;
  1253. console.log(body, attributes);
  1254. const starRatingElement = HTML("div", {class: `difficulty-badge ${attributes.star_rating >= 6.5 ? "difficulty-badge--expert-plus" : ""}`, style: `--bg: ${DiffToColour(attributes.star_rating)}`},
  1255. HTML("span", {class: "difficulty-badge__icon"}, HTML("span", {class: "fas fa-star"})),
  1256. HTML("span", {class: "difficulty-badge__rating"}, HTML(`${attributes.star_rating.toFixed(2)}`))
  1257. );
  1258. bmName.parentElement.insertBefore(starRatingElement, bmName);
  1259. })
  1260. /*
  1261. const ic = ele;
  1262. ic.classList.add("audio-player", "js-audio--player");
  1263. ic.setAttribute("data-audio-url", `https://b.ppy.sh/preview/${data.beatmap.beatmapset_id}.mp3`)
  1264. ic.setAttribute("data-audio-state", "paused");
  1265. const gr = ele;
  1266. gr.classList.add("audio-player__button", "audio-player__button--play", "js-audio--play");
  1267. */
  1268. const bma = ele.querySelector("a.play-detail__title");
  1269. // const modeName = ["STD", "TAIKO", "CTB", "MANIA"];
  1270. bma.onclick = (e) => {e.stopPropagation();};
  1271. switch(data.ruleset_id){
  1272. case 0:{
  1273. du.replaceChildren(
  1274. HTML("span", {class: "play-detail__before"}),
  1275. HTML("span", {class: "play-detail__Accuracy", title: i18n(`${isLazer ? "Lazer Mode" : "Stable Mode"} Accuracy`)}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  1276. HTML("span", {class: "play-detail__combo", title: i18n(`Combo${isLazer ? "/Max Combo" : ""}`)},
  1277. 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}`)),
  1278. isLazer ? HTML("/") : null,
  1279. isLazer ? HTML("span", {class: "max-combo"}, HTML(`${(data.maximum_statistics.great ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0)}`)) : null,
  1280. HTML("x"),
  1281. ),
  1282. );
  1283. const m_300 = HTML("span", {class: "score-detail score-detail-osu-300"},
  1284. HTML("span", {class: "osu-300"},
  1285. HTML("300")
  1286. ),
  1287. HTML("span", {class: "score-detail-data-text"},
  1288. HTML(`${data.statistics.great + data.statistics.perfect}`)
  1289. )
  1290. );
  1291. const s100 = HTML("span", {class: "score-detail score-detail-osu-100"},
  1292. HTML("span", {class: "osu-100"},
  1293. HTML("100")
  1294. ),
  1295. HTML("span", {class: "score-detail-data-text"},
  1296. HTML(`${data.statistics.ok + data.statistics.good}`)
  1297. )
  1298. );
  1299. const s50 = HTML("span", {class: "score-detail score-detail-osu-50"},
  1300. HTML("span", {class: "osu-50"},
  1301. HTML("50")
  1302. ),
  1303. HTML("span", {class: "score-detail-data-text"},
  1304. HTML(`${data.statistics.meh}`)
  1305. )
  1306. );
  1307. const s0 = HTML("span", {class: "score-detail score-detail-osu-miss"},
  1308. HTML("span", {class: "osu-miss"},
  1309. HTML("img", {src: svg_osu_miss, alt: "miss"})
  1310. ),
  1311. HTML("span", {class: "score-detail-data-text"},
  1312. HTML(`${data.statistics.miss}`)
  1313. )
  1314. );
  1315. db.replaceChildren(m_300, s100, s50, s0);
  1316. break;
  1317. }
  1318. case 1:{
  1319. const cur = [data.statistics.great ?? 0, data.statistics.ok ?? 0, data.statistics.miss ?? 0];
  1320. const mx = cur[0] + cur[1] + cur[2];
  1321. du.replaceChildren(
  1322. HTML("span", {class: "play-detail__before"}),
  1323. HTML("span", {class: "play-detail__Accuracy", title: i18n(`${isLazer ? "Lazer Mode" : "Stable Mode"} Accuracy`)}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  1324. HTML("span", {class: "play-detail__combo", title: i18n(`Combo/Max Combo`)},
  1325. HTML("span", {class: `combo ${(data.max_combo === mx ? "legacy-perfect-combo" : "")}`}, HTML(`${data.max_combo}`)),
  1326. HTML("/"),
  1327. HTML("span", {class: "max-combo"}, HTML(`${mx}`)),
  1328. HTML("x"),
  1329. ),
  1330. );
  1331. db.replaceChildren(
  1332. HTML("span", {class: "score-detail score-detail-taiko-300"},
  1333. HTML("span", {class: "taiko-300"}, HTML("300")),
  1334. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.great ?? 0))
  1335. ),
  1336. HTML("span", {class: "score-detail score-detail-taiko-150"},
  1337. HTML("span", {class: "taiko-150"}, HTML("150")),
  1338. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.ok ?? 0))
  1339. ),
  1340. HTML("span", {class: "score-detail score-detail-fruits-combo"},
  1341. HTML("span", {class: "taiko-miss"}, HTML("miss")),
  1342. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss ?? 0))
  1343. ),
  1344. );
  1345. break;
  1346. }
  1347. case 2:{
  1348. if (isLazer) {
  1349. const cur = [data.statistics.great ?? 0, data.statistics.large_tick_hit ?? 0, data.statistics.small_tick_hit ?? 0];
  1350. const mx = [data.maximum_statistics.great ?? 0, data.maximum_statistics.large_tick_hit ?? 0, data.maximum_statistics.small_tick_hit ?? 0];
  1351. du.replaceChildren(
  1352. HTML("span", {class: "play-detail__before"}),
  1353. HTML("span", {class: "play-detail__Accuracy", title: i18n(`Lazer Mode Accuracy`)}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  1354. HTML("span", {class: "play-detail__combo", title: i18n(`Combo/Max Combo`)},
  1355. HTML("span", {class: `combo ${(data.max_combo === mx[0] + mx[1] ? "legacy-perfect-combo" : "")}`}, HTML(`${data.max_combo}`)),
  1356. HTML("/"),
  1357. HTML("span", {class: "max-combo"}, HTML(`${mx[0] + mx[1]}`)),
  1358. HTML("x"),
  1359. ),
  1360. );
  1361. db.replaceChildren(
  1362. HTML("span", {class: "score-detail score-detail-fruits-300"},
  1363. HTML("span", {class: "fruits-300"}, HTML("fruits")),
  1364. HTML("span", {class: "score-detail-data-text"}, HTML(cur[0] + "/" + mx[0]))
  1365. ),
  1366. HTML("span", {class: "score-detail score-detail-fruits-100"},
  1367. HTML("span", {class: "fruits-100"}, HTML("ticks")),
  1368. HTML("span", {class: "score-detail-data-text"}, HTML(cur[1] + "/" + mx[1]))
  1369. ),
  1370. HTML("span", {class: "score-detail score-detail-fruits-50-miss"},
  1371. HTML("span", {class: "fruits-50-miss"}, HTML("drops")),
  1372. HTML("span", {class: "score-detail-data-text"}, HTML(cur[2] + "/" + mx[2]))
  1373. )
  1374. );
  1375. } else {
  1376. du.replaceChildren(
  1377. HTML("span", {class: "play-detail__before"}),
  1378. HTML("span", {class: "play-detail__Accuracy", title: i18n(`Stable Mode Accuracy`)}, HTML(`${(data.accuracy * 100).toFixed(2)}%`)),
  1379. HTML("span", {class: "play-detail__combo", title: i18n(`Combo`)},
  1380. HTML("span", {class: ""}, HTML(`${data.max_combo}`)),
  1381. HTML("x")
  1382. ),
  1383. );
  1384. db.replaceChildren(
  1385. HTML("span", {class: "score-detail score-detail-fruits-300"},
  1386. HTML("span", {class: "fruits-300"}, HTML("FRUIT")),
  1387. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.great ?? 0))
  1388. ),
  1389. HTML("span", {class: "score-detail score-detail-fruits-100"},
  1390. HTML("span", {class: "fruits-100"}, HTML("tick")),
  1391. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.large_tick_hit ?? 0))
  1392. ),
  1393. HTML("span", {class: "score-detail score-detail-fruits-50-miss"},
  1394. HTML("span", {class: "fruits-50-miss"}, HTML("miss")),
  1395. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.small_tick_miss ?? 0))
  1396. ),
  1397. HTML("span", {class: "score-detail score-detail-fruits-miss"},
  1398. HTML("span", {class: "fruits-miss"}, HTML("MISS")),
  1399. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss ?? 0))
  1400. )
  1401. );
  1402. }
  1403. break;
  1404. }
  1405. case 3:{
  1406. const ppAcc = (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));
  1407. const v2Acc = (305*data.statistics.perfect+300*data.statistics.great+200*data.statistics.good+100*data.statistics.ok+50*data.statistics.meh)/(305*(data.statistics.perfect+data.statistics.great+data.statistics.good+data.statistics.ok+data.statistics.meh+data.statistics.miss));
  1408. const v1Acc = (300*data.statistics.perfect+300*data.statistics.great+200*data.statistics.good+100*data.statistics.ok+50*data.statistics.meh)/(300*(data.statistics.perfect+data.statistics.great+data.statistics.good+data.statistics.ok+data.statistics.meh+data.statistics.miss));
  1409. const MCombo = (data.maximum_statistics.perfect ?? 0) + (data.maximum_statistics.legacy_combo_increase ?? 0);
  1410. const isMCombo = isLazer ? data.max_combo >= MCombo : data.legacy_perfect;
  1411. du.replaceChildren(
  1412. HTML("span", {class: "play-detail__before"}),
  1413. HTML("span", {class: "play-detail__Accuracy2", title: i18n(`pp Accuracy`)}, HTML(`${(ppAcc * 100).toFixed(2)}%`)),
  1414. isLazer ?
  1415. HTML("span", {class: "play-detail__Accuracy", title: i18n(`Lazer Mode Accuracy`)}, HTML(`${(((data.rank === "D" && data.accuracy === 0) ? v2Acc : data.accuracy) * 100).toFixed(2)}%`)):
  1416. HTML("span", {class: "play-detail__Accuracy", title: i18n(`Stable Mode Accuracy`)}, HTML(`${(((data.rank === "D" && data.accuracy === 0) ? v1Acc : v1Acc) * 100).toFixed(2)}%`)),
  1417. HTML("span", {class: "play-detail__combo", title: i18n(`Combo${isLazer ? "/Max Combo" : ""}`)},
  1418. HTML("span", {class: `combo ${isMCombo ? "legacy-perfect-combo" : ""}`}, HTML(`${data.max_combo}`)),
  1419. isLazer ? HTML("/") : null,
  1420. isLazer ? HTML("span", {class: "max-combo"}, HTML(MCombo)) : null,
  1421. HTML("x"),
  1422. ),
  1423. );
  1424. if(data.pp){
  1425. const lostpp = CustomToPrecision(data.pp * (0.2 / (Math.min(Math.max(ppAcc, 0.8), 1) - 0.8) - 1), 4);
  1426. ele.querySelector(".play-detail__pp").appendChild(HTML("span", {class: "lost-pp"}, HTML(lostpp === 0 ? "MAX" : `-${lostpp}`)));
  1427. }
  1428. const M_300 = Number(data.statistics.perfect) / Math.max(Number(data.statistics.great), 1);
  1429. db.replaceChildren(
  1430. HTML("span", {class: "score-detail score-detail-mania-max-300", title: i18n("MAX: %{perfect}, 300: %{great}", data.statistics)},
  1431. HTML("span", {class: "mania-max"}, HTML("M")),
  1432. HTML("/"),
  1433. HTML("span", {class: "mania-300"}, HTML("300")),
  1434. 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))}`))
  1435. ),
  1436. HTML("span", {class: "score-detail score-detail-mania-max-200"},
  1437. HTML("span", {class: "mania-200"}, HTML("200")),
  1438. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.good))
  1439. ),
  1440. HTML("span", {class: "score-detail score-detail-mania-max-100"},
  1441. HTML("span", {class: "mania-100"}, HTML("100")),
  1442. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.ok))
  1443. ),
  1444. HTML("span", {class: "score-detail score-detail-mania-max-50"},
  1445. HTML("span", {class: "mania-50"}, HTML("50")),
  1446. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.meh))
  1447. ),
  1448. HTML("span", {class: "score-detail score-detail-mania-max-0"},
  1449. HTML("span", {class: "mania-miss"}, HTML("miss")),
  1450. HTML("span", {class: "score-detail-data-text"}, HTML(data.statistics.miss))
  1451. )
  1452. );
  1453. break;
  1454. }
  1455. default:;
  1456. }
  1457. }
  1458. let lastInitData = null;
  1459. const OsuLevelToExp = (n) => {
  1460. if(n <= 100) return 5000 / 3 * (4 * n ** 3 - 3 * n ** 2 - n) + 1.25 * 1.8 ** (n - 60);
  1461. else return 26_931_190_827 + 99_999_999_999 * (n - 100);
  1462. }
  1463. const OsuExpValToStr = (num) => {
  1464. const exp = Math.log10(num);
  1465. if(exp >= 12){
  1466. return `${(num / 10 ** 12).toPrecision(4)}T`;
  1467. }
  1468. else if(exp >= 9){
  1469. return `${(num / 10 ** 9).toPrecision(4)}B`;
  1470. }
  1471. else if(exp >= 6){
  1472. return `${(num / 10 ** 6).toPrecision(4)}M`;
  1473. }
  1474. else if(exp >= 4){
  1475. return `${(num / 10 ** 3).toPrecision(4)}K`;
  1476. }
  1477. else return `${num}`;
  1478. }
  1479. const messageCache = new Map();
  1480. window.messageCache = messageCache;
  1481. const profUrlReg = /https:\/\/(?:osu|lazer)\.ppy\.sh\/users\/[0-9]+(?:|\/osu|\/taiko|\/fruits|\/mania)/;
  1482. class SortGroup {
  1483. rules = [];
  1484. constructor(){
  1485.  
  1486. }
  1487. Show = () => {
  1488. if(document.querySelector(".sort-detail__items")) return;
  1489. const h3 = document.querySelector('div.js-sortable--page[data-page-id="top_ranks"] h3.title.title--page-extra-small:nth-child(3)');
  1490. h3.insertAdjacentElement("afterend",
  1491. HTML("div", {class: "sort-detail__items"},
  1492. HTML("div", {class: "sort-detail__item sort-detail__item--title"}, i18n("Sort by")),
  1493. )
  1494. );
  1495. };
  1496. AddRule = (name) => {
  1497. this.rules.push(name);
  1498. };
  1499. };
  1500. const ImproveProfile = (mulist) => {
  1501. const wloc = window.location.toString();
  1502. if(!profUrlReg.test(wloc)) return;
  1503. //SortGroup.Show();
  1504. const initDataEle = document.querySelector(".js-react--profile-page.osu-layout.osu-layout--full");
  1505. if(!initDataEle) return;
  1506. const initData = JSON.parse(initDataEle.dataset.initialData);
  1507. const userId = initData.user.id, modestr = initData.current_mode;
  1508. if(initData !== lastInitData){
  1509. let ppDiv = null;
  1510. document.querySelectorAll("div.value-display.value-display--plain").forEach((ele) => {
  1511. if(ele.querySelector("div.value-display__label").textContent === "pp") ppDiv = ele;
  1512. });
  1513. if(ppDiv){
  1514. const ttscore = initData.user.statistics.total_score;
  1515. const lvl = initData.user.statistics.level.current;
  1516. const upgradescore = Math.round(OsuLevelToExp(lvl + 1) - OsuLevelToExp(lvl));
  1517. const lvlscore = ttscore - Math.round(OsuLevelToExp(lvl));
  1518. lastInitData = initData;
  1519. document.querySelector("div.bar__text").textContent = `${OsuExpValToStr(lvlscore)}/${OsuExpValToStr(upgradescore)} (${(lvlscore/upgradescore * 100).toPrecision(3)}%)`;
  1520. const _pp = initData.user.statistics.pp;
  1521. ppDiv.querySelector(".value-display__value > div").textContent = _pp >= 1 ? _pp.toPrecision(6) : (_pp < 0.000005 ? 0 : _pp.toFixed(5));
  1522. }
  1523. }
  1524. if(mulist !== undefined) mulist.forEach((record) => {
  1525. if(record.type === "childList" && record.addedNodes) TopRanksWorker(userId, modestr, record.addedNodes);
  1526. });
  1527. }
  1528. const InsertStyleSheet = () => {
  1529. //const sheetId = "osu-web-enhancement-general-stylesheet";
  1530. const s = new CSSStyleSheet();
  1531. s.replaceSync(inj_style);
  1532. document.adoptedStyleSheets = [...document.adoptedStyleSheets, s];
  1533. }
  1534. const OnBeatmapsetDownload = (message) => {
  1535. beatmapsets.add(message.beatmapsetId);
  1536. }
  1537. const ImproveBeatmapPlaycountItems = () => {
  1538. for(const item of [...document.querySelectorAll("div.beatmap-playcount")]){
  1539. if(item.getAttribute("improved") !== null) continue;
  1540. item.setAttribute("improved", "");
  1541. const a = item.querySelector("a");
  1542. const bms = bmsReg.exec(a.href);
  1543. if(!bms?.[1]) continue;
  1544. const d = item.querySelector("div.beatmap-playcount__detail");
  1545. const b = HTML("div", {class: "beatmap-playcount__background", style: `background-image: url(https://assets.ppy.sh/beatmaps/${bms[1]}/covers/card@2x.jpg)`});
  1546. if(d.childElementCount > 0) d.insertBefore(b, d.children[0]);
  1547. else d.append(b);
  1548. }
  1549. }
  1550. const CopyToClipboard = (txt) => {
  1551. navigator.clipboard.writeText(txt).then(() => {
  1552. console.log(txt);
  1553. ShowPopup(i18n("Score details copied to clipboard!"))
  1554. }, (err) => {
  1555. console.log(err);
  1556. ShowPopup(i18n("Unable to copy score detail to clipboard, check console for more info."), "danger")
  1557. });
  1558. }
  1559. const MakeTextDetail = (data) => {
  1560. let detail = "";
  1561. const s = data.statistics; const m = data.maximum_statistics; const b = data.beatmap;
  1562. const secToMin = (t) => `${Math.floor(t/60)}:${String(t%60).padStart(2, '0')}`;
  1563. const isLazer = IsLazer();
  1564. switch(data.ruleset_id){
  1565. case 0: detail =
  1566. `${(s.great ?? 0) + (s.perfect ?? 0)}-${(s.ok ?? 0) + (s.good ?? 0)}-${s.meh ?? 0}-${s.miss ?? 0} ${data.max_combo ?? 0}${isLazer ? `/${(m.great ?? 0) + (m.legacy_combo_increase ?? 0)}` : ""}x
  1567. ${b.count_circles ?? 0} 🌡️ ${b.count_sliders ?? 0} 🔄 ${b.count_spinners ?? 0}
  1568. `; break;
  1569. case 1: detail =
  1570. `${s.great ?? 0}-${s.ok ?? 0}-${s.miss ?? 0} ${data.max_combo ?? 0}/${(s.great ?? 0) + (s.ok ?? 0) + (s.miss ?? 0)}x
  1571. 🥁 ${b.count_circles ?? 0} 🌡️ ${b.count_sliders ?? 0} 🍥 ${b.count_spinners ?? 0}
  1572. `; break;
  1573. case 2: detail = isLazer ?
  1574. `${s.great ?? 0}/${m.great ?? 0}-${s.large_tick_hit ?? 0}/${m.large_tick_hit ?? 0}-${s.small_tick_hit ?? 0}/${m.small_tick_hit ?? 0} ${data.max_combo ?? 0}/${(m.large_tick_hit ?? 0)+(m.great ?? 0)}}x
  1575. 🍎 ${b.count_circles ?? 0} 💧 ${b.count_sliders ?? 0} 🍌 ${b.count_spinners ?? 0}
  1576. ` :
  1577. `${s.great ?? 0}-${s.large_tick_hit ?? 0}-${s.small_tick_miss ?? 0}-${s.miss ?? 0} ${data.max_combo ?? 0}x
  1578. 🍎 ${b.count_circles ?? 0} 💧 ${b.count_sliders ?? 0} 🍌 ${b.count_spinners ?? 0}
  1579. `; break;
  1580. case 3: detail =
  1581. `${s.perfect ?? 0}-${s.great ?? 0}-${s.good ?? 0}-${s.ok ?? 0}-${s.meh ?? 0}-${s.miss ?? 0} ${data.max_combo}${isLazer ? `/${(m.perfect ?? 0) + (m.legacy_combo_increase ?? 0)}` : ""}x
  1582. 🍚 ${b.count_circles ?? 0} 🍜 ${b.count_sliders ?? 0}
  1583. `; break;
  1584. default:;
  1585. }
  1586. const scrMsg =
  1587. `${data.beatmapset.title}
  1588. [${data.beatmap.version}] ${secToMin(data.beatmap.total_length)}
  1589. ${data.total_score} ${data.rank} ${data.pp ? (data.pp >= 1 ? data.pp.toPrecision(5) : (data.pp < 0.00005 ? 0 : data.pp.toFixed(4))) : "-"}pp
  1590. ${detail}
  1591. `;
  1592. return scrMsg;
  1593. }
  1594. const CopyDetailsPopup = () => {
  1595. const ele = document.querySelector("div.play-detail.play-detail--active"); if(!ele) return;
  1596. const id = ele.dataset.replayId;
  1597. const data = messageCache.get(Number(id)); if(!data) return;
  1598. const msg = MakeTextDetail(data);
  1599. CopyToClipboard(msg);
  1600. };
  1601. const AddPopupButton = () => {
  1602. const p = document.querySelector("div.js-portal")?.querySelector("div.simple-menu");
  1603. if(!p || p.querySelector("button.score-card-popup-button")) return;
  1604. // p.append(HTML("button", {class: "score-card-popup-button simple-menu__item", type: "button", eventListener: [{type: "click", listener: ShowScoreCardPopup}]}, HTML("Popup")));
  1605. p.append(HTML("button", {class: "score-card-popup-button simple-menu__item", type: "button", eventListener: [{type: "click", listener: CopyDetailsPopup}]}, HTML(i18n("Copy Text Details"))));
  1606. };
  1607. const OnMutation = (mulist) => {
  1608. mut.disconnect();
  1609. AddMenu();
  1610. FilterBeatmapSet();
  1611. ImproveBeatmapPlaycountItems();
  1612. ImproveProfile(mulist);
  1613. AddPopupButton();
  1614. mut.observe(document, {childList: true, subtree: true});
  1615. };
  1616. const MessageFilter = (message) => {
  1617. const info = `${message.userId},${message.mode},${message.subdomain}`;
  1618. switch(message.type){
  1619. case "beatmapset_download_complete": OnBeatmapsetDownload(message); break;
  1620. case "top_ranks":
  1621. [message.data.pinned.items, message.data.best.items, message.data.firsts.items].forEach(items => items.forEach(item => {
  1622. messageCache.set(`${info},${item.ended_at}`, item);
  1623. messageCache.set(item.id, item);
  1624. }));
  1625. TopRanksWorker(message.userId, message.mode);
  1626. break;
  1627. case "firsts": case "pinned": case "best": case "recent":
  1628. message.data.forEach(item => { messageCache.set(`${info},${item.ended_at}`, item); messageCache.set(item.id, item); });
  1629. TopRanksWorker(message.userId, message.mode);
  1630. break;
  1631. case "historical":
  1632. message.data.recent.items.forEach(item => { messageCache.set(`${info},${item.ended_at}`, item); messageCache.set(item.id, item); });
  1633. TopRanksWorker(message.userId, message.mode);
  1634. break;
  1635. default:;
  1636. }
  1637. }
  1638. const WindowMessageFilter = (event) => {
  1639. if(event.source === window && event?.data?.id === "osu!web enhancement"){
  1640. MessageFilter(event.data);
  1641. }
  1642. }
  1643. const OnClick = (event) => {
  1644. let t = event.target;
  1645. while(t){
  1646. if(t.tagName === "A"){
  1647. const e = bmsdlReg.exec(t.href);
  1648. if(!e) continue;
  1649. beatmapsets.add(Number(e[1]));
  1650. FilterBeatmapSet();
  1651. break;
  1652. }
  1653. t = t.parentElement;
  1654. }
  1655. }
  1656. //document.addEventListener("click", OnClick);
  1657. const curLocale = window.currentLocale;
  1658. if (curLocale && locales[curLocale]) {
  1659. console.log("localization available");
  1660. i18n.translator.add(locales[curLocale]);
  1661. }
  1662. window.addEventListener("message", WindowMessageFilter);
  1663. const mut = new MutationObserver(OnMutation);
  1664. mut.observe(document, {childList: true, subtree: true});
  1665. InsertStyleSheet();
  1666. console.log("osu!web enhancement loaded");
  1667. // below are test code
  1668. /*
  1669. const osusrc = "https://i.ppy.sh/bde5906f8f985126f4ea624d3eb14c8702883aa2/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d6f73752e706e67";
  1670. const taikosrc = "https://i.ppy.sh/c1a9502ea05c9fcde03a375ebf528a12ff30cae7/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d7461696b6f2e706e67";
  1671. const fruitsrc = "https://i.ppy.sh/e7cad0470810a868df06d597e3441812659c0bfa/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d6672756974732e706e67";
  1672. const maniasrc = "https://i.ppy.sh/55d9494fcf7c3ef2d614695a9a951977a21f23f6/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f536b696e6e696e672f496e746572666163652f696d672f6d6f64652d6d616e69612e706e67";
  1673. const pngsrc = [osusrc, taikosrc, fruitsrc, maniasrc];
  1674. const png = [null, null, null, null];
  1675. let canvas, ctx, cw, ch;
  1676. const ToggleSnow = async (modeid) => {
  1677. if(canvas) {canvas.remove(); return;}
  1678. canvas = HTML("canvas", {style: `position: fixed; bottom: 0px; left: 0px;`, width: window.innerWidth, height: window.innerHeight});
  1679. document.body.append(canvas);
  1680. ctx = canvas.getContext("webgl2");
  1681. if(!png[modeid]){
  1682. const response = await fetch(pngsrc[modeid]);
  1683. png[modeid] = await response.blob();
  1684. }
  1685. }
  1686. */