· 6 years ago · Mar 08, 2020, 08:18 PM
1// ==UserScript==
2// @name osuplus
3// @namespace https://osu.ppy.sh/u/1843447
4// @version 2.2.0
5// @description show pp, selected mods ranking, friends ranking and other stuff
6// @author oneplusone
7// @include http://osu.ppy.sh*
8// @include https://osu.ppy.sh*
9// @include http://old.ppy.sh*
10// @include https://old.ppy.sh*
11
12// @noframes
13// @grant GM.xmlHttpRequest
14// @grant GM.setValue
15// @grant GM.getValue
16// @grant GM_xmlhttpRequest
17// @grant GM_setValue
18// @grant GM_getValue
19// @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js
20// @require http://timeago.yarp.com/jquery.timeago.js
21// ==/UserScript==
22/* jshint esversion: 6 */
23(() => {
24 "use strict";
25
26 var debug = false;
27
28 // backwards compatible
29 var GMX;
30 if(typeof GM == 'undefined'){
31 GMX = {
32 xmlHttpRequest: GM_xmlhttpRequest,
33 setValue: function(name, value){
34 return Promise.resolve(GM_setValue(name, value));
35 },
36 getValue: function(name, def){
37 return Promise.resolve(GM_getValue(name, def));
38 }
39 };
40 }else{
41 GMX = GM;
42 }
43
44 //https://i.imgur.com/87WeqCL.png
45 var bloodcatBtnImg = "data:image/png;base64,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",
46
47 FImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQCAYAAAAiYZ4HAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjYwNDg3Q0VFMDdDNTExRTZCRUNBQUJFQjU3NDhGRjk4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjYwNDg3Q0VGMDdDNTExRTZCRUNBQUJFQjU3NDhGRjk4Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NjA0ODdDRUMwN0M1MTFFNkJFQ0FBQkVCNTc0OEZGOTgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NjA0ODdDRUQwN0M1MTFFNkJFQ0FBQkVCNTc0OEZGOTgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6y4WsfAAABoklEQVR42pSSyy8DURTGvzvTh3am1fFoqwjVVERYeHRpayMRsZfYSqwk/AcSOxsbiQUrsbDrgpVIEIKgIiH1aAijQyjVdtoxHXcmJSyYuMmX3HPP+eWeL/mIpmn4zyE6QAiBu1yItLeGe2U5l6rzCb46r6vJJzj9VQLnV54vC7NLG8snojppKYHlU2ODOyODHeRwZx1pWUNefkMu+4C8cot3RkQlp3XQuS8gp0hHmbXFKD8+l8SZhOMMcEeXFWmPXuGmmtcHP4GCpirZk+ssvy9hgdbDv3lgSlYaGgKCN/Go6sXKX6YNoMzhDFZwBKnXrF5ul3osiKPZyXu6vwPGStWCM1hmZdFZT4oY6JoJhVss9VX2tkafqzZzu4eJmY29mIjIF+Cv5EM2VgVnI0x/WO2DIwnx5h7RLQmnV/SexvmPH5oC7hBR0pheLWpH0sEufbqgSlDdUMWoNn8AjdW24FPqBceS0egxNe3hrN68LMNlgWQWDQPwu4oBKymAZWE3A4yVEvGYPcnIyGuoME2fHj7eitEaN+IWBkNms+S/8f4QYACrg5Lyg2EOtgAAAABJRU5ErkJggg==",
48 //UImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAA1VBMVEUAAACioqKbm5vy8vLX19fv7+/Ozs6ioqK0tLTR0dHJycmqqqqzs7Ojo6Oamprb29vT09Ourq6mpqb///8mJiYzMzMxMTEsLCwpKSkvLy81NTUjIyMgICAeHh5JSUlISEhRUVFFRUUZGRkcHBympqZaWlqrq6s6Ojo+Pj5WVlbExMSjo6NCQkI3NzfY2Ni4uLivr695eXlycnJMTEze3t66urqoqKj8/Pz4+Pjg4ODa2tq+vr60tLSbm5t+fn5ra2vk5OTj4+OTk5OMjIyFhYVpaWlmZmaYnrJ3AAAAE3RSTlMAUlL+8f7nQ6np562iS0jy8aRXW3S7rwAAAbZJREFUSMft1dlymzAYhuGmxnaapOtnLLGvNjgBL+DsW/f7v6T+mrrIbQXKccJzAqPROyAxGl71er2X4PhoiH8N3hw+JX33AWrGWNu+vsLykeV+npDc9/0gEhbfvmA41rXA3E0YRSxJLJ9QSQI/PIWhbc88z+OMMb67TISsYMzc4qO2/VssOEFgekmFI01blpYrsdoScR4EgRNbGHa3bv25cqUFampZQKZxDHS2jnOPrSMNcZYkXkT7VmQOx6CjTWn6HJCtC6zixPZJlpk/MOpoJ4TiScOhOOEhifK8WOK4qyVriiWKvUzEtOgVjI5WGZ/zvA6FFS7ftrb8N4q5RLFZZgG5Az4p2wPZ8jlgNa0F3NEHm3CeAgfqc3SFC/bHV2DDdngF3DuEX6hbYmDNGj9Btc0EuxL3Ik5b20PcTK1GucW+JaNta2vJCBtLqh9v9tuIhlrfmQyQyzabzWbf0/NT4TatTBqat7YEmEohxdF0n2zVTy6amcVMKPWtXPODuWOHC8E3G7JVO8F1ZqroW2LgNrNVyrWmJeMhrh+i/9Jis8QltbragNp7Okd6J6OB8g/T6/WevV831F5ngplDwwAAAABJRU5ErkJggg==",
49 FImgNew = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzMiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzIgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojQjlCOUI5O30KCS5zdDF7ZmlsbDojRkY1QTVBO2ZpbHRlcjp1cmwoI0Fkb2JlX09wYWNpdHlNYXNrRmlsdGVyKTt9Cgkuc3Qye21hc2s6dXJsKCNtYXNrMF8xXyk7fQoJLnN0M3tmaWxsOiNDNEM0QzQ7fQoJLnN0NHtmaWxsOiNBRkFGQUY7fQoJLnN0NXtmaWxsOiNBNEE0QTQ7fQoJLnN0NntmaWxsOiMzMzMzMzM7fQo8L3N0eWxlPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNOCwwaDE2YzQuNCwwLDgsMy42LDgsOGwwLDBjMCw0LjQtMy42LDgtOCw4SDhjLTQuNCwwLTgtMy42LTgtOGwwLDBDMCwzLjYsMy42LDAsOCwweiIvPgo8ZGVmcz4KCTxmaWx0ZXIgaWQ9IkFkb2JlX09wYWNpdHlNYXNrRmlsdGVyIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9Ii0xLjMiIHk9Ii05IiB3aWR0aD0iMzUuMyIgaGVpZ2h0PSIzMCI+CgkJPGZlQ29sb3JNYXRyaXggIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIxIDAgMCAwIDAgIDAgMSAwIDAgMCAgMCAwIDEgMCAwICAwIDAgMCAxIDAiLz4KCTwvZmlsdGVyPgo8L2RlZnM+CjxtYXNrIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9Ii0xLjMiIHk9Ii05IiB3aWR0aD0iMzUuMyIgaGVpZ2h0PSIzMCIgaWQ9Im1hc2swXzFfIj4KCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik04LDBoMTZjNC40LDAsOCwzLjYsOCw4bDAsMGMwLDQuNC0zLjYsOC04LDhIOGMtNC40LDAtOC0zLjYtOC04bDAsMEMwLDMuNiwzLjYsMCw4LDB6Ii8+CjwvbWFzaz4KPGcgY2xhc3M9InN0MiI+Cgk8cGF0aCBjbGFzcz0ic3QzIiBkPSJNMTYtOWwxNy4zLDMwSC0xLjNMMTYtOXoiLz4KCTxwYXRoIGNsYXNzPSJzdDQiIGQ9Ik0yNy41LDNMMzQsMTQuMkgyMUwyNy41LDN6Ii8+Cgk8cGF0aCBjbGFzcz0ic3Q1IiBkPSJNNy41LTJsMy45LDYuOEgzLjZMNy41LTJ6Ii8+Cgk8cGF0aCBjbGFzcz0ic3Q1IiBkPSJNOS41LDEzbDMuOSw2LjhINS42TDkuNSwxM3oiLz4KPC9nPgo8Zz4KCTxwb2x5Z29uIGNsYXNzPSJzdDYiIHBvaW50cz0iMTEsMy45IDIxLjMsMy45IDIxLjMsNS4yIDEyLjUsNS4yIDEyLjUsNy41IDIwLjgsNy41IDIwLjgsOC44IDEyLjUsOC44IDEyLjUsMTIuMyAxMSwxMi4zIAkiLz4KPC9nPgo8L3N2Zz4K",
50
51 loaderImg = "data:image/gif;base64,R0lGODlhEAAQAPIAAKmp/wAAAIGBwisrQgAAAEFBYlZWgmFhkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==",
52
53 subImg = "data:image/png;base64,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",
54 subbedImg = "data:image/png;base64,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",
55
56 settingsImg = "data:image/png;base64,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",
57
58 modIconImgs = {
59 //http://puu.sh/n41q9/dd04d2b8b6.png
60 "NM": "data:image/png;base64,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",
61
62 //http://w.ppy.sh/2/22/No_Fail.png
63 "NF": "data:image/png;base64,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",
64
65 //http://w.ppy.sh/7/7a/Easy.png
66 "EZ": "data:image/png;base64,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",
67
68 //http://w.ppy.sh/b/b9/Hidden.png
69 "HD": "data:image/png;base64,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",
70
71 //http://w.ppy.sh/2/28/Hard_Rock.png
72 "HR": "data:image/png;base64,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",
73
74 //http://w.ppy.sh/c/c6/Sudden_Death.png
75 "SD": "data:image/png;base64,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",
76
77 //http://w.ppy.sh/1/16/Double_time.png
78 "DT": "data:image/png;base64,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",
79
80 //http://w.ppy.sh/b/bc/Half-time.png
81 "HT": "data:image/png;base64,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",
82
83 //http://w.ppy.sh/f/f9/Nightcore.png
84 "NC": "data:image/png;base64,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",
85
86 //http://w.ppy.sh/4/4d/Flashlight.png
87 "FL": "data:image/png;base64,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",
88
89 //http://w.ppy.sh/2/22/Spun_Out.png
90 "SO": "data:image/png;base64,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",
91
92 //http://w.ppy.sh/4/46/Perfect.png
93 "PF": "data:image/png;base64,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",
94
95 //http://w.ppy.sh/8/87/FadeIn.png
96 "FI": "data:image/png;base64,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",
97
98 //http://w.ppy.sh/1/12/4K.png
99 "4K": "data:image/png;base64,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",
100
101 //http://w.ppy.sh/0/0f/5k.png
102 "5K": "data:image/png;base64,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",
103
104 //http://w.ppy.sh/8/80/6k.png
105 "6K": "data:image/png;base64,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",
106
107 //http://w.ppy.sh/e/ea/7k.png
108 "7K": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7CAAAOwgEVKEqAAAAAGnRFWHRTb2Z0d2FyZQBQYWludC5ORVQgdjMuNS4xMDD0cqEAAAkHSURBVFhHzZkJVFZ1GsYpM3FDdhTEFZAdERAQ0EQ0nBo9Ne4nZzgWpU1q2li54KhNegxRzAnnWOMuwiBKIi64MaaV5TIu+dlJM5dEc6mxZZrOzDzzPH+/7wb46WB2zsA5z/nue+//vv/ffbd7PbrY/xpRrlTzBiKxiMn8NfLz8Iht7+e3qYOf3+GGILG0JpPYBOjKE5UdWrdGQxKZtolNgC1o2Jwt+n9KTGK7Cejra2No0ZAkpp8FsCOf9hY5WVPTro/uGVCbBvn7o0vbtghr184oNDDQ2CEBAejcpg062dcE0+5E25mf26kWYDsazLnyXm915IZ909Lwx/nzUZCfj8ULF+KtggJkpqYib84cRHbqhC4ETk9KQu7s2QgmuDM/t5OY7glQhRzZuTN6EyCToEMzM1FVWYkBKSnYs3MnekRGIi02FtsqKtC/Vy90ZiSd+bmd7hlQUpq1cVj79sgaPBiLcnORTqi9u3bhVw89hNLCQowaNsxE0t6Z9datgL6+yvtdS5DhBFy9dCn6JScjKTQU+6uqsGXtWszNyUFUx46mFp3deyfVBvTx+cmAagaleMWbbyKSoN2CgnDiyBFUlZdj7fLliCagHsLZvXeSmCzAQBokFvVdSalQ+tQgj/XrZ7o3ms3xDlP8WM+eKFy2DE+PHGm62J62ektM9wyo0dEjJgbri4tNmmUrpbvZLGqSnqzHPYTVGl1z5uN2+lkANeP+wDrLZpR0rEYQ6M5t2xAbHGzmYtbQoShatcoc300UbwEM1Ml6yOFAmwnqmawsRNjrTANW6fztk08ilINbUVMJjM3ORlx4uHkAC+B/qSZgWwH6+OjkHeUAE4w2F6DqLsj+ppA0dgSpX7OGxyEc0tbbRA2jSDrxX1NiuitAwSlCglK6lMoIqUMH8xtuf93VlVmnNZRswQrU3qlO95LuClCODByjEMl0JnLWqQliw8KR3rUrkiIikaymiI5GWlQUEsLC0JO/WpMSGYVebJJYneP1ONamQB2z0dl+Ui3AABo8oZO3SIvlSPMuklEQwOghQ5DcNRZp8QmYN2Mm4giRPXwE5kyZhlmTXkI87dwZszDm179BevfuKFiwEF3DIzA2axSeePSXSOjSxXxUKN2qN2f7iql+gHRg3hZ86pSICIweNBhDf/EIokLDMPXFyZjw3HhE83jZ8lVYX7YJK1YWGlvH+fMXIpZg5RXb0CsxCT1iu6EgNw/9CR3DealyacfMONu3NqC3t62ttzfqStHTU6pu9NRD0/tg0uhnEcPUxkdFY8v23UhkJGOZ4g+PnUTxujLkv/6GgV+5pghritaZ47x5CzDu2efM8UT+Th49Bql8WKXaRFFQdfYW0x0BAymlVk8Zw6+W/gkJmD15KpIZBW00+aUpmPtanjnO6NUb1Te+R3Hp25g561VzbvGSpdi1bz+iuoRiyOODsI4RVWQTY7qicMVqDOB7O5avRdW19qm7/y2AlE4aaYEaQ4Ws7uvF4p4+bjyGDBhoNk8k5IHjJ5HKtMl+kam2na3G1t3v4IWJk8y5+fmLcOzUWXRjPcYT6sDxj5GakGiuTRw7HnOn5hi/6nKVkKJYk4H6EdC/LiAXa1ZpnCSx+0YNHIh8pkkR0AbTp89EYckGRDI6UsnbFfjr+wdxyHYK2U89bda8Ouc1XPjyW6R0TzRriks34ncv3ISPY0ns2LUHw/pkIC4kxMxIjTF75IzE5BRQcFqs0OvLZCA/QKuq9uIvTN8jfR9GemoaTpz5HLl5+QgPDjH1eOriFWyoqMSZy19i+LARBmhazgxc+ce/kZbcA+FBwVhUsARbdu4xTZPZOx0fnT6PJQsXmTGlOVo3irUBvbwMoJ5AXatBqpd+Bl/2cqKNrn7/HxSt24BPL103xx+fv4Q8prHqvQPm+uriUnzx3b9QsaMKueze0vItuMrz+w4dxbwFr+PMF1/hCq8XsZFOXbxqfJypvoYsjp3ubEA1orLmiKKYagN6eZmLGsjqrhQO2UlPZePiV98ZZ59evo4N2pTHUhkjdqr65kbSyrUl1vHeg0fwIWvO2bUTZy6gsmqfZZcWlaBfXJz1YWtGDFlqAbaxR9Ckll0bz7oYntEXx1hTDkfqwnNXb1j2co4Rx/Glb34wNemw9x08ivf/9pFlr7FH17I5fhzHCsC4J0aaWnfUoljE9COgp6cBVPTUVX2Y2o2lZZaTc9e+RikBHfZppqZsc6Vln736d2zYtNWy3zt8HPuPnLDsrWyIk+eqLXv9xs24cP0by97Fsng4Pt7UohgMIJksQD93d5u/p6eZR1EcK9PGTzD14nBQwrqp/vqfll2+dSc+YWM47A+O2vDuoWOWrejVBDz6yWfY/e4Hln3u2g1s5NvFYSu6k54ZY/55IAaxiMkC9HFzs7V2dzfDOYL19/K4562bpfXrN9aya6ZIKt+6w3SwwxacoB32eUarrEaEpdI6Pl95eYoJTjsyiEVMFqBXixY235Yt4c8LQezgDH4E7OBr7OTpc6ZGNpVvruWsmPVWs6bKt2zHeZaBwz7IBtnPNDvsz+mjZsSkcjbc5W9/wGfs6CN8TQ7L7I8Q1n+Ahwd83dzgTSYL0LNZM5tX8+bwIWQAwxtCyHi+3vrwM+nRxEQM4j+8x/HLJOf5icid+Qp/J+CtN/6EwpVrsI7RXJQ7HyXs1E2sU2nVn5djCd/JxWwkHS/mmJnGrMzN+T1y+DuevkZkZODx1FT0Z+2lsEHCOWYC2b0KlFjEZAG2cnW1ubu6wrNpU5GjTatWZnEn1kMwWz+UTyYHkXyzxLCQY1krcfwa0UN05zBP4jdeMjs/mfPMEm2d1/UErtN63af75Uf+5DeYTdGBafVn5BQggkEsYrIAWzZubHN78EFIrZo0gQcXeHGht6JKYD2VH9WaoRe8pHIIoNrScaBDjL4l+zld1zqtd9wrP/Inv/KvvTwYHHfu3crOISYLsHmjRjYKUosHHjBqKTVubOQmCb6G5KymPJyo7pqa95uA2P1Ljn0dHGJyALo2ve++7RTqo2bS/ff/dNXxdwdVik2AjRq7uHRr4uJSQR1uIKoQk9gEqL8G+t8QLi7/Bfkeyys8HLztAAAAAElFTkSuQmCC",
109
110 //http://w.ppy.sh/e/e9/8k.png
111 "8K": "data:image/png;base64,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",
112
113 //http://w.ppy.sh/6/67/9k.png
114 "9K": "data:image/png;base64,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",
115
116 "TD": "data:image/png;base64,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",
117 };
118
119 var BEATMAP = 0,
120 USERPAGE = 1,
121 PPRANKING = 2,
122 BEATMAPLISTING = 3;
123
124 var OLDSITE = 0,
125 NEWSITE = 1;
126
127 var apikey = null,
128 hasKey = false,
129 pageType = BEATMAP,
130 siteType = OLDSITE;
131
132 var modnames = [
133 {val: 1, name: "NoFail", short: "NF"},
134 {val: 2, name: "Easy", short: "EZ"},
135 {val: 4, name: "TouchDevice", short: "TD"},
136 {val: 8, name: "Hidden", short: "HD"},
137 {val: 16, name: "HardRock", short: "HR"},
138 {val: 32, name: "SuddenDeath", short: "SD"},
139 {val: 64, name: "DoubleTime", short: "DT"},
140 {val: 128, name: "Relax", short: "RX"},
141 {val: 256, name: "HalfTime", short: "HT"},
142 {val: 512, name: "Nightcore", short: "NC"},
143 {val: 1024, name: "Flashlight", short: "FL"},
144 {val: 2048, name: "Autoplay", short: "AT"},
145 {val: 4096, name: "SpunOut", short: "SO"},
146 {val: 8192, name: "Relax2", short: "AP"},
147 {val: 16384, name: "Perfect", short: "PF"},
148 {val: 32768, name: "Key4", short: "4K"},
149 {val: 65536, name: "Key5", short: "5K"},
150 {val: 131072, name: "Key6", short: "6K"},
151 {val: 262144, name: "Key7", short: "7K"},
152 {val: 524288, name: "Key8", short: "8K"},
153 {val: 1048576, name: "FadeIn", short: "FI"},
154 {val: 2097152, name: "Random", short: "RD"},
155 {val: 4194304, name: "LastMod", short: "LM"},
156 {val: 16777216, name: "Key9", short: "9K"},
157 {val: 33554432, name: "Key10", short: "10K"},
158 {val: 67108864, name: "Key1", short: "1K"},
159 {val: 134217728, name: "Key3", short: "3K"},
160 {val: 268435456, name: "Key2", short: "2K"},
161 {val: 1073741824, name: "Mirror", short: "MR"}
162 ],
163 // if the first is set, the second has to be set also
164 doublemods = [
165 ["NC", "DT"],
166 ["PF", "SD"]
167 ],
168
169 playerCountries = null;
170
171 var defaultSettings = {
172 showMirror: true,
173 showSubscribeMap: true,
174 apikey: null,
175 failedChecked: true,
176 showDates: true,
177 showPpRank: false,
178 fetchPlayerCountries: true,
179 showTop100: true,
180 showDetailedHitCount: true,
181 showHitsPerPlay: true,
182 fetchUserpageMaxCombo: true,
183 fetchFirstsInfo: true,
184 rankingVisible: true,
185 forceShowDifficulties: false,
186 pp2dp: true,
187 };
188
189 var settings = {};
190 function initSettings(){
191 var promises = [];
192 //promises.push(GMX.setValue("apikey", null));
193 //promises.push(GMX.setValue("playerCountries", "{}"));
194 //promises.push(GMX.setValue("mapperSubList", "[]"));
195 //promises.push(GMX.setValue("mapSubList", "[]"));
196 var settings = {};
197 for(let settingVar in defaultSettings){
198 promises.push(GMX.getValue(settingVar, defaultSettings[settingVar]).then((result) => {
199 settings[settingVar] = result;
200 }));
201 }
202 promises.push(GMX.getValue("playerCountries", "{}").then((result) => {
203 playerCountries = typeof(result) === "string" ? JSON.parse(result) : {};
204 }));
205 return Promise.all(promises).then(() => {
206 settings.displayTopNum = settings.showTop100 ? 100 : 50;
207 return settings;
208 });
209 }
210
211 //peppy code
212 (function($) {
213 $.fn.textWidth = function() {
214 var html_org = $(this).html();
215 var html_calc = '<span>' + html_org + '</span>';
216 $(this).html(html_calc);
217 var width = $(this).find('span:first').width();
218 $(this).html(html_org);
219 return width;
220 }
221 ;
222 $.fn.marquee = function(args) {
223 var that = $(this);
224 var textWidth = that.textWidth()
225 , actualWidth = that.width()
226 , offset = 0
227 , width = 0
228 , css = {
229 'text-indent': that.css('text-indent'),
230 'overflow': that.css('overflow'),
231 'white-space': that.css('white-space')
232 }
233 , marqueeCss = {
234 'text-indent': width,
235 'overflow': 'hidden',
236 'white-space': 'nowrap'
237 }
238 , args = $.extend(true, {
239 count: -1,
240 speed: 1e1,
241 leftToRight: false
242 }, args)
243 , i = 0
244 , stop = (actualWidth - textWidth)
245 , dfd = $.Deferred();
246 if (textWidth < actualWidth || that.attr('stop') == -1)
247 return;
248 that.attr('stop', -1);
249 function go() {
250 if (!that.length)
251 return dfd.reject();
252 if (that.attr('stop') >= 0) {
253 that.css(css);
254 that.attr('stop', 0);
255 return dfd.resolve();
256 }
257 if (width == stop) {
258 i++;
259 if (i >= args.count) {
260 setTimeout(go, args.speed);
261 return;
262 }
263 if (args.leftToRight) {
264 width = textWidth * -1;
265 } else {
266 width = offset;
267 }
268 }
269 that.css('text-indent', width + 'px');
270 if (args.leftToRight) {
271 width++;
272 } else {
273 width--;
274 }
275 setTimeout(go, args.speed);
276 }
277 if (args.leftToRight) {
278 width = textWidth * -1;
279 width++;
280 stop = offset;
281 } else {
282 width--;
283 }
284 that.css(marqueeCss);
285 go();
286 return dfd.promise();
287 }
288 ;
289 })($);
290
291 var subscriberManager = (function(){
292 var MAPPER = 0,
293 MAP = 1;
294
295 function isSubscribed(list, id){
296 if(getIndex(list, id) === -1) return false;
297 else return true;
298 }
299
300 function add(list, params, type){
301 if(getIndex(list, params.id) === -1){
302 var x = {};
303 for(var p in params){
304 x[p] = params[p];
305 }
306 list.push(x);
307 sortList(list, type);
308 }
309 return list;
310 }
311
312 function remove(list, id){
313 var index = getIndex(list, id);
314 if(index === -1) return;
315 else{
316 list.splice(index, 1);
317 }
318 }
319
320 function getIndex(list, id){
321 for(var i=0; i<list.length; i++){
322 if(list[i].id === id){
323 return i;
324 }
325 }
326 return -1;
327 }
328
329 function sortList(list, type){
330 if(type === MAPPER){
331 list.sort(function(a, b){
332 return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
333 });
334 }else if(type === MAP){
335 list.sort(function(a, b){
336 if(a.artist.toLowerCase() < b.artist.toLowerCase()) return -1;
337 else if(a.artist.toLowerCase() > b.artist.toLowerCase()) return 1;
338 else if(a.title.toLowerCase() < b.title.toLowerCase()) return -1;
339 else if(a.title.toLowerCase() > b.title.toLowerCase()) return 1;
340 else return a.creator.toLowerCase() < b.creator.toLowerCase() ? -1 : 1;
341 });
342 }
343 }
344
345 async function getList(type){
346 if(type === MAPPER){
347 return JSON.parse(await GMX.getValue("mapperSubList", "[]"));
348 }else{
349 return JSON.parse(await GMX.getValue("mapSubList", "[]"));
350 }
351 }
352
353 async function saveList(list, type){
354 if(type === MAPPER){
355 await GMX.setValue("mapperSubList", JSON.stringify(list));
356 }else{
357 await GMX.setValue("mapSubList", JSON.stringify(list));
358 }
359 }
360
361 var me = {
362 mapper: {
363 isSubscribed: async function(id){
364 return isSubscribed(await getList(MAPPER), id);
365 },
366 add: async function(id, username){
367 var list = await getList(MAPPER);
368 add(list, {id: id, name: username}, MAPPER);
369 await saveList(list, MAPPER);
370 },
371 remove: async function(id){
372 var list = await getList(MAPPER);
373 remove(list, id);
374 await saveList(list, MAPPER);
375 },
376 removeList: async function(ls){
377 var list = await getList(MAPPER);
378 ls.forEach((id) => { remove(list, id); });
379 await saveList(list, MAPPER);
380 },
381 getList: async function(){
382 return getList(MAPPER);
383 }
384 },
385 map: {
386 isSubscribed: async function(id){
387 return isSubscribed(await getList(MAP), id);
388 },
389 add: async function(id, artist, title, creator, creator_id){
390 var list = await getList(MAP);
391 add(list, {id: id, artist: artist, title: title, creator: creator, creator_id: creator_id}, MAP);
392 await saveList(list, MAP);
393 },
394 remove: async function(id){
395 var list = await getList(MAP);
396 remove(list, id);
397 await saveList(list, MAP);
398 },
399 removeList: async function(ls){
400 var list = await getList(MAP);
401 ls.forEach((id) => { remove(list, id); });
402 await saveList(list, MAP);
403 },
404 getList: async function(){
405 return getList(MAP);
406 }
407 },
408 util: {
409 retrieveBeatmaps: async function(){
410 var result = [];
411 return Promise.all([subscriberManager.mapper.getList(), subscriberManager.map.getList()]).then(([mapperList, mapList]) => {
412 var promises = [];
413 mapperList.forEach(function(sub, index){
414 promises.push(new Promise((resolve, reject) => {
415 getBeatmaps({u: sub.id, type: "id"}, function(beatmaps){
416 beatmaps.forEach(function(beatmap){
417 beatmap.creator_id = sub.id;
418 });
419 result = result.concat(beatmaps);
420 resolve();
421 });
422 }));
423 });
424 mapList.forEach(function(map, index){
425 promises.push(new Promise((resolve, reject) => {
426 getBeatmaps({s: map.id}, function(beatmaps){
427 beatmaps.forEach(function(beatmap){
428 beatmap.creator_id = map.creator_id;
429 });
430 result = result.concat(beatmaps);
431 resolve();
432 });
433 }));
434 });
435 return Promise.all(promises);
436 }).then(() => {
437 return result;
438 });
439 },
440 mergeBeatmaps: function(beatmaps){
441 function addDifficulty(difficulties, beatmap){
442 for(var i=0; i<difficulties.length; i++){
443 if(difficulties[i].beatmap_id === beatmap.beatmap_id){
444 return;
445 }
446 }
447 difficulties.push({
448 beatmap_id: beatmap.beatmap_id,
449 version: beatmap.version,
450 mode: beatmap.mode,
451 playcount: beatmap.playcount,
452 difficultyrating: beatmap.difficultyrating
453 });
454 }
455
456 var merged = [];
457 beatmaps.forEach(function(beatmap){
458 var mergedlen = merged.length;
459 var index = -1;
460 for(var i=0; i<mergedlen; i++){
461 if(beatmap.beatmapset_id === merged[i].beatmapset_id){
462 index = i;
463 break;
464 }
465 }
466 if(index === -1){
467 merged.push({
468 beatmapset_id: beatmap.beatmapset_id,
469 approved: beatmap.approved,
470 last_update: beatmap.last_update,
471 artist: beatmap.artist,
472 title: beatmap.title,
473 creator: beatmap.creator,
474 creator_id: beatmap.creator_id,
475 source: beatmap.source,
476 favourite_count: beatmap.favourite_count,
477 difficulties: []
478 });
479 addDifficulty(merged[mergedlen].difficulties, beatmap);
480 }else{
481 addDifficulty(merged[index].difficulties, beatmap);
482 }
483 });
484
485 //Sort
486 merged.forEach(function(map){
487 map.difficulties.sort(function(a, b){
488 if(a.mode < b.mode) return -1;
489 if(a.mode > b.mode) return 1;
490 if(parseFloat(a.difficultyrating) < parseFloat(b.difficultyrating)) return -1;
491 else return 1;
492 });
493 });
494 merged.sort(function(a, b){
495 var date1 = new Date(a.last_update.replace(' ','T') + "+0800");
496 var date2 = new Date(b.last_update.replace(' ','T') + "+0800");
497 if(date1 > date2) return -1;
498 else return 1;
499 });
500 return merged;
501 },
502 addMapperName: async function(mapper){
503 return new Promise((resolve, reject) => {
504 getUser({u: mapper, type: "string"}, function(result){
505 if(result.length){
506 var id = result[0].user_id;
507 me.mapper.add(id, result[0].username).then(() => {
508 resolve(id);
509 });
510 }else{
511 reject(Error(`mapper ${mapper} not found`));
512 }
513 });
514 });
515 }
516 }
517 };
518 return me;
519 })();
520
521 $(document).ready(function(){
522 initSettings().then((_settings) => {
523 settings = _settings;
524 var url = window.location.href;
525 if(isOldSite(url)){
526 siteType = OLDSITE;
527 }else{
528 siteType = NEWSITE;
529 }
530 if(siteType === OLDSITE){
531 mainInit();
532 if(url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/u\//)){
533 pageType = USERPAGE;
534 osuplusUserpage().init();
535 }else if(url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/([bs]\/|p\/beatmap\?)/)){
536 pageType = BEATMAP;
537 osuplusBeatmap().init();
538 }else if(url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/p\/pp/)){
539 pageType = PPRANKING;
540 osuplusPpRanking().init();
541 }else if(url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/p\/beatmaplist/)){
542 pageType = BEATMAPLISTING;
543 osuplusBeatmapListing().init();
544 }
545 }else{
546 osuplusNew().init();
547 }
548 });
549 });
550
551 function isOldSite(url){
552 if(url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/u\//) ||
553 url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/([bs]\/|p\/beatmap\?)/) ||
554 url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/p\/pp/) ||
555 url.match(/^https?:\/\/(osu|old)\.ppy\.sh\/p\/beatmaplist/)){
556 return true;
557 }else{
558 return false;
559 }
560 }
561
562 function mainInit(){
563 apikey = settings.apikey;
564 if(apikey !== null){
565 hasKey = true;
566 }else{
567 hasKey = false;
568 displayGetKey();
569 }
570 insertSettings();
571 }
572
573 function insertSettings(){
574 function makeSettingRow(title, description, option){
575 return `<tr class='row2'><td><b>${title}</b>
576 ${description ? `<br>${description}` : ""}
577 </td><td class='settingOption'>${option}</td></tr>`;
578 }
579
580 function makeCheckboxOption(property){
581 return `<input type='checkbox' id='settings-${property}'
582 ${settings[property] ? " checked" : ""}>`;
583 }
584
585 if(!$(".osuplus-settings-style").length){
586 $(document.head).append($("<style class='osuplus-settings-style'></style>").html(
587 `#osuplusSettingsBtn {background:rgba(0,0,0,0.2) url(${settingsImg}) no-repeat 5px 10px; position:fixed; width:42px; height:47px; right:80px; cursor:pointer; z-index:10000;}
588 #osuplusModalOverlay {position:fixed; top:0px; width:100%; height:100%; z-index:19999; background:rgba(0,0,0,0.5);}
589 #osuplusModal {position:fixed; width:600px; z-index:20000; top:30px; background:white;
590 margin-left:-300px; left:50%; border-radius:10px; padding:15px;}
591 .osuplusModalClose {position:absolute; right:15px; top:15px;}
592 .osuplusSettingsContent {height:400px; overflow:auto; padding:10px; margin-bottom:10px;}
593 .osuplusSettingsTable {border-collapse:collapse;}
594 .osuplusSettingsTable tr {margin:1px;}
595 .settingOption {text-align:right;}`
596 ));
597 }
598
599 $(document.body).prepend("<div id='osuplusSettingsBtn'></div>");
600 $(document.body).append(
601 $("<div id='osuplusModalOverlay' style='display:none;'></div>"),
602 $("<div id='osuplusModal' style='display:none;'></div>").append(
603 "<h1>osuplus settings</h1>",
604 "<button class='osuplusModalClose'>x</button>",
605 $("<div class='osuplusSettingsContent'>").append(
606 $("<div>").append(
607 "<h2>General</h2>",
608 $("<table class='osuplusSettingsTable' width='100%'>").append(
609 makeSettingRow("API key", null, `<input type='text' style='display:none' id='settings-apikey' name='apikey' value='${settings.apikey}'>
610 <label><input type='checkbox' id='show-apikey'>Show</label>`)
611 )
612 ),
613 $("<div>").append(
614 "<h2>Beatmaps page</h2>",
615 $("<table class='osuplusSettingsTable' width='100%'>").append(
616 makeSettingRow("Show bloodcat mirror", null, makeCheckboxOption("showMirror")),
617 makeSettingRow("Show subscribe map", null, makeCheckboxOption("showSubscribeMap")),
618 makeSettingRow("Show dates", null, makeCheckboxOption("showDates")),
619 makeSettingRow("Show pp rank beside player", "scores may take longer to load", makeCheckboxOption("showPpRank")),
620 makeSettingRow("Fetch player countries outside top 50", "disable to load faster, but some players' countries won't be loaded", makeCheckboxOption("fetchPlayerCountries")),
621 makeSettingRow("Show top 100", "rather than top 50", makeCheckboxOption("showTop100")),
622 makeSettingRow("pp 2 decimal places", "rather than 0 dp", makeCheckboxOption("pp2dp"))
623 )
624 ),
625 $("<div>").append(
626 "<h2>Userpage</h2>",
627 $("<table class='osuplusSettingsTable' width='100%'>").append(
628 makeSettingRow("Show failed scores", "", makeCheckboxOption("failedChecked")),
629 makeSettingRow("Show detailed hit count", "", makeCheckboxOption("showDetailedHitCount")),
630 makeSettingRow("Show hits per play", "", makeCheckboxOption("showHitsPerPlay")),
631 makeSettingRow("Show top ranks max possible combo", "may take longer to load", makeCheckboxOption("fetchUserpageMaxCombo")),
632 makeSettingRow("Show first places detailed info", "may take longer to load", makeCheckboxOption("fetchFirstsInfo"))
633 )
634 ),
635 $("<div>").append(
636 "<h2>Performance ranking</h2>",
637 $("<table class='osuplusSettingsTable' width='100%'>").append(
638 makeSettingRow("Show global/country ranking", "", makeCheckboxOption("rankingVisible"))
639 )
640 ),
641 $("<div>").append(
642 "<h2>Beatmap Listing</h2>",
643 $("<table class='osuplusSettingsTable' width='100%'>").append(
644 makeSettingRow("Force show difficulties", "so no need to hover over maps", makeCheckboxOption("forceShowDifficulties"))
645 )
646 )
647 ),
648 $("<button id='osuplusSettingsSaveBtn'>Save</button>").click(function(){
649 GMX.setValue("apikey", $("#settings-apikey").val());
650 var properties = ["showMirror", "showSubscribeMap", "showDates", "showPpRank", "fetchPlayerCountries", "showTop100", "pp2dp", "failedChecked",
651 "showDetailedHitCount", "showHitsPerPlay", "fetchUserpageMaxCombo", "fetchFirstsInfo", "rankingVisible", "forceShowDifficulties"];
652 for(let property of properties){
653 setBoolProperty(property);
654 }
655 })
656 )
657 );
658
659 function setBoolProperty(property){
660 GMX.setValue(property, $(`#settings-${property}`).prop("checked"));
661 }
662
663 $("#show-apikey").click(function(){
664 if(this.checked){
665 $("#settings-apikey").show();
666 }else{
667 $("#settings-apikey").hide();
668 }
669 });
670
671 function openModal(){
672 $("#osuplusModal, #osuplusModalOverlay").fadeIn(200);
673 }
674
675 function closeModal(){
676 $("#osuplusModalOverlay, #osuplusModal").fadeOut(200);
677 }
678
679 $("#osuplusSettingsBtn").click(openModal);
680 $("#osuplusModalOverlay, .osuplusModalClose, #osuplusSettingsSaveBtn").click(closeModal);
681 }
682
683 function osuplusNew(){
684 var currentBody = null,
685 currentOsuplus = {
686 init: function(){},
687 destroy: function(){}
688 };
689
690 function init(){
691 setInterval(function(){
692 if(currentBody != document.body){
693 currentBody = document.body;
694 mainInit();
695 var url = window.location.href;
696 var pathname = window.location.pathname;
697 if(pathname == "/beatmapsets"){
698 pageType = BEATMAPLISTING;
699 setNewOsuplus(osuplusNewBeatmapListing);
700 }else if(url.match(/^https?:\/\/osu\.ppy\.sh\/beatmapsets\//)){
701 pageType = BEATMAP;
702 setNewOsuplus(osuplusNewBeatmap);
703 }else if(url.match(/^https?:\/\/osu\.ppy\.sh\/users\//)){
704 pageType = USERPAGE;
705 setNewOsuplus(osuplusNewUserpage);
706 }else if(url.match(/^https?:\/\/osu\.ppy\.sh\/rankings\//)){
707 pageType = PPRANKING;
708 setNewOsuplus(osuplusNewPpRanking);
709 }
710 }
711 }, 1000);
712 }
713
714 function setNewOsuplus(newOsuplus){
715 currentOsuplus.destroy();
716 currentOsuplus = newOsuplus();
717 currentOsuplus.init();
718 }
719
720 $(window).unload(function(){
721 currentOsuplus.destroy();
722 });
723
724 return {init: init};
725 }
726
727 function osuplusBeatmapListing(){
728 var subsTitle = null,
729 subsContent = null,
730 beatmaplistingTitle = null,
731 loadingNotice = null,
732 beatmaplistingContent = null,
733 mappersSelect = null,
734 mappersRemoveBtn = null,
735 mapsSelect = null,
736 mapsRemoveBtn = null,
737 subsTxtbox = null,
738 subsAddBtn = null,
739 pageLefts = null,
740 pageRights = null,
741 pageDisplays = null,
742 subsBeatmaplist = null,
743 beatmapList = [],
744 pages = 1,
745 curPage = 1,
746 pageSize = 40,
747 refreshBtn = null;
748
749 //0: unloaded
750 //1: loading
751 //2: loaded
752 var subLoadingStatus = 0;
753
754 function addCss(){
755 $(document.head).append($("<style></style>").html(
756 ".small-content-with-bg {background-image: url(//s.ppy.sh/images/main-bg-new.png);}\n" +
757 ".maintitlediv {font-size: 160%; padding-left: 20px; padding-bottom: 30px;}\n" +
758 ".maintitle {padding: 4px; margin-right: 10px;}\n" +
759 ".maintitle:hover, .maintitle.selected {background-color: #A9A9FF; color: white; text-decoration: none;}\n" +
760 "#subsControl {padding: 20px;}\n" +
761 "#subsControl th {width: 80px; padding-right: 10px; float: left; text-align: right;}\n" +
762 "#mappersSelect {width: 140px;}\n" +
763 "#subsTxtbox {width: 100px;}\n" +
764 "#mapsSelect {width: 450px;}\n" +
765 ".subsBtn {font-size: 100%;}\n" +
766 ".subsTxt, #mappersSelect option, #mapsSelect option {font-size: 110%;}\n" +
767 "#pageDisplay {padding-left: 15px; padding-right: 15px;}\n" +
768 ".centered {display: block; margin-left: auto; margin-right: auto;}\n"
769 ));
770 }
771
772 function init(){
773 addCss();
774 beatmaplistingContent = $(".content-with-bg");
775 subsTitle = $("<a class='maintitle'>Subscriptions</a>");
776 subsContent = $("<div class='subContent content-with-bg'></div>").hide();
777 beatmaplistingTitle = $("<a class='maintitle selected'>Beatmap Listing</a>");
778 loadingNotice = $("<div><img src='" + loaderImg + "' class='centered'></div>");
779 subsTitle.click(function(){
780 subsTitle.addClass("selected");
781 subsContent.show();
782 beatmaplistingTitle.removeClass("selected");
783 beatmaplistingContent.hide();
784 if(subLoadingStatus === 0){
785 loadSubs();
786 }
787 });
788 beatmaplistingTitle.click(function(){
789 beatmaplistingTitle.addClass("selected");
790 beatmaplistingContent.show();
791 subsTitle.removeClass("selected");
792 subsContent.hide();
793 });
794 beatmaplistingContent.before($("<div class='maintitlediv small-content-with-bg'>").append(beatmaplistingTitle, subsTitle));
795 beatmaplistingContent.after(subsContent);
796 $(".newHeader").remove();
797
798 mappersSelect = $("<select id='mappersSelect' multiple size=5>");
799 mappersRemoveBtn = $("<button id='mappersRemoveBtn' class='subsBtn'>Remove</button>");
800 mapsSelect = $("<select id='mapsSelect' multiple size=5>");
801 mapsRemoveBtn = $("<button id='mapsRemoveBtn' class='subsBtn'>Remove</button>");
802 subsTxtbox = $("<input id='subsTxtbox' class='subsTxt'>");
803 subsAddBtn = $("<button id='subsAddBtn' class='subsBtn'>Add</button>");
804 var pageLeft = $("<button class='subsBtn'><</button>");
805 var pageRight = $("<button class='subsBtn'>></button>");
806 var pageDisplay = $("<span class='pageDisplay'>Page 1/1</span>");
807 var pageLeft2 = $("<button class='subsBtn'><</button>");
808 var pageRight2 = $("<button class='subsBtn'>></button>");
809 var pageDisplay2 = $("<span class='pageDisplay'>Page 1/1</span>");
810 pageLefts = $.merge(pageLeft, pageLeft2);
811 pageRights = $.merge(pageRight, pageRight2);
812 pageDisplays = $.merge(pageDisplay, pageDisplay2);
813 subsBeatmaplist = $("<div class='beatmapListing'>");
814 refreshBtn = $("<button class='subsBtn'>Refresh</button>");
815
816 pageLefts.click(function(){
817 if(curPage > 1){
818 curPage--;
819 refreshPage();
820 }
821 });
822 pageRights.click(function(){
823 if(curPage < pages){
824 curPage++;
825 refreshPage();
826 }
827 });
828 refreshBtn.click(function(){
829 if(subLoadingStatus !== 1){
830 loadSubs();
831 }
832 });
833
834 subsContent.append(
835 $("<div id='subsControl'>").append(
836 $("<table style='width:100%;'>").append(
837 $("<tr>").append(
838 $("<th>Subscribed mappers:</th>"),
839 $("<td>").append(
840 mappersSelect, "<br>", mappersRemoveBtn
841 ),
842 $("<th>Subscribed maps:</th>"),
843 $("<td>").append(
844 mapsSelect, "<br>", mapsRemoveBtn
845 )
846 ),
847 $("<tr>").append(
848 $("<th>Add mapper:</th>"),
849 $("<td>").append(
850 subsTxtbox, " ", subsAddBtn
851 )
852 )
853 )
854 ),
855 $("<div class='pagination'>").append(
856 refreshBtn, "<br>",
857 pageLeft, pageDisplay, pageRight
858 ),
859 loadingNotice,
860 subsBeatmaplist,
861 $("<div class='pagination'>").append(
862 pageLeft2, pageDisplay2, pageRight2
863 )
864 );
865 refreshMappersSub();
866 refreshMapsSub();
867
868 subsAddBtn.click(function(){
869 subscriberManager.util.addMapperName(subsTxtbox.val()).then(refreshMappersSub)
870 .catch((e) => {
871 alert("User does not exist!");
872 });
873 });
874 mappersRemoveBtn.click(function(){
875 var selected = mappersSelect.find(":selected");
876 var toRemove = [];
877 selected.each(function(i, ele){
878 toRemove.push(ele.value);
879 });
880 subscriberManager.mapper.removeList(toRemove).then(refreshMappersSub);
881 });
882 mapsRemoveBtn.click(function(){
883 var selected = mapsSelect.find(":selected");
884 var toRemove = [];
885 selected.each(function(i, ele){
886 toRemove.push(ele.value);
887 });
888 subscriberManager.map.removeList(toRemove).then(refreshMapsSub);
889 });
890
891 if(settings.forceShowDifficulties){
892 $(".initiallyHidden").removeClass("initiallyHidden");
893 }
894 }
895
896 function refreshPage(){
897 subsBeatmaplist.children().hide();
898 subsBeatmaplist.children().eq(curPage - 1).show();
899 pageDisplays.text("Page "+curPage+"/"+pages);
900 }
901
902 function loadSubs(){
903 subLoadingStatus = 1;
904 subsBeatmaplist.empty();
905 loadingNotice.show();
906 subscriberManager.util.retrieveBeatmaps().then((result) => {
907 beatmapList = subscriberManager.util.mergeBeatmaps(result);
908 if(beatmapList.length === 0) pages = 1;
909 else{
910 pages = (((beatmapList.length-1) / pageSize) >> 0) + 1;
911 }
912 curPage = 1;
913 var pageContainer = $("<div class='beatmapListing'>").hide();
914 beatmapList.forEach(function(beatmapset, index){
915 if(index % pageSize === 0){
916 if(index > 0){
917 subsBeatmaplist.append(pageContainer);
918 pageContainer = $("<div class='beatmapListing'>").hide();
919 }
920 }
921 pageContainer.append(makeBeatmapBox(beatmapset));
922 });
923 subsBeatmaplist.append(pageContainer);
924 refreshPage();
925
926 $(".timeago").timeago();
927 $(".sub-beatmap").hover(function() {
928 $(this).find(".maintext").marquee({
929 speed: 60
930 });
931 $(this).find(".initiallyHidden").stop().fadeTo(1, 100);
932 $(this).find(".bmlist-options").clearQueue().stop().delay(500).animate({
933 width: 'show'
934 }, 100);
935 }, function() {
936 $(this).find(".initiallyHidden").fadeOut(400);
937 $(this).find(".maintext").attr('stop', 1);
938 $(this).find(".bmlist-options").clearQueue().stop().delay(500).animate({
939 width: 'hide'
940 }, 100);
941 }).click(function(event) {
942 return load(this.id, event);
943 });
944 $(".sub-beatmap .bmlist-options .icon-heart").click(function() {
945 $.ajax({
946 url: "/web/favourite.php?localUserCheck=" + localUserCheck + "&a=" + $(this).parent().parent().parent().attr('id')
947 }).done(function() {
948 alert("Added as favourite!");
949 });
950 return false;
951 });
952 $(".sub-beatmap .bmlistt").click(function() {
953 $('.sub-beatmap .bmlistt>.icon-pause').removeClass("icon-pause").addClass("icon-play");
954 if (playBeatmapPreview($(this).parent().attr('id')))
955 $(this).find('i').removeClass("icon-play").addClass("icon-pause");
956 return false;
957 });
958 subLoadingStatus = 2;
959 loadingNotice.hide();
960 });
961 }
962
963 function makeBeatmapBox(beatmapset){
964 var id = beatmapset.beatmapset_id;
965 var lastupdate = new Date(beatmapset.last_update.replace(' ','T') + "+0800");
966 var initiallyHiddenClass = "initiallyHidden";
967 if(settings.forceShowDifficulties) initiallyHiddenClass = "";
968 var approved = "?";
969 switch(beatmapset.approved){
970 case "-2":
971 approved = "Graveyard"; break;
972 case "-1":
973 approved = "WIP"; break;
974 case "0":
975 approved = "Pending"; break;
976 case "1":
977 approved = "Ranked"; break;
978 case "2":
979 approved = "Approved"; break;
980 case "3":
981 approved = "Qualified"; break;
982 case "4":
983 approved = "Loved";
984 }
985 var source = "";
986 if(beatmapset.source !== ""){
987 source = `<div class='${initiallyHiddenClass}'><span class='light'>from</span> ${beatmapset.source}</div>`;
988 }
989 var difficulties = "";
990 beatmapset.difficulties.forEach(function(beatmap){
991 difficulties += `<div class='diffIcon ${getDifficultyClass(beatmap.mode, beatmap.difficultyrating)}'></div> `;
992 });
993
994 return $(
995 `<div class='beatmap sub-beatmap' id='${id}' style='width:420px;'>
996 <div class='bmlistt' style='background: url(//b.ppy.sh/thumb/${id}.jpg)'>
997 <i class='icon-play'></i>
998 </div>
999 <div class='bmlist-options'>
1000 <a href='#'><i class='icon-heart'></i></a>
1001 <a class='require-login beatmap_download_link' href='/d/${id}'><i class='icon-download-alt'></i></a>
1002 </div>
1003 <div class='maintext'>
1004 <span class='artist'>${beatmapset.artist}</span> - <a href='/s/${id}' class='title'>${beatmapset.title}</span></a>
1005 </div>
1006 <div class='left-aligned'>
1007 <div><span class='light'>mapped by</span> <a href='/u/${beatmapset.creator_id}'>${beatmapset.creator}</a></div>
1008 ${source}
1009 <div class='difficulties ${initiallyHiddenClass}'>${difficulties} </div>
1010 </div>
1011 <div class='right-aligned'>
1012 <div class='status'><b>${approved}</b></div>
1013 <div class='small-details'>
1014 <div><time class='timeago' datetime='${lastupdate.toISOString()}'>${lastupdate.toLocaleString()}</time></div>
1015 <i class='icon-heart'></i> ${beatmapset.favourite_count}
1016 </div>
1017 </div>
1018 </div>`
1019 );
1020 }
1021
1022 async function refreshMappersSub(id){
1023 var optionsls = [];
1024 var subscribedList = await subscriberManager.mapper.getList();
1025 var sublen = subscribedList.length;
1026 var focus = null;
1027 mappersSelect.find("option").remove();
1028 for(var i=0; i<sublen; i++){
1029 var option = $(`<option value='${subscribedList[i].id}'>${subscribedList[i].name}</option>`);
1030 if(subscribedList[i].id === id) focus = option;
1031 optionsls.push(option);
1032 }
1033 mappersSelect.append(optionsls);
1034 if(focus !== null){
1035 focus.prop("selected", true);
1036 }
1037 }
1038
1039 async function refreshMapsSub(){
1040 var optionsls = [];
1041 var subscribedList = await subscriberManager.map.getList();
1042 var sublen = subscribedList.length;
1043 mapsSelect.find("option").remove();
1044 for(var i=0; i<sublen; i++){
1045 var map = subscribedList[i];
1046 var option = $(`<option value='${map.id}'>${map.artist} - ${map.title} (${map.creator})</option>`);
1047 optionsls.push(option);
1048 }
1049 mapsSelect.append(optionsls);
1050 }
1051
1052 function getDifficultyClass(mode, difficultyrating){
1053 var diff = "?";
1054 var rating = parseFloat(difficultyrating);
1055 if(rating < 1.5) diff = "easy";
1056 else if(rating < 2.25) diff = "normal";
1057 else if(rating < 3.75) diff = "hard";
1058 else if (rating < 5.25) diff = "insane";
1059 else diff = "expert";
1060 if(mode === "1") diff += "-t";
1061 else if(mode === "2") diff += "-f";
1062 else if(mode === "3") diff += "-m";
1063 return diff;
1064 }
1065
1066 return {init: init};
1067 }
1068
1069 function osuplusNewBeatmapListing(){
1070 var subsTitle = null,
1071 subsContent = null,
1072 beatmaplistingTitle = null,
1073 loadingNotice = null,
1074 mappersSelect = null,
1075 mappersRemoveBtn = null,
1076 mapsSelect = null,
1077 mapsRemoveBtn = null,
1078 subsTxtbox = null,
1079 subsAddBtn = null,
1080 pageLefts = null,
1081 pageRights = null,
1082 pageDisplays = null,
1083 subsBeatmaplist = null,
1084 beatmapList = [],
1085 pages = 1,
1086 curPage = 1,
1087 pageSize = 40,
1088 refreshBtn = null;
1089
1090 //0: unloaded
1091 //1: loading
1092 //2: loaded
1093 var subLoadingStatus = 0;
1094
1095 function addCss(){
1096 if(!$(".osuplus-new-beatmaplisting-style").length){
1097 $(document.head).append($("<style class='osuplus-new-beatmaplisting-style'></style>").html(
1098 `.small-content-with-bg {background-image: url(//s.ppy.sh/images/main-bg-new.png);}
1099 .maintitle {padding: 4px; margin-right: 10px; font-size: 25px;}
1100 .maintitle:hover {cursor: pointer;}
1101 #subsControl {padding: 20px;}
1102 #subsControl th {width: 80px; padding-right: 10px; float: left; text-align: right;}
1103 #mappersSelect {width: 180px;}
1104 #subsTxtbox {width: 140px;}
1105 #mapsSelect {width: 500px;}
1106 .subsBtn {font-size: 100%;}
1107 .subsTxt, #mappersSelect option, #mapsSelect option {font-size: 110%;}
1108 .pageDisplay {padding-left: 15px; padding-right: 15px;}
1109 .centered {display: block; margin-left: auto; margin-right: auto;}
1110 .subs-pagination {text-align: center; padding: 10px 0px;}
1111 .beatmapset-panel__date {bottom: 10px; right: 10px; text-align: right; position: absolute;}
1112 .op-sub-txt {color: white;}`
1113 ));
1114 }
1115 }
1116
1117 function init(){
1118 if($("#osuplusloaded").length) return;
1119 $("body").append("<a hidden id='osuplusloaded'></a>");
1120 addCss();
1121 subLoadingStatus = 0;
1122 subsContent = $("<div class='subContent'></div>").hide();
1123 loadingNotice = $(`<div><img src='${loaderImg}' class='centered'></div>`);
1124 $(".osu-page--beatmapsets-search-header").parent().before(
1125 `<div class='osu-layout__section'>
1126 <div class='osu-page'>
1127 <ul class='page-mode'>
1128 <li class="page-mode__item">
1129 <a class="page-mode-link page-mode-link--is-active maintitle beatmaplisting-title">
1130 Beatmap Listing
1131 <span class="page-mode-link__stripe"></span>
1132 </a>
1133 </li>
1134 <li class="page-mode__item">
1135 <a class="page-mode-link maintitle subs-title">
1136 Subscriptions
1137 <span class="page-mode-link__stripe"></span>
1138 </a>
1139 </li>
1140 </ul>
1141 </div>
1142 </div>`
1143 );
1144 subsTitle = $(".subs-title");
1145 beatmaplistingTitle = $(".beatmaplisting-title");
1146 subsTitle.click(function(){
1147 subsTitle.addClass("page-mode-link--is-active");
1148 subsContent.show();
1149 beatmaplistingTitle.removeClass("page-mode-link--is-active");
1150 $(".osu-layout__row--page-compact").hide();
1151 if(subLoadingStatus === 0){
1152 loadSubs();
1153 }
1154 });
1155 beatmaplistingTitle.click(function(){
1156 beatmaplistingTitle.addClass("page-mode-link--is-active");
1157 $(".osu-layout__row--page-compact").show();
1158 subsTitle.removeClass("page-mode-link--is-active");
1159 subsContent.hide();
1160 });
1161 $(".js-react--beatmaps").append($("<div class='osu-layout__section'></div>").append(
1162 $("<div class='osu-page'></div>").append(subsContent)
1163 )
1164 );
1165
1166 mappersSelect = $("<select id='mappersSelect' multiple size=5>");
1167 mappersRemoveBtn = $("<button id='mappersRemoveBtn' class='subsBtn'>Remove</button>");
1168 mapsSelect = $("<select id='mapsSelect' multiple size=5>");
1169 mapsRemoveBtn = $("<button id='mapsRemoveBtn' class='subsBtn'>Remove</button>");
1170 subsTxtbox = $("<input id='subsTxtbox' class='subsTxt'>");
1171 subsAddBtn = $("<button id='subsAddBtn' class='subsBtn'>Add</button>");
1172 var pageLeft = $("<button class='subsBtn'><</button>");
1173 var pageRight = $("<button class='subsBtn'>></button>");
1174 var pageDisplay = $("<span class='pageDisplay op-sub-txt'>Page 1/1</span>");
1175 var pageLeft2 = $("<button class='subsBtn'><</button>");
1176 var pageRight2 = $("<button class='subsBtn'>></button>");
1177 var pageDisplay2 = $("<span class='pageDisplay op-sub-txt'>Page 1/1</span>");
1178 pageLefts = $.merge(pageLeft, pageLeft2);
1179 pageRights = $.merge(pageRight, pageRight2);
1180 pageDisplays = $.merge(pageDisplay, pageDisplay2);
1181 subsBeatmaplist = $("<div class='beatmapsets'>");
1182 refreshBtn = $("<button class='subsBtn'>Refresh</button>");
1183
1184 pageLefts.click(function(){
1185 if(curPage > 1){
1186 curPage--;
1187 refreshPage();
1188 }
1189 });
1190 pageRights.click(function(){
1191 if(curPage < pages){
1192 curPage++;
1193 refreshPage();
1194 }
1195 });
1196 refreshBtn.click(function(){
1197 if(subLoadingStatus !== 1){
1198 loadSubs();
1199 }
1200 });
1201
1202 subsContent.append(
1203 $("<div id='subsControl'>").append(
1204 $("<table style='width:100%;'>").append(
1205 $("<tr>").append(
1206 $("<th class='op-sub-txt'>Subscribed mappers:</th>"),
1207 $("<td>").append(
1208 mappersSelect, "<br>", mappersRemoveBtn
1209 ),
1210 $("<th class='op-sub-txt'>Subscribed maps:</th>"),
1211 $("<td>").append(
1212 mapsSelect, "<br>", mapsRemoveBtn
1213 )
1214 ),
1215 $("<tr>").append(
1216 $("<th class='op-sub-txt'>Add mapper:</th>"),
1217 $("<td>").append(
1218 subsTxtbox, " ", subsAddBtn
1219 )
1220 )
1221 )
1222 ),
1223 $("<div class='subs-pagination'>").append(
1224 refreshBtn, "<br>",
1225 pageLeft, pageDisplay, pageRight
1226 ),
1227 loadingNotice,
1228 subsBeatmaplist,
1229 $("<div class='subs-pagination'>").append(
1230 pageLeft2, pageDisplay2, pageRight2
1231 )
1232 );
1233 refreshMappersSub();
1234 refreshMapsSub();
1235
1236 subsAddBtn.click(function(){
1237 subscriberManager.util.addMapperName(subsTxtbox.val()).then(refreshMappersSub)
1238 .catch(() => {
1239 alert("User does not exist!");
1240 });
1241 });
1242 mappersRemoveBtn.click(function(){
1243 var selected = mappersSelect.find(":selected");
1244 var toRemove = [];
1245 selected.each(function(i, ele){
1246 toRemove.push(ele.value);
1247 });
1248 subscriberManager.mapper.removeList(toRemove).then(refreshMappersSub);
1249 });
1250 mapsRemoveBtn.click(function(){
1251 var selected = mapsSelect.find(":selected");
1252 var toRemove = [];
1253 selected.each(function(i, ele){
1254 toRemove.push(ele.value);
1255 });
1256 subscriberManager.map.removeList(toRemove).then(refreshMapsSub);
1257 });
1258 }
1259
1260 function refreshPage(){
1261 subsBeatmaplist.children().hide();
1262 subsBeatmaplist.children().eq(curPage - 1).show();
1263 pageDisplays.text("Page "+curPage+"/"+pages);
1264 }
1265
1266 function loadSubs(){
1267 subLoadingStatus = 1;
1268 subsBeatmaplist.empty();
1269 loadingNotice.show();
1270 subscriberManager.util.retrieveBeatmaps().then((result) => {
1271 beatmapList = subscriberManager.util.mergeBeatmaps(result);
1272 if(beatmapList.length === 0) pages = 1;
1273 else{
1274 pages = (((beatmapList.length-1) / pageSize) >> 0) + 1;
1275 }
1276 curPage = 1;
1277 var pageContainer = $("<div class='beatmapListing-page'>").hide();
1278 var pageRow = $("<div class='beatmapListing-row beatmapsets__items-row'>");
1279 pageContainer.append(pageRow);
1280 beatmapList.forEach(function(beatmapset, index){
1281 if(index % pageSize === 0){
1282 if(index > 0){
1283 subsBeatmaplist.append(pageContainer);
1284 pageContainer = $("<div class='beatmapListing-page'>").hide();
1285 }
1286 }
1287 if(index % 2 === 0 && index > 0){
1288 pageRow = $("<div class='beatmapListing-row beatmapsets__items-row'>");
1289 pageContainer.append(pageRow);
1290 }
1291 pageRow.append(makeBeatmapBox(beatmapset));
1292 });
1293 subsBeatmaplist.append(pageContainer);
1294 refreshPage();
1295
1296 subLoadingStatus = 2;
1297 loadingNotice.hide();
1298 });
1299 }
1300
1301 function makeBeatmapBox(beatmapset){
1302 var id = beatmapset.beatmapset_id;
1303 var approved = "?";
1304 switch(beatmapset.approved){
1305 case "-2":
1306 approved = "Graveyard"; break;
1307 case "-1":
1308 approved = "WIP"; break;
1309 case "0":
1310 approved = "Pending"; break;
1311 case "1":
1312 approved = "Ranked"; break;
1313 case "2":
1314 approved = "Approved"; break;
1315 case "3":
1316 approved = "Qualified"; break;
1317 case "4":
1318 approved = "Loved";
1319 }
1320 var dateset = new Date(beatmapset.last_update.replace(' ','T') + "+0000");
1321
1322 return $(
1323 `<div class="beatmapsets__item">
1324 <div class="beatmapset-panel">
1325 <div class="beatmapset-panel__panel">
1326 <a href="/beatmapsets/${id}" class="beatmapset-panel__header">
1327 <img src="https://assets.ppy.sh/beatmaps/${id}/covers/card.jpg" srcset="https://assets.ppy.sh/beatmaps/${id}/covers/card.jpg 1x, https://assets.ppy.sh/beatmaps/${id}/covers/card@2x.jpg 2x" class="beatmapset-panel__image">
1328 <div class="beatmapset-panel__image-overlay"></div>
1329 <div class="beatmapset-panel__status-container">
1330 <div class="beatmapset-status">${approved}</div>
1331 </div>
1332 <div class="beatmapset-panel__title-artist-box">
1333 <div class="u-ellipsis-overflow beatmapset-panel__header-text beatmapset-panel__header-text--title">${beatmapset.title}</div>
1334 <div class="beatmapset-panel__header-text">${beatmapset.artist}</div>
1335 </div>
1336 <div class="beatmapset-panel__counts-box">
1337 <div class="beatmapset-panel__count"><span class="beatmapset-panel__count-number">${commarise(beatmapset.favourite_count)}</span><i class="fas fa-fw fa-heart"></i></div>
1338 </div>
1339 <div class="beatmapset-panel__preview-bar" style="transition-duration: 0s; width: 0px;"></div>
1340 </a>
1341 <div class="beatmapset-panel__content">
1342 <div class="beatmapset-panel__row">
1343 <div class="beatmapset-panel__mapper-source-box">
1344 <div class="u-ellipsis-overflow">mapped by <a href="/users/${beatmapset.creator_id}" class="js-usercard" data-user-id="${beatmapset.creator_id}">${beatmapset.creator}</a></div>
1345 <div class="u-ellipsis-overflow">${beatmapset.source}</div>
1346 </div>
1347 <div class="beatmapset-panel__icons-box"><a href="/beatmapsets/${id}/download" class="beatmapset-panel__icon js-beatmapset-download-link" data-turbolinks="false"><i class="fas fa-download"></i></a></div>
1348 </div>
1349 <div class="beatmapset-panel__difficulties">
1350 ${beatmapset.difficulties.map(function(beatmap){
1351 var difficulty = getDifficultyClass(beatmap.difficultyrating);
1352 var diffrating = parseFloat(beatmap.difficultyrating).toFixed(2);
1353 return `
1354 <div class="beatmapset-panel__difficulty-icon">
1355 <div class="beatmap-icon beatmap-icon--${difficulty} beatmap-icon--with-hover js-beatmap-tooltip" data-beatmap-title="${beatmap.version}" data-stars="${diffrating}" data-difficulty="${difficulty}">
1356 <div class="beatmap-icon__shadow"></div>
1357 <i class="fal fa-extra-mode-${intToMode(parseInt(beatmap.mode))}"></i>
1358 </div>
1359 </div>`;
1360 }).join("")}
1361 </div>
1362 <div class="beatmapset-panel__date">
1363 <time class="timeago" datetime="${dateset.toISOString()}">${dateset.toLocaleString()}</time>
1364 </div>
1365 </div>
1366 </div>
1367 <a href="#" class="beatmapset-panel__play js-audio--play" data-audio-url="//b.ppy.sh/preview/${id}.mp3"><i class="fas fa-play"></i></a>
1368 <div class="beatmapset-panel__shadow"></div>
1369 </div>
1370 </div>`
1371 );
1372 }
1373
1374 async function refreshMappersSub(id){
1375 mappersSelect.find("option").remove();
1376 var optionsls = [];
1377 var subscribedList = await subscriberManager.mapper.getList();
1378 var sublen = subscribedList.length;
1379 var focus = null;
1380 for(var i=0; i<sublen; i++){
1381 var option = $("<option value='" + subscribedList[i].id + "'>" + subscribedList[i].name + "</option>");
1382 if(subscribedList[i].id === id) focus = option;
1383 optionsls.push(option);
1384 }
1385 mappersSelect.append(optionsls);
1386 if(focus !== null){
1387 focus.prop("selected", true);
1388 }
1389 }
1390
1391 async function refreshMapsSub(){
1392 mapsSelect.find("option").remove();
1393 var optionsls = [];
1394 var subscribedList = await subscriberManager.map.getList();
1395 var sublen = subscribedList.length;
1396 for(var i=0; i<sublen; i++){
1397 var map = subscribedList[i];
1398 var option = $("<option value='" + map.id + "'>" +
1399 map.artist+" - "+map.title+" ("+map.creator+")</option>");
1400 optionsls.push(option);
1401 }
1402 mapsSelect.append(optionsls);
1403 }
1404
1405 function getDifficultyClass(difficultyrating){
1406 var rating = parseFloat(difficultyrating);
1407 if(rating < 1.5) return "easy";
1408 if(rating < 2.25) return "normal";
1409 if(rating < 3.75) return "hard";
1410 if(rating < 5.25) return "insane";
1411 if(rating < 6.75) return "expert";
1412 return "expert-plus";
1413 }
1414
1415 function destroy(){
1416 $(".osuplus-new-beatmaplisting-style").remove();
1417 }
1418
1419 return {init: init, destroy: destroy};
1420 }
1421
1422 function osuplusPpRanking(){
1423 var country = null,
1424 isGlobal = true,
1425 mode = null,
1426 tableBody = null,
1427 tableLoadingNotice = null,
1428 playerInfo = [];
1429
1430
1431 function addCss(){
1432 $(document.head).append($("<style></style>").html(
1433 `.centered {display: block; margin-left: auto; margin-right: auto;}`
1434 ));
1435 }
1436
1437 function init(){
1438 addCss();
1439 if(window.location.search === ""){
1440 isGlobal = true;
1441 mode = 0;
1442 }else{
1443 var searchObj = searchParser(window.location.search);
1444 if(searchObj.c === undefined){
1445 isGlobal = true;
1446 }else{
1447 isGlobal = false;
1448 }
1449 if(searchObj.m === undefined){
1450 mode = 0;
1451 }else{
1452 mode = searchObj.m;
1453 }
1454 }
1455 tableBody = $(".beatmapListing > tbody");
1456
1457 //Add Loader
1458 tableLoadingNotice = $(`<div><img src='${loaderImg}' class='centered'></div>`).hide();
1459 $("#tablist").before(tableLoadingNotice);
1460
1461 //Add global/country ranking
1462 if(settings.rankingVisible){
1463 tableLoadingNotice.show();
1464
1465 //Get player list
1466 var playerList = [];
1467 tableBody.children().first().nextAll().each(function(index, ele){
1468 var href = $(ele).children().eq(1).children().eq(1).attr("href");
1469 playerList[index] = href.split("/")[2];
1470 });
1471 var funs = [];
1472 playerList.forEach(function(id, index){
1473 funs.push(function(donecb){
1474 getUser({u: id, m: mode, type: "id"}, function(response){
1475 playerInfo[index] = response[0];
1476 donecb();
1477 });
1478 });
1479 });
1480 doManyFunc(funs, function(){
1481 tableLoadingNotice.hide();
1482
1483 //Add new headers
1484 var newHeader = isGlobal ? "Country" : "Global";
1485 tableBody.children().first().children().first().after("<th class='rank2col'>" + newHeader + "</th>");
1486
1487 //Add new ranks
1488 tableBody.children().first().nextAll().each(function(index, row){
1489 row = $(row);
1490 var newRank = isGlobal ? playerInfo[index].pp_country_rank : playerInfo[index].pp_rank;
1491 row.children().first().after("<td class='rank2col'>#" + newRank + "</td>");
1492 });
1493 });
1494 }
1495 }
1496
1497 function searchParser(str){
1498 if(str[0] === "?") str = str.slice(1);
1499 var arr = str.split("&");
1500 var rtn = {};
1501 arr.forEach(function(x){
1502 var xsplit = x.split("=");
1503 if(xsplit.length > 1){
1504 rtn[xsplit[0]] = xsplit[1];
1505 }
1506 })
1507 return rtn;
1508 }
1509
1510 return {init: init}
1511 }
1512
1513 function osuplusNewPpRanking(){
1514 var isGlobal = true,
1515 mode = null,
1516 tableLoadingNotice = null,
1517 playerInfo = [];
1518
1519
1520 function addCss(){
1521 if(!$(".osuplus-new-ppranking-style").length){
1522 $(document.head).append($("<style class='osuplus-new-ppranking-style'></style>").html(
1523 `.centered {display: block; margin-left: auto; margin-right: auto;}`
1524 ));
1525 }
1526 }
1527
1528 function init(){
1529 if($("#osuplusloaded").length) return;
1530 $("body").append("<a hidden id='osuplusloaded'></a>");
1531 var path = window.location.pathname.split('/');
1532 if(path[3] != "performance") return;
1533
1534 addCss();
1535 mode = modeToInt(path[2]);
1536 var searchObj = searchParser(window.location.search);
1537 if(searchObj.country){
1538 isGlobal = false;
1539 }else{
1540 isGlobal = true;
1541 }
1542
1543 //Add Loader
1544 tableLoadingNotice = $(`<div><img src='${loaderImg}' class='centered'></div>`).hide();
1545 $(".ranking-page-table").before(tableLoadingNotice);
1546
1547 //Add global/country ranking
1548 if(settings.rankingVisible){
1549 tableLoadingNotice.show();
1550
1551 //Get player list
1552 var playerList = $(".ranking-page-table__row").map(function(i, ele){
1553 var temp = $(ele).find(".ranking-page-table__user-link a").eq(1).attr("href").split("/");
1554 return temp[temp.length-1];
1555 });
1556 var funs = [];
1557 playerInfo = [];
1558 playerList.each(function(index, id){
1559 funs.push(function(donecb){
1560 getUser({u: id, m: mode, type: "id"}, function(response){
1561 playerInfo[index] = response[0];
1562 donecb();
1563 });
1564 });
1565 });
1566 doManyFunc(funs, function(){
1567 tableLoadingNotice.hide();
1568
1569 //Add new header
1570 var newHeader = isGlobal ? "Country" : "Global";
1571 $(".ranking-page-table__heading--main").before(`<th class='ranking-page-table__heading'>${newHeader}</th>`);
1572
1573 //Add new ranks
1574 $(".ranking-page-table__row").each(function(index, row){
1575 row = $(row);
1576 var newRank = isGlobal ? playerInfo[index].pp_country_rank : playerInfo[index].pp_rank;
1577 row.find(".ranking-page-table__column--rank").after(
1578 `<td class='rank2col'>#${newRank}</td>`
1579 );
1580 });
1581 });
1582 }
1583 }
1584
1585 function searchParser(str){
1586 if(str[0] === "?") str = str.slice(1);
1587 var arr = str.split("&");
1588 var rtn = {};
1589 arr.forEach(function(x){
1590 var xsplit = x.split("=");
1591 if(xsplit.length > 1){
1592 rtn[xsplit[0]] = xsplit[1];
1593 }
1594 })
1595 return rtn;
1596 }
1597
1598 function destroy(){
1599 $(".osuplus-new-ppranking-style").remove();
1600 }
1601
1602 return {init: init, destroy: destroy}
1603 }
1604
1605 function osuplusUserpage(){
1606 var userId = null,
1607 username = null,
1608 gameMode = null,
1609 userInfo = [],
1610 mainTableBody = null,
1611 profileTabs = null,
1612 generalNode = null,
1613 userRecent = null,
1614 beatmapsCache = null,
1615 observer = null,
1616 generalObserver = null,
1617 topObserver = null,
1618 topSlider = null,
1619 topSliderLbl = null,
1620 topSliderCB = null,
1621 loadingTop = false,
1622 subscribeBtn = null,
1623 subscribed = false,
1624 subscribedColour = "#ef77af",
1625 unsubscribedColour = "#5db8ef",
1626 opModalContent = null;
1627
1628 function addCss(){
1629 $(document.head).append($("<style></style>").html(
1630 `.pc-display {text-align: right; font-size: 175%; color: #9492dc; text-shadow: #b5c6cb 2px 0px 3px; padding-right: 5px;}
1631 .pc-img {width: 80px;}
1632 .pc-imgcol {width: 90px;}
1633 #opslider {width: 250px;}
1634 .recentscore {background-color: greenyellow;}
1635 .star-display {text-align: right; color: #444444; padding-right: 5px;}
1636 .prof-beatmap {position: relative;}
1637 .modalBtn {position: absolute; right: -1px; top: -1px; background: white; padding: 3px; width: 10px; text-align: center; border-style: solid; border-width: 1px;}
1638 .opModalCloseBtnDiv {position: absolute; right: 10px;}
1639 .opModal {width:700px;position: fixed; display: none;z-index: 10000;padding: 15px 20px 10px;-webkit-border-radius: 10px;-moz-border-radius: 10px;border-radius: 10px;background: #fff; left: 50%; top:50%; transform: translate(-50%, -50%);}
1640 .opModalOverlay {position: fixed;top: 0;left: 0;bottom:0;right:0;width: 100%;height: 100%;z-index: 9999;background: #000;display: none;-ms-filter: 'alpha(Opacity=50)';-moz-opacity: .5;-khtml-opacity: .5;opacity: .5;}
1641 .tableAttr {width: 50px;}
1642 .modal-hr {color: red;}
1643 .modal-ez {color: green;}`
1644 ));
1645 }
1646
1647 var getBeatmapsCache = (function(){
1648 var callbacks = {},
1649 beatmapsCache = {};
1650
1651 function getBeatmapsCache(params, callback){
1652 var id = params.b;
1653 if(id in beatmapsCache){
1654 callback(beatmapsCache[id]);
1655 }else if(id in callbacks){
1656 callbacks[id].push(callback);
1657 }else{
1658 callbacks[id] = [callback];
1659 getBeatmaps(params, function(response){
1660 beatmapsCache[id] = response;
1661 callbacks[id].forEach(function(cb){
1662 cb(response);
1663 });
1664 delete callbacks[id];
1665 });
1666 }
1667 }
1668
1669 return getBeatmapsCache;
1670 })();
1671
1672 function init(){
1673 addCss();
1674 userId = getUserId();
1675 username = $(".profile-username").text().trim();
1676 gameMode = getGameMode();
1677 mainTableBody = $(".beatmapListing").children();
1678 profileTabs = $(".profile-tabs").children().children();
1679 generalNode = $("#general");
1680
1681 //Listen for change in game mode
1682 observer = new MutationObserver(function(mutations){
1683 changeGameMode(getGameMode());
1684 });
1685 observer.observe($(".profileGameModes")[0], {attributes:true, subtree:true});
1686
1687 //Listen for expand top ranks
1688 topObserver = new MutationObserver(function(mutations){
1689 detailedTop();
1690 });
1691 topObserver.observe($("#leader")[0], {attributes:true, subtree:true});
1692
1693 doGeneral();
1694 addMostPlayed();
1695 addRecent();
1696
1697 //Subscribe button
1698 subscriberManager.mapper.isSubscribed(userId).then((result) => {
1699 subscribed = result;
1700 subscribeBtn = $("<a class='btn'>");
1701 updateSubscribeBtn(subscribed);
1702 $(".playstyle-container").after($("<div class='centrep'>").append(subscribeBtn));
1703
1704 subscribeBtn.click(function(){
1705 var p;
1706 if(subscribed){
1707 p = subscriberManager.mapper.remove(userId).then(() => {
1708 subscribed = false;
1709 return subscribed;
1710 });
1711 }else{
1712 p = subscriberManager.mapper.add(userId, username).then(() => {
1713 subscribed = true;
1714 return subscribed;
1715 });
1716 }
1717 p.then(updateSubscribeBtn);
1718 });
1719 });
1720
1721 // Add modal
1722 $("body").append('<div class="opModalOverlay opModalOverride" id="opModalOverlay" style="display:none;"></div>');
1723 $("body").append('<div class="opModal" id="opModal" style="display:none;"></div>');
1724 opModalContent = $("<div id='opModalContent'>");
1725 $("#opModal").append(
1726 '<div class="opModalCloseBtnDiv" style="display: block;"><button class="opModalCloseBtn">x</button></div>',
1727 opModalContent
1728 );
1729 $("#opModalOverlay, .opModalCloseBtn").click(function(){
1730 closeModal();
1731 });
1732
1733 // :)
1734 if(userId === "1843447"){
1735 $(".profile-username").parent().after("<div><b>osuplus creator</b></div>");
1736 }
1737 }
1738
1739 function updateSubscribeBtn(subscribed){
1740 if(subscribed){
1741 subscribeBtn.empty();
1742 subscribeBtn.css("background", subscribedColour);
1743 subscribeBtn.append("<i class='icon-heart'></i> Subscribed</a>");
1744 }else{
1745 subscribeBtn.empty();
1746 subscribeBtn.css("background", unsubscribedColour);
1747 subscribeBtn.append("<i class='icon-plus-sign'></i> Subscribe mapper</a>");
1748 }
1749 }
1750
1751 function getGameMode(){
1752 return parseInt($(".profileGameModeButton.active").attr("id")[3]);
1753 }
1754
1755 function getUserId(){
1756 var line = $("script").text().match(/var userId = \d*;/)[0];
1757 return line.slice(13, line.length-1);
1758 }
1759
1760 function changeGameMode(gm){
1761 if(gameMode !== gm){
1762 //Clear recent
1763 $("#recent").removeClass("loaded").empty();
1764 gameMode = gm;
1765 }
1766 }
1767
1768 function detailedTop(){
1769 //Put slider
1770 if($("#leader").find("#opslider").length === 0){
1771 var sliderDiv = createSlider(function(val, checked){
1772 if(checked){
1773 var tops = $(".prof-beatmap");
1774 var curTime = new Date();
1775 tops.each(function(index, top){
1776 top = $(top);
1777 if(!top.attr("id")) return;
1778 var scoreTime = new Date(top.find("time").attr("datetime"));
1779 var diff = (curTime - scoreTime) / (1000*60*60*24);
1780 if(diff < val){
1781 top.find("table").addClass("recentscore");
1782 }else{
1783 top.find("table").removeClass("recentscore");
1784 }
1785 });
1786 }else{
1787 $(".prof-beatmap > table").removeClass("recentscore");
1788 }
1789 });
1790 $("#leader").find("h2").first().after(sliderDiv);
1791 }
1792
1793 //Update tops
1794 var topsBest = $("#leader h2").last().prevAll().find(".prof-beatmap").addBack(".prof-beatmap").filter(":not(.oploading,.oploaded)");
1795 var topsFirst = $("#leader h2").last().nextAll().find(".prof-beatmap").addBack(".prof-beatmap").filter(":not(.oploading,.oploaded)");
1796 topsBest.each(function(index, top){
1797 $(top).addClass("oploading");
1798 });
1799 topsFirst.each(function(index, top){
1800 $(top).addClass("oploading");
1801 });
1802 if(topsBest.length > 0){
1803 getUserBest({u: userId, m: gameMode, type: "id", limit: 100}, function(scores){
1804 topsBest.each(function(index, top){
1805 top = $(top);
1806 var mapId = top.attr("id");
1807 if(!mapId) return;
1808 mapId = mapId.split("-")[1];
1809 var score = null;
1810 for(var i in scores){
1811 if(scores[i].beatmap_id == mapId){
1812 score = scores[i];
1813 break;
1814 }
1815 }
1816 if(score !== null){
1817 addModalBtn(top, mapId);
1818 if(settings.fetchUserpageMaxCombo){
1819 getBeatmapsCache({b: mapId, m: gameMode, a: 1}, function(beatmap){
1820 if(beatmap.length < 1) return;
1821 else{
1822 detailify(top, score, beatmap[0]);
1823 }
1824 });
1825 }else{
1826 detailify(top, score);
1827 }
1828 }
1829 });
1830 });
1831 }
1832 topsFirst.each(function(index, top){
1833 top = $(top);
1834 var mapId = top.attr("id");
1835 if(!mapId) return;
1836 mapId = mapId.split("-")[1];
1837 addModalBtn(top, mapId);
1838 if(settings.fetchFirstsInfo){
1839 getScores({b: mapId, u: userId, type: "id", m: gameMode, a: 1, limit: 1}, function(scores){
1840 if(scores.length < 1) return;
1841 if(settings.fetchUserpageMaxCombo){
1842 getBeatmapsCache({b: mapId, m: gameMode, a: 1}, function(beatmap){
1843 if(beatmap.length < 1) return;
1844 else detailify($(top), scores[0], beatmap[0]);
1845 });
1846 }else{
1847 scores[0].beatmap_id = mapId;
1848 detailify($(top), scores[0]);
1849 }
1850 });
1851 }
1852 });
1853 }
1854
1855 function detailify(top, score, beatmap){
1856 var maxmapcombo = $("<span></span>").css("color", "#b7b1e5");
1857 if(beatmap && beatmap.max_combo !== null){
1858 maxmapcombo.text("(" + beatmap.max_combo + "x)");
1859 }
1860 var h = top.find(".h");
1861 if(score.perfect === "1"){
1862 h.append("(FC)");
1863 }
1864 h.append(makeScoreStats(score, maxmapcombo));
1865 }
1866
1867 function makeScoreStats(score, maxmapcombo){
1868 return $("<div>").append(
1869 $("<b>").append(
1870 `${commarise(score.score)} / ${score.maxcombo}x`,
1871 maxmapcombo
1872 ),
1873 gameMode <= 1 ? // Standard/Taiko
1874 ` { ${score.count300} / ${score.count100} / ${score.count50} / ${score.countmiss} }` :
1875 gameMode == 2 ? // CTB
1876 ` { ${score.count300} / ${score.count100} / ${score.count50} / ${score.countkatu} / ${score.countmiss} }` :
1877 // Mania
1878 ` { ${score.countgeki} / ${score.count300} / ${score.countkatu} / ${score.count100} / ${score.count50} / ${score.countmiss} }`
1879 );
1880 }
1881
1882 function addModalBtn(top, beatmap_id){
1883 var modalBtn = $("<a class='modalBtn'>?</a>").hide();
1884 modalBtn.click(function(){
1885 openModal($(this).parent().find(".identifier").val());
1886 });
1887 top.append(modalBtn, $("<input type=hidden class='identifier' value='"+beatmap_id+"'>"));
1888 top.removeClass("oploading").addClass("oploaded");
1889 top.hover(function(){
1890 $(this).find(".modalBtn").show();
1891 }, function(){
1892 $(this).find(".modalBtn").hide();
1893 });
1894 }
1895
1896 function openModal(id){
1897 if(id === undefined) return;
1898 var opModalContent = $("#opModalContent");
1899 opModalContent.empty();
1900 getBeatmapsCache({b: id, m: gameMode, a: 1}, function(beatmap){
1901 beatmap = beatmap[0];
1902 opModalContent.append(
1903 `<h1>${beatmap.artist} - <a href=/s/${beatmap.beatmapset_id}>${beatmap.title}</a> [<a href=/b/${beatmap.beatmap_id}>${beatmap.version}</a>]</h1>
1904 <input type="radio" name="mods" value="none" checked="">Nomod
1905 <input type="radio" name="mods" value="hr">HR
1906 <input type="radio" name="mods" value="ez">EZ
1907 <table id='songinfo' style='width:100%;'>
1908 <tr>
1909 <td rowspan=5 style='width:1%'><a onclick='playBeatmapPreview(${beatmap.beatmapset_id})'><img class='bmt' src=//b.ppy.sh/thumb/${beatmap.beatmapset_id}l.jpg></a></td>
1910 <td class='tableAttr'>CS:</td><td>${beatmap.diff_size} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_size)*1.3, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_size)/2).toFixed(2)})</span></td>
1911 <td class='tableAttr'>AR:</td><td>${beatmap.diff_approach} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_approach)*1.4, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_approach)/2).toFixed(2)})</span></td>
1912 </tr>
1913 <tr>
1914 <td class='tableAttr'>HP:</td><td>${beatmap.diff_drain} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_drain)*1.4, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_drain)/2).toFixed(2)})</span></td>
1915 <td class='tableAttr'>Stars:</td><td>${beatmap.difficultyrating}</td>
1916 </tr>
1917 <tr>
1918 <td class='tableAttr'>OD:</td><td>${beatmap.diff_overall} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_overall)*1.4, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_overall)/2).toFixed(2)})</span></td>
1919 <td class='tableAttr'>Length:</td>
1920 <td>${secsToMins(parseInt(beatmap.total_length))} (${secsToMins(parseInt(beatmap.hit_length))} drain)${beatmap.max_combo === null ? "" : `<br>${beatmap.max_combo}x combo`}</td>
1921 </tr>
1922 <tr>
1923 <td class='tableAttr'>Creator:</td><td>${beatmap.creator}</td>
1924 <td class='tableAttr'>BPM:</td><td>${beatmap.bpm}</td>
1925 </tr>
1926 <tr>
1927 <td colspan=4><a href=/d/${beatmap.beatmapset_id}>Download</a><br><a href='http://bloodcat.com/osu/s/${beatmap.beatmapset_id}'>Bloodcat mirror</a></td>
1928 </tr>
1929 </table>`
1930 );
1931 opModalContent.find("input[type=radio][name=mods]").change(function(){
1932 if(this.value == "none"){
1933 $(".modal-hr").hide();
1934 $(".modal-ez").hide();
1935 }else if(this.value == "hr"){
1936 $(".modal-hr").css("display", "inline");
1937 $(".modal-ez").hide();
1938 }else{
1939 $(".modal-hr").hide();
1940 $(".modal-ez").css("display", "inline");
1941 }
1942 });
1943 });
1944 $("#opModalOverlay").fadeIn(200);
1945 $("#opModal").fadeIn(200);
1946 }
1947
1948 function closeModal(){
1949 $("#opModalOverlay").fadeOut(200);
1950 $("#opModal").fadeOut(200);
1951 }
1952
1953 function doGeneral(){
1954 addGeneral();
1955 generalObserver = new MutationObserver(function(mutations){
1956 addGeneral();
1957 });
1958 generalObserver.observe(generalNode[0], {childList:true});
1959 }
1960
1961 function addGeneral(){
1962 function useUserInfo(user){
1963 if(settings.showDetailedHitCount){
1964 var c300 = parseInt(user.count300), c100 = parseInt(user.count100), c50 = parseInt(user.count50),
1965 ctotal = c300 + c100 + c50;
1966 generalNode.find(".profileStatLine").eq(7).append(
1967 $("<span></span>").text(
1968 " (" + commarise(c300) + " x300 / " + commarise(c100) + " x100 / " + commarise(c50) + " x50) (" +
1969 (100*c300/ctotal).toFixed(2) + "% / " + (100*c100/ctotal).toFixed(2) + "% / " + (100*c50/ctotal).toFixed(2) + "%)"
1970 )
1971 );
1972 }
1973
1974 if(settings.showHitsPerPlay){
1975 generalNode.find(".profileStatLine").eq(7).after(
1976 "<br>",
1977 $("<div class='profileStatLine'></div>").append(
1978 "<b>Hits/play</b>: " + (ctotal/parseInt(user.playcount)).toFixed(2)
1979 )
1980 );
1981 }
1982 }
1983
1984 if(generalNode.find("#general-check").length === 0 && $(".profileStatLine").length > 0){
1985 generalNode.append($("<div id='general-check'></div>").hide());
1986
1987 if(userInfo[gameMode]){
1988 useUserInfo(userInfo[gameMode]);
1989 }else{
1990 getUser({u: userId, m: gameMode, type: "id"}, function(response){
1991 userInfo[gameMode] = response[0]
1992 useUserInfo(userInfo[gameMode]);
1993 });
1994 }
1995 }
1996 }
1997
1998 function addMostPlayed(){
1999 mainTableBody.append($("<tr>").click(function(){ expandProfile("mostplayed", addMostPlayedContent); })
2000 .append($("<td class='sectionHeading' id='_mostplayed'>Most Played</td>")),
2001 $("<tr><td class='sectionContents'><div id='mostplayed'></div></td></tr>")
2002 );
2003 profileTabs.append($("<td>Most Played</td>").click(function(){
2004 expandProfile("mostplayed", addMostPlayedContent, true);
2005 }));
2006 }
2007
2008 function addRecent(){
2009 mainTableBody.append($("<tr>").click(function(){ expandProfile("recent", addRecentContent); })
2010 .append($("<td class='sectionHeading' id='_recent'>Recent</td>")),
2011 $("<tr><td class='sectionContents'><div id='recent'></div></td></tr>")
2012 );
2013 profileTabs.append($("<td>Recent</td>").click(function(){
2014 expandProfile("recent", addRecentContent, true);
2015 }));
2016 }
2017
2018 function expandProfile(id, addContent, forceExpand){
2019 var content = $("#" + id);
2020 if(!content.hasClass("expanded") || forceExpand){
2021 content.addClass("expanded");
2022 content.slideDown(500);
2023 if(forceExpand){
2024 //window.location.hash = "#_" + id;
2025 }
2026 if(!content.hasClass("loaded")){
2027 addContent(content);
2028 content.addClass("loaded");
2029 }
2030 }else{
2031 content.slideUp(500);
2032 content.removeClass("expanded");
2033 }
2034 }
2035
2036 function addMostPlayedContent(container){
2037 function makeItem(beatmapInfo){
2038 var maplink = $("<a href='/b/" + beatmapInfo.beatmap.id + "'></a>"),
2039 mapperlink = $("<a href='/u/" + beatmapInfo.beatmapset.user_id + "'>" + beatmapInfo.beatmapset.creator + "</a>");
2040 maplink.append(
2041 beatmapInfo.beatmapset.artist + " - ",
2042 beatmapInfo.beatmapset.title,
2043 " [" + beatmapInfo.beatmap.version + "]"
2044 );
2045
2046 return $("<div class='prof-beatmap'></div>").append(
2047 $("<table></table>").append($("<tr></tr>").append(
2048 $("<td class='pc-imgcol'></td>").append(
2049 $("<img class='pc-img' src='" + beatmapInfo.beatmapset.covers.list + "'></img>")
2050 ),
2051 $("<td></td>").append(
2052 $("<div></div>").append(
2053 $("<b></b>").append(
2054 maplink
2055 )
2056 ),
2057 $("<div></div>").append(
2058 "mapped by ",
2059 mapperlink
2060 )
2061 ),
2062 $("<td></td>").append(
2063 $("<div class='pc-display'><b>" + beatmapInfo.count + " plays</b></div>"),
2064 $("<div class='star-display'>" + beatmapInfo.beatmap.difficulty_rating.toFixed(2) + "★</div>")
2065 )
2066 ))
2067 );
2068 }
2069
2070 gameMode = getGameMode();
2071 retrieveMostPlayed(0, 50, function(beatmaps){
2072 container.append("<h2>Most Played</h2>");
2073
2074 //Put first 50
2075 beatmaps.forEach(function(beatmapInfo){
2076 container.append(makeItem(beatmapInfo));
2077 });
2078
2079 //Show more next 50
2080 if(beatmaps.length >= 50){
2081 container.append(
2082 $("<div id='more-most'></div>").append(
2083 $("<div class='prof-beatmap'></div>").append(
2084 $("<a></a>").append(
2085 "<b>Show me more!</b>"
2086 ).click(function(){
2087 $("#more-most").empty();
2088 retrieveMostPlayed(50, 100, function(beatmaps2){
2089 beatmaps2.forEach(function(beatmapInfo2){
2090 container.append(makeItem(beatmapInfo2));
2091 });
2092 });
2093 })
2094 )
2095 )
2096 );
2097 }
2098 });
2099 }
2100
2101 function retrieveMostPlayed(offset, number, callback){
2102 var numgrps = (number/5|0); //integer divide
2103 var funs = [];
2104 var mostPlayedRaw = [];
2105 for(var i=0; i<numgrps; i++){
2106 funs.push(function(suboffset){
2107 return function(donecb){
2108 GetPage("https://osu.ppy.sh/users/" + userId + "/beatmapsets/most_played?offset="+suboffset, function(response){
2109 mostPlayedRaw.push({
2110 offset: suboffset,
2111 response: JSON.parse(response)
2112 });
2113 donecb();
2114 });
2115 };
2116 }(offset + 5*i));
2117 }
2118 doManyFunc(funs, function(){
2119 mostPlayedRaw.sort(function(a,b){
2120 return a.offset - b.offset;
2121 });
2122 var mostPlayed = [];
2123 mostPlayedRaw.forEach(function(responseSet){
2124 responseSet.response.forEach(function(beatmap){
2125 mostPlayed.push(beatmap);
2126 });
2127 });
2128 callback(mostPlayed);
2129 });
2130 }
2131
2132 function addRecentContent(container){
2133 gameMode = getGameMode();
2134 getUserRecent({u: userId, m: gameMode, limit: 50, type: "id"}, function(response){
2135 userRecent = response;
2136 var failedCheckbox = $("<input type=checkbox id='failedCheckbox'/>"),
2137 failedChecked = settings.failedChecked;
2138
2139 failedCheckbox.prop("checked", failedChecked).click(function(){
2140 var me = $(this);
2141 failedChecked = me.prop("checked");
2142 if(failedChecked){
2143 $(".failedScore").show();
2144 }else{
2145 $(".failedScore").hide();
2146 }
2147 });
2148
2149 container.append(
2150 "<h2>Recent Plays</h2>",
2151 $("<label></label>").append(
2152 failedCheckbox, "Show failed scores"
2153 )
2154 );
2155
2156 userRecent.forEach(function(play){
2157 var modstr = getMods(play.enabled_mods),
2158 acc = calcAcc(play, gameMode),
2159 dateset = new Date(play.date.replace(' ','T') + "+0000"), // dates from API in GMT+0
2160 maplink = $("<a href='/b/" + play.beatmap_id + "?m=" + gameMode + "'></a>").text("Loading..."),
2161 maxmapcombo = $("<span></span>").css("color", "#b7b1e5"),
2162 //starrating = $("<b>...★</b>"),
2163 failClass = play.rank === "F" ? "failedScore" : "passScore";
2164
2165 var profbeatmap = $("<div class='prof-beatmap'>").addClass(failClass).append(
2166 $("<table>").append($("<tr>").append(
2167 $("<td>").append(
2168 $("<div class='h'>").append(
2169 getRankImg(play.rank), "\n",
2170 $("<b>").append(
2171 maplink,
2172 modstr === "None" ? " " : " +" + modstr
2173 ),
2174 " (" + acc.toFixed(2) + "%)" + (play.perfect === '1' ? " (FC)" : "") + "\n"
2175 ),
2176 makeScoreStats(play, maxmapcombo),
2177 $("<div class='c'>").append(
2178 $("<time class='timeago'>").attr("datetime", dateset.toISOString())
2179 .text(dateset.toLocaleString())
2180 )
2181 )
2182 ))
2183 );
2184 addModalBtn(profbeatmap, play.beatmap_id);
2185
2186 container.append(profbeatmap);
2187
2188 getBeatmapsCache({b: play.beatmap_id, m: gameMode, a: 1}, function(response){
2189 var r = response[0];
2190 maplink.text(r.artist + " - " + r.title + " [" + r.version + "]");
2191 if(r.max_combo !== null){
2192 maxmapcombo.text("(" + r.max_combo + "x)");
2193 }
2194 //starrating.html(parseFloat(r.difficultyrating).toFixed(2) + "★");
2195 profbeatmap.find(".identifier").val(r.beatmap_id);
2196 });
2197 });
2198 $(".timeago").timeago();
2199 if(failedChecked){
2200 $(".failedScore").show();
2201 }else{
2202 $(".failedScore").hide();
2203 }
2204 });
2205 }
2206
2207 return {init: init};
2208 }
2209
2210 function osuplusNewUserpage(){
2211 var jsonUser = null,
2212 gameMode = null,
2213 userBest = null,
2214 subscribeBtn = null,
2215 subscribed = false,
2216 subscribedColour = "#ef77af",
2217 unsubscribedColour = "#5db8ef",
2218 opModalContent = null;
2219
2220 function addCss(){
2221 if(!$(".osuplus-new-userpage-style").length){
2222 $(document.head).append($("<style class='osuplus-new-userpage-style'></style>").html(
2223 `.pc-display {text-align: right; font-size: 175%; color: #9492dc; text-shadow: #b5c6cb 2px 0px 3px; padding-right: 5px;}
2224 .pc-img {width: 80px;}
2225 .pc-imgcol {width: 90px;}
2226 #opslider {width: 250px; display: inline-block; margin: 10px;}
2227 .recentscore .play-detail__group--top, .recentscore .play-detail__score-detail, .recentscore .play-detail__pp:before {background-color: #009612;}
2228 .recentscore {background-color: green;}
2229 .star-display {text-align: right; color: #444444; padding-right: 5px;}
2230 .prof-beatmap {position: relative;}
2231 .modalBtn {position: absolute; right: -1px; top: -1px; background: white; padding: 3px; width: 22px; text-align: center; border-style: solid; border-width: 1px;}
2232 .modalBtn:hover {cursor: pointer;}
2233 .opModalCloseBtnDiv {position: absolute; right: 15px; top: 15px;}
2234 .opModal {width: 800px; position: fixed; display: none;z-index: 10000;padding: 15px 20px 10px;-webkit-border-radius: 10px;-moz-border-radius: 10px;border-radius: 10px;background: #fff; left: 50%; top:50%; transform: translate(-50%, -50%);}
2235 .opModalOverlay {position: fixed;top: 0;left: 0;bottom:0;right:0;width: 100%;height: 100%;z-index: 9999;background: #000;display: none;-ms-filter: 'alpha(Opacity=50)';-moz-opacity: .5;-khtml-opacity: .5;opacity: .5;}
2236 .opModal-song-info {}
2237 .opModal-song-info td {padding: 2px 7px;}
2238 .tableAttr {width: 70px;}
2239 .sub-button {width: 90px; margin-left: 30px;}
2240 .score-rank--F {background-image: url('${FImgNew}')}
2241 .play-detail__pp.play-detail__recent-pp {min-width: 0px; padding: 0px;}
2242 .div-24h {margin-top: 50px;}
2243 .modal-hr {color: red;}
2244 .modal-ez {color: green;}`
2245 ));
2246 }
2247 }
2248
2249 var getBeatmapsCache = (function(){
2250 var callbacks = {},
2251 beatmapsCache = {};
2252
2253 function getBeatmapsCache(params, callback){
2254 var id = params.b;
2255 if(id in beatmapsCache){
2256 callback(beatmapsCache[id]);
2257 }else if(id in callbacks){
2258 callbacks[id].push(callback);
2259 }else{
2260 callbacks[id] = [callback];
2261 getBeatmaps(params, function(response){
2262 beatmapsCache[id] = response;
2263 callbacks[id].forEach(function(cb){
2264 cb(response);
2265 });
2266 delete callbacks[id];
2267 });
2268 }
2269 }
2270
2271 return getBeatmapsCache;
2272 })();
2273
2274 function init(){
2275 if($("#osuplusloaded").length) return;
2276 $("body").append("<a hidden id='osuplusloaded'></a>");
2277 addCss();
2278 jsonUser = JSON.parse($("#json-user").text());
2279 gameMode = getGameMode();
2280
2281 addDetailedTop();
2282 addGeneral();
2283 addRecent();
2284
2285 // Add modal
2286 $("body").append('<div class="opModalOverlay opModalOverride" id="opModalOverlay" style="display:none;"></div>');
2287 $("body").append('<div class="opModal" id="opModal" style="display:none;"></div>');
2288 opModalContent = $("<div id='opModalContent'>");
2289 $("#opModal").append(
2290 opModalContent,
2291 '<div class="opModalCloseBtnDiv" style="display: block;"><button class="opModalCloseBtn">x</button></div>');
2292 $("#opModalOverlay, .opModalCloseBtn").click(function(){
2293 closeModal();
2294 });
2295
2296 //Subscribe button
2297 subscriberManager.mapper.isSubscribed(jsonUser.id.toString()).then((result) => {
2298 subscribed = result;
2299 subscribeBtn = $("<button class='user-action-button sub-button' title='subscribe mapper'>subscribe</button>");
2300 updateSubscribeBtn(subscribed);
2301 $(".profile-header-extra--follower-meta").append(subscribeBtn);
2302
2303 subscribeBtn.click(function(){
2304 var p;
2305 if(subscribed){
2306 p = subscriberManager.mapper.remove(jsonUser.id.toString()).then(() => {
2307 subscribed = false;
2308 return subscribed;
2309 });
2310 }else{
2311 p = subscriberManager.mapper.add(jsonUser.id.toString(), jsonUser.username).then(() => {
2312 subscribed = true;
2313 return subscribed;
2314 });
2315 }
2316 p.then(updateSubscribeBtn);
2317 });
2318 });
2319
2320 // :)
2321 if(jsonUser.id == 1843447){
2322 $(".profile-info__title").text("osuplus creator");
2323 }
2324 }
2325
2326 function destroy(){
2327 $(".osuplus-new-userpage-style").remove();
2328 }
2329
2330 function updateSubscribeBtn(subscribed){
2331 if(subscribed){
2332 subscribeBtn.text("Subscribed");
2333 subscribeBtn.css("background", subscribedColour);
2334 subscribeBtn.attr("title", "unsubscribe mapper")
2335 }else{
2336 subscribeBtn.text("Subscribe");
2337 subscribeBtn.css("background", unsubscribedColour);
2338 subscribeBtn.attr("title", "subscribe mapper")
2339 }
2340 }
2341
2342 function getGameMode(){
2343 var modeStr = $(".game-mode-link--active").attr("href").split("/");
2344 modeStr = modeStr[modeStr.length-1];
2345 return modeToInt(modeStr);
2346 }
2347
2348 function addDetailedTop(){
2349 var bestObserver = new MutationObserver(function(mutationList){
2350 for(var mutation of mutationList){
2351 if(mutation.addedNodes.length){
2352 addBestDetails(mutation.addedNodes[0]);
2353 }
2354 }
2355 });
2356 var firstObserver = new MutationObserver(function(mutationList){
2357 for(var mutation of mutationList){
2358 if(mutation.addedNodes.length){
2359 addFirstDetails(mutation.addedNodes[0]);
2360 }
2361 }
2362 });
2363 $("div[data-page-id=top_ranks] .profile-extra-entries").eq(1).find(".play-detail").each(function(i, ele){
2364 addFirstDetails(ele);
2365 });
2366 if($("div[data-page-id=top_ranks] .profile-extra-entries .play-detail-list")[1]){
2367 firstObserver.observe($("div[data-page-id=top_ranks] .profile-extra-entries .play-detail-list")[1], {childList: true});
2368 }
2369
2370 getUserBest({u: jsonUser.id, m: gameMode, type: "id", limit: 100}, function(scores){
2371 userBest = scores;
2372 $("div[data-page-id=top_ranks] .profile-extra-entries").eq(0).find(".play-detail").each(function(i, ele){
2373 addBestDetails(ele);
2374 });
2375 if($("div[data-page-id=top_ranks] .profile-extra-entries .play-detail-list")[0]){
2376 bestObserver.observe($("div[data-page-id=top_ranks] .profile-extra-entries .play-detail-list")[0], {childList: true});
2377 }
2378 });
2379
2380 // Add slider
2381 var sliderDiv = createSlider(function(val, checked){
2382 if(checked){
2383 var tops = $("div[data-page-id=top_ranks] .play-detail");
2384 var curTime = new Date();
2385 tops.each(function(index, top){
2386 top = $(top);
2387 var scoreTime = new Date(top.find("time").attr("datetime"));
2388 var diff = (curTime - scoreTime) / (1000*60*60*24);
2389 if(diff < val){
2390 top.addClass("recentscore");
2391 }else{
2392 top.removeClass("recentscore");
2393 }
2394 });
2395 }else{
2396 $("div[data-page-id=top_ranks] .play-detail.recentscore").removeClass("recentscore");
2397 }
2398 });
2399 $("div[data-page-id=top_ranks] .title.title--page-extra-small").first().after(sliderDiv);
2400 }
2401
2402 function addBestDetails(ele){
2403 var beatmapId = beatmapIdOfDetailRow(ele);
2404 var score = null;
2405 for(var uscore of userBest){
2406 if(uscore.beatmap_id == beatmapId){
2407 score = uscore;
2408 break;
2409 }
2410 }
2411 if(score !== null){
2412 addDetails($(ele), score, beatmapId);
2413 }
2414 }
2415
2416 function addFirstDetails(ele){
2417 if(settings.fetchFirstsInfo){
2418 var beatmapId = beatmapIdOfDetailRow(ele);
2419 getScores({b: beatmapId, u: jsonUser.id, type: "id", m: gameMode, a: 1, limit: 1}, function(scores){
2420 if(scores.length){
2421 addDetails($(ele), scores[0], beatmapId);
2422 }
2423 });
2424 }
2425 }
2426
2427 function beatmapIdOfDetailRow(ele){
2428 var href = $(ele).find(".play-detail__title").attr("href");
2429 var temp = href.split("/");
2430 return temp[temp.length - 1];
2431 }
2432
2433 function addDetails(top, score, mapId){
2434 addModalBtn(top, mapId);
2435 if(settings.fetchUserpageMaxCombo){
2436 getBeatmapsCache({b: mapId, m: gameMode, a: 1}, function(beatmap){
2437 if(beatmap.length){
2438 detailify(top, score, beatmap[0]);
2439 }
2440 });
2441 }else{
2442 detailify(top, score);
2443 }
2444 }
2445
2446 function detailify(top, score, beatmap){
2447 var maxmapcombo = $("<span></span>").css("color", "#b7b1e5");
2448 if(beatmap && beatmap.max_combo !== null){
2449 maxmapcombo.text(` (${beatmap.max_combo}x)`);
2450 }
2451 if(score.perfect === "1"){
2452 if(top.find(".play-detail__pp-weight").length){
2453 top.find(".play-detail__pp-weight").prepend("<span class='play-detail__fc'>(FC) </span>")
2454 }else{
2455 top.find(".play-detail__accuracy-and-weighted-pp").after("<div><span class='play-detail__fc'>(FC)</span></div>");
2456 }
2457 }
2458 top.find(".play-detail__title").after(makeScoreStats(score, maxmapcombo));
2459 }
2460
2461 function makeScoreStats(score, maxmapcombo){
2462 return $("<div class='play-detail__opstats'></div>").append(
2463 $("<b></b>").append(
2464 `${commarise(score.score)} / ${score.maxcombo}x`,
2465 maxmapcombo
2466 ),
2467 gameMode <= 1 ? // Standard/Taiko
2468 ` { ${score.count300} / ${score.count100} / ${score.count50} / ${score.countmiss} }` :
2469 gameMode == 2 ? // CTB
2470 ` { ${score.count300} / ${score.count100} / ${score.count50} / ${score.countkatu} / ${score.countmiss} }` :
2471 // Mania
2472 ` { ${score.countgeki} / ${score.count300} / ${score.countkatu} / ${score.count100} / ${score.count50} / ${score.countmiss} }`
2473 )
2474 }
2475
2476 function addModalBtn(top, beatmap_id){
2477 var modalBtn = $("<a class='modalBtn'>?</a>").hide();
2478 modalBtn.click(function(){
2479 openModal($(this).parent().find("input:hidden").val());
2480 });
2481 top.append(modalBtn, $(`<input type=hidden class='identifier' value='${beatmap_id}'>`));
2482
2483 top.hover(function(){
2484 $(this).find(".modalBtn").show();
2485 }, function(){
2486 $(this).find(".modalBtn").hide();
2487 });
2488 modalBtn.click(function(){
2489 openModal($(this).parent().find(".identifier").val());
2490 });
2491 }
2492
2493 function openModal(id){
2494 if(id === undefined) return;
2495 var opModalContent = $("#opModalContent");
2496 opModalContent.empty();
2497 getBeatmapsCache({b: id, m: gameMode, a: 1}, function(beatmap){
2498 beatmap = beatmap[0];
2499 opModalContent.html(
2500 `<h1>${beatmap.artist} - <a href=/beatmapsets/${beatmap.beatmapset_id}>${beatmap.title}</a> [<a href=/beatmaps/${beatmap.beatmap_id}>${beatmap.version}</a>]</h1>
2501 <input type="radio" name="mods" value="none" checked="">Nomod
2502 <input type="radio" name="mods" value="hr">HR
2503 <input type="radio" name="mods" value="ez">EZ
2504 <table class='opModal-song-info' style='width:100%;'>
2505 <tr>
2506 <td rowspan=5 style='width:1%'>
2507 <a>
2508 <img class='bmt' src=//b.ppy.sh/thumb/${beatmap.beatmapset_id}l.jpg>
2509 </a>
2510 </td>
2511 <td class='tableAttr'>CS:</td><td>${beatmap.diff_size} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_size)*1.3, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_size)/2).toFixed(2)})</span></td>
2512 <td class='tableAttr'>AR:</td><td>${beatmap.diff_approach} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_approach)*1.4, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_approach)/2).toFixed(2)})</span></td>
2513 </tr>
2514 <tr>
2515 <td class='tableAttr'>HP:</td><td>${beatmap.diff_drain} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_drain)*1.4, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_drain)/2).toFixed(2)})</span></td>
2516 <td class='tableAttr'>Stars:</td><td>${beatmap.difficultyrating}</td>
2517 </tr>
2518 <tr>
2519 <td class='tableAttr'>OD:</td><td>${beatmap.diff_overall} <span class="modal-hr" hidden>(${Math.min(parseFloat(beatmap.diff_overall)*1.4, 10).toFixed(2)})</span><span class="modal-ez" hidden>(${(parseFloat(beatmap.diff_overall)/2).toFixed(2)})</span></td>
2520 <td class='tableAttr'>Length:</td>
2521 <td>${secsToMins(parseInt(beatmap.total_length))} (${secsToMins(parseInt(beatmap.hit_length))} drain)${(beatmap.max_combo == null ? "" : `<br>${beatmap.max_combo}x combo`)}</td>
2522 </tr>
2523 <tr>
2524 <td class='tableAttr'>Creator:</td><td>${beatmap.creator}</td>
2525 <td class='tableAttr'>BPM:</td><td>${beatmap.bpm}</td>
2526 </tr>
2527 <tr>
2528 <td colspan=4><a href=/beatmapsets/${beatmap.beatmapset_id}/download>Download</a><br><a href='http://bloodcat.com/osu/s/${beatmap.beatmapset_id}'>Bloodcat mirror</a></td>
2529 </tr>
2530 </table>`
2531 );
2532 opModalContent.find("input[type=radio][name=mods]").change(function(){
2533 if(this.value == "none"){
2534 $(".modal-hr").hide();
2535 $(".modal-ez").hide();
2536 }else if(this.value == "hr"){
2537 $(".modal-hr").css("display", "inline");
2538 $(".modal-ez").hide();
2539 }else{
2540 $(".modal-hr").hide();
2541 $(".modal-ez").css("display", "inline");
2542 }
2543 });
2544 });
2545 $("#opModalOverlay").fadeIn(200);
2546 $("#opModal").fadeIn(200);
2547 }
2548
2549 function closeModal(){
2550 $("#opModalOverlay").fadeOut(200);
2551 $("#opModal").fadeOut(200);
2552 }
2553
2554 function addGeneral(){
2555 if(settings.showDetailedHitCount || settings.showHitsPerPlay){
2556 getUser({u: jsonUser.id, m: gameMode, type: "id"}, function(response){
2557 var user = response[0];
2558 var c300 = parseInt(user.count300), c100 = parseInt(user.count100), c50 = parseInt(user.count50),
2559 ctotal = c300 + c100 + c50;
2560
2561 if(settings.showHitsPerPlay){
2562 $(".profile-stats__entry").eq(4).after(
2563 `<dl class="profile-stats__entry"><dt class="profile-stats__key">Hits per Play</dt>
2564 <dd class="profile-stats__value">${(ctotal/parseInt(user.playcount)).toFixed(2)}</dd></dl>`
2565 );
2566 }
2567 if(settings.showDetailedHitCount){
2568 $(".profile-stats__entry").eq(4).after(
2569 `<dl class="profile-stats__entry"><dt class="profile-stats__key">300x</dt>
2570 <dd class="profile-stats__value">${commarise(c300)} (${(100*c300/ctotal).toFixed(2)}%)</dd></dl>
2571 <dl class="profile-stats__entry"><dt class="profile-stats__key">100x</dt>
2572 <dd class="profile-stats__value">${commarise(c100)} (${(100*c100/ctotal).toFixed(2)}%)</dd></dl>
2573 <dl class="profile-stats__entry"><dt class="profile-stats__key">50x</dt>
2574 <dd class="profile-stats__value">${commarise(c50)} (${(100*c50/ctotal).toFixed(2)}%)</dd></dl>`
2575 );
2576 }
2577 });
2578 }
2579 }
2580
2581 function addRecent(){
2582 $("div[data-page-id=recent_activity] .page-extra").append(
2583 `<div class="div-24h"><h2 class="title title--page-extra">Recent 24h</h2></div>`,
2584 $("<div id='op-recent'>Loading...</div>")
2585 );
2586 var container = $("#op-recent");
2587
2588 getUserRecent({u: jsonUser.id, m: gameMode, limit: 50, type: "id"}, function(response){
2589 var userRecent = response;
2590 var failedCheckbox = $("<input type=checkbox id='failedCheckbox'/>"),
2591 failedChecked = settings.failedChecked;
2592
2593 failedCheckbox.prop("checked", failedChecked).click(function(){
2594 var me = $(this);
2595 failedChecked = me.prop("checked");
2596 if(failedChecked){
2597 $(".failedScore").show();
2598 }else{
2599 $(".failedScore").hide();
2600 }
2601 });
2602
2603 container.empty().append(
2604 $("<label></label>").append(
2605 failedCheckbox, "Show failed scores"
2606 )
2607 );
2608
2609 userRecent.forEach(function(play){
2610 var modstr = getMods(play.enabled_mods),
2611 acc = calcAcc(play, gameMode),
2612 dateset = new Date(play.date.replace(' ','T') + "+0000"), // dates from API in GMT+0
2613 maplink = $(`<a href="https://osu.ppy.sh/beatmaps/${play.beatmap_id}" class="play-detail__title u-ellipsis-overflow">
2614 Loading...<small class="play-detail__artist"></small></a>`),
2615 mapver = $(`<span class="play-detail__beatmap">Loading...</span>`),
2616 maxmapcombo = $("<span></span>").css("color", "#b7b1e5"),
2617 //starrating = $("<b>...★</b>"),
2618 failClass = play.rank === "F" ? "failedScore" : "passScore";
2619
2620 var detailrow = $(`<div class='play-detail play-detail--highlightable play-detail--compact ${failClass}'>`).append(
2621 $(`<div class="play-detail__group play-detail__group--top"></div>`).append(
2622 `<div class="play-detail__icon play-detail__icon--main">
2623 <div class="score-rank score-rank--full score-rank--${play.rank}"></div>
2624 </div>`,
2625 $(`<div class="play-detail__detail"></div>`).append(
2626 maplink,
2627 makeScoreStats(play, maxmapcombo),
2628 $(`<div class="play-detail__beatmap-and-time"></div>`).append(
2629 mapver,
2630 `<span class="play-detail__time">
2631 <time class="timeago" datetime="${dateset.toISOString()}">${dateset.toLocaleString()}</time>
2632 </span>`
2633 )
2634 )
2635 ),
2636 `<div class="play-detail__group play-detail__group--bottom">
2637 <div class="play-detail__score-detail play-detail__score-detail--score">
2638 <div class="play-detail__score-detail-top-right">
2639 <div class="play-detail__accuracy-and-weighted-pp">
2640 <span class="play-detail__accuracy">${acc.toFixed(2)}%</span>
2641 </div>
2642 ${play.perfect == '1' ?
2643 `<div><span class="play-detail__fc">(FC)</span></div>` : ""}
2644 </div>
2645 </div>
2646 <div class="play-detail__score-detail play-detail__score-detail--mods">
2647 <div class="mods mods--profile-page">
2648 ${getModsArray(play.enabled_mods).map(function(mod){
2649 return `<div class="mods__mod"><div class="mods__mod-image"><div class="mod mod--${mod.short}" title="${mod.name}"></div></div></div>`;
2650 }).join("")}
2651 </div>
2652 </div>
2653 </div>
2654 <div class="play-detail__pp play-detail__recent-pp"></div>
2655 <div class="play-detail__more"></div>
2656 </div>`
2657 );
2658 addModalBtn(detailrow, play.beatmap_id);
2659
2660 container.append(detailrow);
2661
2662 getBeatmapsCache({b: play.beatmap_id, m: gameMode, a: 1}, function(response){
2663 var r = response[0];
2664 maplink.html(
2665 `${r.title} <small class="play-detail__artist">by ${r.artist}</small>`
2666 );
2667 if(r.max_combo !== null){
2668 maxmapcombo.text(` (${r.max_combo}x)`);
2669 }
2670 mapver.text(r.version);
2671 detailrow.find(".identifier").val(r.beatmap_id);
2672 });
2673 });
2674 $(".timeago").timeago();
2675 if(failedChecked){
2676 $(".failedScore").show();
2677 }else{
2678 $(".failedScore").hide();
2679 }
2680 });
2681 }
2682
2683 return {init: init, destroy: destroy};
2684 }
2685
2686 function osuplusBeatmap(){
2687 var result = null,
2688 mapID = null,
2689 mapsetID = null,
2690 mapMode = null,
2691 beatmapInfo = null,
2692 timeDelay = 1000,
2693 timeoutID = null,
2694 songInfoRef = null,
2695 modBtns = [],
2696 localUser = null,
2697 localUsername = null,
2698 localScore = null,
2699 friends = null,
2700 scoreListing = null,
2701 scoreListingTitlerow = null,
2702 tableLoadingNotice = null,
2703 showDates = true,
2704 modsEnabled = true,
2705 osupreviewLoaded = false,
2706 scoreReqs = [],
2707 maxcomboSpan = null,
2708 subscribed = false;
2709
2710
2711 function addCss(){
2712 $(document.head).append($("<style></style>").html(
2713 ".modIconGroup {display: inline-block; margin: 2px;}\n" +
2714 ".modIcon {overflow: hidden; position: relative; width: 40px; height: 40px;}\n" +
2715 ".modIconOption, .modIconOption img, .modIcon img { width: 100%; height: 100%; }\n" +
2716 ".modIconOption {overflow: hidden; position: absolute; transform: skewX(-45deg);}\n" +
2717 ".modIconOption:first-child {left: 0px; transform-origin: 100% 0;}\n" +
2718 ".modIconOption:last-child {right: 0px; transform-origin: 0 100%;}\n" +
2719 ".modIconOption img {transform: skewX(45deg); transform-origin: inherit;}\n" +
2720 ".notSelected {border: 3px solid transparent;}\n" +
2721 ".isSelected {border: 3px solid red;}\n" +
2722 ".partialSelected {border: 3px dashed red;}\n" +
2723 ".osupreview {width: 425px; height: 344px;}\n" +
2724 ".count-display {text-align: right;}\n" +
2725 "#opslider {width: 250px;}\n" +
2726 ".recentscore {background-color: greenyellow;}\n" +
2727 ".recentscore:hover {background: #fde1ff; cursor: pointer;}\n" +
2728 ".centered {display: block; margin-left: auto; margin-right: auto;}\n" +
2729 ".greyedout {opacity: 0.5;}\n" +
2730 ".ppcalc-pp {cursor: pointer;}\n"
2731 ));
2732 }
2733
2734 function init(){
2735 var temp;
2736
2737 addCss();
2738 songInfoRef = $("#songinfo");
2739 scoreListing = $(".beatmapListing");
2740 scoreListingTitlerow = $(".titlerow");
2741 temp = $(".beatmapTab.active").attr("href").split("&")[0].split("/");
2742 mapID = temp[temp.length - 1];
2743 temp = $(".bmt").attr("src").split("/");
2744 mapsetID = temp[temp.length - 1].split("l")[0];
2745 mapMode = getMapmode();
2746
2747 minePlayerCountries();
2748 showDates = settings.showDates;
2749 temp = $(".content-infoline").children("div").children("b");
2750 if(temp.length > 0){
2751 temp = temp.children("a");
2752 localUsername = temp.text();
2753 temp = temp.attr("href").split("/");
2754 localUser = temp[temp.length - 1];
2755 GMX.getValue("friends", []).then((temp) => {
2756 friends = temp;
2757 getFriends(function(response){
2758 friends = response;
2759 GMX.setValue("friends", friends);
2760 });
2761 });
2762 }else{
2763 localUser = null;
2764 friends = [];
2765 }
2766 maxcomboSpan = $("<span></span>").hide();
2767 songInfoRef.children().children().eq(2).children().eq(5).append("<br>", maxcomboSpan);
2768
2769 if(settings.showMirror)
2770 addBloodcatMirror();
2771 if(settings.showSubscribeMap)
2772 addSubBtn();
2773 showMapValues();
2774 addOsuPreview();
2775
2776 if(hasKey){
2777 putModButtons();
2778 putRankingType();
2779 addSlider();
2780 addTableLoadingNotice();
2781 modifyTableHeaders();
2782 addSearchUser();
2783
2784 var promises = [];
2785 doManyFunc([
2786 function(callback){
2787 getBeatmapInfo(function(response){
2788 beatmapInfo = response;
2789 if(beatmapInfo.max_combo !== null){
2790 maxcomboSpan.text(beatmapInfo.max_combo + "x combo");
2791 maxcomboSpan.show();
2792 }
2793
2794 callback();
2795 });
2796 },
2797 function(callback){
2798 getScoresWithPlayerInfo({b:mapID, m:mapMode, limit:settings.displayTopNum}, settings.showPpRank, function(response){
2799 result = response;
2800 callback();
2801 });
2802 }
2803 ], function(){
2804 modifyTableHeadersMaxcombo();
2805 addScoreLeaderpp();
2806 updateScoresTable();
2807 });
2808 }
2809
2810 $(window).unload(function(){
2811 // save player's countries
2812 GMX.setValue("playerCountries", JSON.stringify(playerCountries));
2813 });
2814 }
2815
2816 function addSlider(){
2817 scoreListing.before(
2818 createSlider(function(val, checked){
2819 var rows = scoreListingTitlerow.nextAll();
2820 if(checked){
2821 var curTime = new Date();
2822 rows.each(function(index, row){
2823 row = $(row);
2824 var scoreTime = new Date(row.find("time").attr("datetime"));
2825 var diff = (curTime - scoreTime) / (1000*60*60*24);
2826 if(diff < val){
2827 row.addClass("recentscore");
2828 }else{
2829 row.removeClass("recentscore");
2830 }
2831 });
2832 }else{
2833 rows.removeClass("recentscore");
2834 }
2835 })
2836 );
2837 }
2838
2839 function abortReqs(){
2840 while(scoreReqs.length > 0){
2841 scoreReqs.pop().abort();
2842 }
2843 }
2844
2845 function addOsuPreview(){
2846 songInfoRef.parent().next().after(
2847 $("<div class='spoiler'></div>").html(
2848 "<div class='spoiler_head' onclick='return toggleSpoiler(this);'>osu!preview</div>" +
2849 "<div class='spoiler_body'><div id='osupreview'></div></div>"
2850 ).click(function(){
2851 if(osupreviewLoaded) return;
2852 $("#osupreview").html(
2853 "If below doesn't work, <a href='http://bloodcat.com/osu/preview.html#" + mapID + "' target='_blank'>open in new tab</a><br>" +
2854 "<iframe class='osupreview' src='https://bloodcat.com/osu/preview.html#" + mapID + "' allowfullscreen></iframe>"
2855 );
2856 osupreviewLoaded = true;
2857 })
2858 );
2859 }
2860
2861 function getBeatmapInfo(callback){
2862 getBeatmaps({b: mapID, m: mapMode, a: 1}, function(response){
2863 callback(response[0]);
2864 });
2865 }
2866
2867 function addTableLoadingNotice(){
2868 tableLoadingNotice = $("<div><img src='" + loaderImg + "' class='centered'></div>");
2869 scoreListing.before(tableLoadingNotice);
2870 }
2871
2872 function addSearchUser(){
2873 $(".content-with-bg").children("h2").before(
2874 $("<div></div>").attr("id", "searchuser")
2875 .append(
2876 $("<strong>Search user: </strong>"),
2877 $("<input type='text' id='searchusertxt' name='searchusertxt'>")
2878 .val(localUsername)
2879 .bind("enterKey", searchUserEnter)
2880 .keyup(function(e){
2881 if(e.keyCode == 13)
2882 {
2883 $(this).trigger("enterKey");
2884 }
2885 }),
2886 $("<div id='searchuserinfo'>Searching...</div>").hide(),
2887 $("<div></div>").attr("class","beatmapListing")
2888 .attr("id", "searchuserresult")
2889 .append(
2890 $("<table width=100% cellspacing=0></table>").append("<tbody></tbody>").append(
2891 scoreListingTitlerow.clone()
2892 )
2893 ).hide()
2894 )
2895 );
2896 }
2897
2898 function searchUserEnter(){
2899 $("#searchuserinfo").text("Searching...").show();
2900 $("#searchuserresult").hide();
2901 var searchusernames = $("#searchusertxt").val().split(',');
2902 var promises = searchusernames.map((username) => new Promise(function(resolve, reject){
2903 getScoresWithPlayerInfo({b:mapID, u:username, m:mapMode, type:"string"}, settings.showPpRank, resolve);
2904 }));
2905 Promise.all(promises).then((responses) => {
2906 var response = [];
2907 for(let r of responses){
2908 response = response.concat(r);
2909 }
2910 response.sort((a,b) => parseInt(b.score) - parseInt(a.score));
2911 if(response.length > 0){
2912 $("#searchuserresult").find(".titlerow").nextAll().remove();
2913 response.forEach(function(score, index){
2914 var tableRow = makeScoreTableRow(score, index+1);
2915 $("#searchuserresult").find(".titlerow").parent().children().last().after(tableRow);
2916 });
2917
2918 $(".timeago").timeago();
2919 $("#searchuserinfo").hide();
2920 $("#searchuserresult").show();
2921 }else{
2922 $("#searchuserinfo").text("No scores found :(");
2923 }
2924 });
2925 }
2926
2927 function putRankingType(){
2928 $(".content-with-bg").children("h2").after(
2929 $("<div></div>").attr("id", "rankingtype").append(
2930 $("<label></label>").append($("<input>").attr({type: "radio",
2931 name: "rankingtype",
2932 value: "global"})
2933 .prop("checked", true)
2934 .change(rankingTypeChanged),
2935 "Global"),
2936 $("<label></label>").append($("<input>").attr({type: "radio",
2937 name: "rankingtype",
2938 value: "friends"})
2939 .change(rankingTypeChanged),
2940 "Friends"),
2941 //Show date button
2942 $("<label></label>").append($("<input>").attr({type: "checkbox",
2943 id: "showdatebox"})
2944 .change(showDateChanged)
2945 .prop("checked", showDates),
2946 "Show date")
2947 )
2948 );
2949 }
2950
2951 function showDateChanged(){
2952 showDates = $("#showdatebox").prop("checked");
2953 updateShowDate();
2954 }
2955
2956 function updateShowDate(){
2957 if(showDates) $(".datecol").show();
2958 else $(".datecol").hide();
2959 }
2960
2961 function rankingTypeChanged(){
2962 var rankingType = $("input[name=rankingtype]:checked").val();
2963
2964 if(rankingType == "global"){
2965 modsEnabled = true;
2966 if(timeoutID !== null) clearTimeout(timeoutID);
2967 updateModScores();
2968 }else if(rankingType == "friends"){
2969 modsEnabled = false;
2970 if(timeoutID !== null) clearTimeout(timeoutID);
2971 updateFriendsScores();
2972 }
2973 }
2974
2975 function updateFriendsScores(){
2976 clearScoresTable();
2977 abortReqs();
2978
2979 // Make copy of friends including yourself
2980 var friends2 = friends.slice(0);
2981 if(!inArray(friends2, localUser)){
2982 friends2.push(localUser);
2983 }
2984
2985 var funs = [];
2986 for(var i=0; i<friends2.length; i++){
2987 funs.push(function(uid){
2988 return function(callback){
2989 getScoresWithPlayerInfo({b:mapID, u:uid, m:mapMode, type:"id"}, settings.showPpRank, function(response){
2990 if(response.length > 0){
2991 result.push(response[0]);
2992 }
2993 callback();
2994 }, scoreReqs);
2995 };
2996 }(friends2[i]));
2997 }
2998 doManyFunc(funs, function(){
2999 sortResult("score");
3000 updateScoresTable();
3001 });
3002 }
3003
3004 function sortResult(sortby){
3005 if(sortby === "score"){
3006 result.sort(function(a,b){
3007 var ascore = parseInt(a.score),
3008 bscore = parseInt(b.score);
3009 if(ascore < bscore){
3010 return 1;
3011 }else if(ascore > bscore){
3012 return -1;
3013 }else{
3014 return getTime(a.date) - getTime(b.date);
3015 }
3016 });
3017 }else if(sortby === "pp"){
3018 result.sort(function(a,b){
3019 var ascore = parseFloat(a.pp),
3020 bscore = parseFloat(b.pp);
3021 if(ascore < bscore){
3022 return 1;
3023 }else if(ascore > bscore){
3024 return -1;
3025 }else{
3026 ascore = parseFloat(a.score);
3027 bscore = parseFloat(b.score);
3028 if(ascore < bscore){
3029 return 1;
3030 }else if(ascore > bscore){
3031 return -1;
3032 }else{
3033 return getTime(a.date) - getTime(b.date);
3034 }
3035 }
3036 });
3037 }
3038 }
3039
3040 function getFriends(callback){
3041 GetPage("https://old.ppy.sh/p/friends", function(response){
3042 var friends = [];
3043 response = response.replace(/<img[^>]*>/g,"");
3044 $(response).find(".paddingboth").children("div").each(function(ind, ele){
3045 friends.push($(ele).attr("user_id"));
3046 });
3047 callback(friends);
3048 });
3049 }
3050
3051 function isFriend(uid){
3052 for(var i=0; i<friends.length; i++){
3053 if(friends[i] == uid) return true;
3054 }
3055 return false;
3056 }
3057
3058 function addScoreLeaderpp(){
3059 var scoreLeaders = $(".scoreLeader");
3060 if(scoreLeaders.length > 0) updateScoreLeaderpp(scoreLeaders.first(), result[0]);
3061 if(scoreLeaders.length > 1){
3062 getScoresWithPlayerInfo({b:mapID, u:localUser, m:mapMode, type:"id"}, settings.showPpRank, function(response){
3063 if(response.length > 0){
3064 localScore = response[0];
3065 updateScoreLeaderpp(scoreLeaders.eq(1), localScore);
3066 }
3067 });
3068 }
3069 }
3070
3071 function updateScoreLeaderpp(scoreLeader, score){
3072 var rows = scoreLeader.children().children();
3073 var numrows = rows.length;
3074 rows.eq(1).children().last().attr("rowspan", numrows);
3075 var rowclass = "row" + ((numrows+1)%2+1) + "p";
3076 scoreLeader.children().append($("<tr></tr>").attr("class", rowclass)
3077 .append($("<td><strong>pp</strong></td>"))
3078 .append($("<td></td>").text(parseFloat(score.pp).toFixed(2)))
3079 );
3080 }
3081
3082
3083 function showMapValues(){
3084 var basewidth = 140;
3085 var csele = songInfoRef.children().children().eq(0).children().eq(3).children(),
3086 arele = songInfoRef.children().children().eq(0).children().eq(5).children(),
3087 hpele = songInfoRef.children().children().eq(1).children().eq(3).children(),
3088 odele = songInfoRef.children().children().eq(2).children().eq(3).children();
3089 var csval = 10 * csele.children().width() / basewidth,
3090 arval = 10 * arele.children().width() / basewidth,
3091 hpval = 10 * hpele.children().width() / basewidth,
3092 odval = 10 * odele.children().width() / basewidth;
3093 csele.after("\n(" + csval.toFixed(1) + ")");
3094 arele.after("\n(" + arval.toFixed(1) + ")");
3095 hpele.after("\n(" + hpval.toFixed(1) + ")");
3096 odele.after("\n(" + odval.toFixed(1) + ")");
3097
3098 // Bold star difficulty
3099 var starparent = songInfoRef.children().children().eq(1).children().last();
3100 var star = starparent.text();
3101 starparent.contents().filter(function(){
3102 return this.nodeType === 3;
3103 }).remove();
3104 starparent.append($("<strong>" + star + "</strong>"));
3105 }
3106
3107 function addBloodcatMirror(){
3108 if(mapsetID !== null){
3109 $(".beatmapDownloadButton").first().before(
3110 $("<div>").addClass("beatmapDownloadButton").append(
3111 $("<a>").addClass("beatmap_download_link")
3112 .attr("href", "http://bloodcat.com/osu/s/" + mapsetID)
3113 .append($("<img src='"+bloodcatBtnImg+"'>"))
3114 )
3115 );
3116 // old mirror
3117 //$(".beatmap_download_link").last().after("<br>",$("<a></a>").text("Bloodcat mirror").attr("href", "http://bloodcat.com/osu/s/" + mapsetID))
3118 }
3119 }
3120
3121 async function addSubBtn(){
3122 subscribed = await subscriberManager.map.isSubscribed(mapsetID);
3123 var artist = songInfoRef.find("tr").first().children().eq(1).children().text();
3124 var title = songInfoRef.find("tr").eq(1).children().eq(1).children().text();
3125 var creator = songInfoRef.find("tr").eq(2).children().eq(1).children().text();
3126 var temp = songInfoRef.find("tr").eq(2).children().eq(1).children().attr("href").split("/");
3127 var creator_id = temp[temp.length-1];
3128 $(".beatmapDownloadButton").first().before(
3129 $("<div>").addClass("beatmapDownloadButton").append(
3130 $("<a>").addClass("beatmap_download_link")
3131 .append($("<img class='subImg'>"))
3132 .click(function(){
3133 if(subscribed){
3134 subscriberManager.map.remove(mapsetID).then(() => {
3135 subscribed = false;
3136 refreshSubImg(subscribed);
3137 });
3138 }else{
3139 subscriberManager.map.add(mapsetID, artist, title, creator, creator_id).then(() => {
3140 subscribed = true;
3141 refreshSubImg(subscribed);
3142 });
3143 }
3144 })
3145 )
3146 );
3147 refreshSubImg(subscribed);
3148 }
3149
3150 function refreshSubImg(subscribed){
3151 if(subscribed){
3152 $(".subImg").attr("src", subbedImg);
3153 }else{
3154 $(".subImg").attr("src", subImg);
3155 }
3156 }
3157
3158 function putModButtons(){
3159 function genModBtns(modArray){
3160 var modgroup = $("<div></div>").addClass("modIconGroup"),
3161 modgroupArr = [];
3162 for(var i=0; i<modArray.length; i++){
3163 var modinfo = modArray[i];
3164 var modimg;
3165 if(modinfo.mods.length === 1){
3166 modimg = $("<div></div>").addClass("modIcon")
3167 .append($("<img>").attr("src", modIconImgs[modinfo.mods[0]]))
3168 .attr("value", modinfo.mods[0]);
3169 }else{
3170 modimg = $("<div></div>").addClass("modIcon").append(
3171 $("<div></div>").addClass("modIconOption").append(
3172 $("<img>").attr("src", modIconImgs[modinfo.mods[0]])
3173 ),
3174 $("<div></div>").addClass("modIconOption").append(
3175 $("<img>").attr("src", modIconImgs[modinfo.mods[1]])
3176 )
3177 )
3178 .attr("value", modinfo.mods.join(","));
3179 }
3180 if(i > 0) modimg.hide();
3181
3182 if(modinfo.mods[0] === "NM"){
3183 modimg.click(nomodIconClick);
3184 modimg.addClass("nomodIcon");
3185 }else{
3186 modimg.click(modIconClick);
3187 }
3188
3189 if(modinfo.selection === 0){
3190 modimg.addClass("notSelected");
3191 modimg.attr("value", "XX");
3192 }else if(modinfo.selection === 1){
3193 modimg.addClass("isSelected");
3194 modimg.attr("value", modinfo.mods.join(','));
3195 }else{ // modinfo.selection === 2
3196 modimg.addClass("partialSelected");
3197 modimg.attr("value", ["XX"].concat(modinfo.mods).join(','));
3198 }
3199 modgroupArr.push(modimg);
3200 }
3201 modgroup.html(modgroupArr);
3202 return modgroup;
3203 }
3204
3205 $(".content-with-bg").children("h2").next().empty().append(
3206 genModBtns([
3207 {mods: ["NM"], selection: 0},
3208 {mods: ["NM"], selection: 1}]),
3209 mapMode < 3 ? // HD for non-mania
3210 genModBtns([
3211 {mods: ["HD"], selection: 0},
3212 {mods: ["HD"], selection: 1},
3213 {mods: ["HD"], selection: 2}]) :
3214 genModBtns([ // FI, HD for mania
3215 {mods: ["FI"], selection: 0},
3216 {mods: ["FI"], selection: 1},
3217 {mods: ["HD"], selection: 1},
3218 {mods: ["FI", "HD"], selection: 1},
3219 {mods: ["FI", "HD"], selection: 2}]),
3220 genModBtns([
3221 {mods: ["HR"], selection: 0},
3222 {mods: ["HR"], selection: 1},
3223 {mods: ["HR"], selection: 2},
3224 {mods: ["EZ"], selection: 1},
3225 {mods: ["EZ"], selection: 2}]),
3226 genModBtns([
3227 {mods: ["DT"], selection: 0},
3228 {mods: ["DT"], selection: 1},
3229 {mods: ["NC"], selection: 1},
3230 {mods: ["DT", "NC"], selection: 1},
3231 {mods: ["HT"], selection: 1}]),
3232 genModBtns([
3233 {mods: ["SD"], selection: 0},
3234 {mods: ["SD"], selection: 1},
3235 {mods: ["PF"], selection: 1},
3236 {mods: ["SD", "PF"], selection: 1},
3237 {mods: ["SD", "PF"], selection: 2},
3238 {mods: ["NF"], selection: 1},
3239 {mods: ["NF"], selection: 2}]),
3240 genModBtns([
3241 {mods: ["FL"], selection: 0},
3242 {mods: ["FL"], selection: 1},
3243 {mods: ["FL"], selection: 2}]),
3244 mapMode < 3 ? [] : //mania keys
3245 genModBtns([
3246 {mods: ["4K"], selection: 0},
3247 {mods: ["4K"], selection: 1},
3248 {mods: ["5K"], selection: 1},
3249 {mods: ["6K"], selection: 1},
3250 {mods: ["7K"], selection: 1},
3251 {mods: ["8K"], selection: 1},
3252 {mods: ["9K"], selection: 1}]),
3253 mapMode > 0 ? [] : //SO only for standard
3254 [genModBtns([
3255 {mods: ["SO"], selection: 0},
3256 {mods: ["SO"], selection: 1},
3257 {mods: ["SO"], selection: 2}]),
3258 genModBtns([
3259 {mods: ["TD"], selection: 0},
3260 {mods: ["TD"], selection: 1},
3261 {mods: ["TD"], selection: 2}])]
3262 );
3263 }
3264
3265 function nomodIconClick(){
3266 if(!modsEnabled) return;
3267 $(".modIcon").each(function(){
3268 $(this).hide();
3269 $(this).parent().children().first().show();
3270 });
3271 modIconClick.bind(this)();
3272 }
3273
3274 function modIconClick(){
3275 if(!modsEnabled) return;
3276 if(!$(this).hasClass("nomodIcon")){
3277 $(".nomodIcon").hide().parent().children().first().show();
3278 }
3279 var parent = $(this).parent();
3280 $(this).hide();
3281 if($(this).next().length === 0){
3282 parent.children().first().show();
3283 }else{
3284 $(this).next().show();
3285 }
3286
3287 timeoutUpdate();
3288 }
3289
3290 function timeoutUpdate(){
3291 if(timeoutID !== null){
3292 clearTimeout(timeoutID);
3293 }
3294 timeoutID = setTimeout(function(){
3295 timeoutID = null;
3296 updateModScores();
3297 }, timeDelay);
3298 }
3299
3300 function updateModScores(){
3301 clearScoresTable();
3302 abortReqs();
3303
3304 var modvals = getSelectedMods();
3305 var funs = [];
3306 result = [];
3307 for(var i=0; i<modvals.length; i++){
3308 var modval = modvals[i];
3309 if(modval < 0){
3310 funs.push(function(callback){
3311 getScoresWithPlayerInfo({b:mapID, m:mapMode, limit:settings.displayTopNum}, settings.showPpRank, function(response){
3312 result = result.concat(response);
3313 callback();
3314 }, scoreReqs);
3315 });
3316 }else{
3317 funs.push(function(modval){
3318 return function(callback){
3319 getScoresWithPlayerInfo({b:mapID, m:mapMode, limit:settings.displayTopNum, mods:modval}, settings.showPpRank, function(response){
3320 result = result.concat(response);
3321 callback();
3322 }, scoreReqs);
3323 };
3324 }(modval));
3325 }
3326 }
3327 doManyFunc(funs, function(){
3328 sortResult("score");
3329 result.splice(settings.displayTopNum);
3330 updateScoresTable();
3331 });
3332
3333 }
3334
3335 function getSelectedMods(){
3336 var selected = [[]];
3337 $(".modIcon:visible").each(function(){
3338 var modarray = $(this).attr("value").split(',');
3339 selected = cartesianProd(selected, modarray);
3340 });
3341
3342 // handle doublemods
3343 for(var si=0; si<selected.length; si++){
3344 for(var i=0; i<doublemods.length; i++){
3345 if(selected[si].indexOf(doublemods[i][0]) >= 0){
3346 if(selected[si].indexOf(doublemods[i][1]) < 0){
3347 selected[si].push(doublemods[i][1]);
3348 }
3349 }
3350 }
3351 }
3352
3353 var modvals = [];
3354 for(var si=0; si<selected.length; si++){
3355 var modval = 0;
3356 for(var i=0; i<modnames.length; i++){
3357 if(selected[si].indexOf(modnames[i].short) >= 0){
3358 modval += modnames[i].val;
3359 }
3360 }
3361 modvals.push(modval);
3362 }
3363 if(selected.length === 1 && modvals[0] === 0 && selected[0].indexOf("NM") < 0){ //get all scores
3364 return [-1];
3365 }
3366 return modvals;
3367 }
3368
3369 function getMapmode(){
3370 return parseInt($(".beatmapTab.active").attr("href").slice(-1));
3371 }
3372
3373 function modifyTableHeaders(){
3374 if(mapMode == 3){
3375 //because ppy fked up
3376 scoreListingTitlerow.children().eq(12).remove();
3377 }
3378
3379 // Add pp column
3380 scoreListingTitlerow.children().eq(2).after(
3381 $("<th></th>")
3382 .append($(`<a><strong>pp</strong></a>`)
3383 .click(function(){
3384 sortResult("pp");
3385 updateScoresTable();
3386 })
3387 )
3388 );
3389
3390 // Add click scores to sort
3391 scoreListingTitlerow.children().eq(2).empty().append(
3392 $("<a><strong>Score</strong></a>")
3393 .click(function(){
3394 sortResult("score");
3395 updateScoresTable();
3396 })
3397 );
3398
3399 // Add date column
3400 scoreListingTitlerow.children().last().before(
3401 $("<th class='datecol'>Date</th>")
3402 );
3403
3404 // Add replay column
3405 scoreListingTitlerow.children().last().after(
3406 $("<th></th>")
3407 );
3408
3409 // Fill empty pp and date
3410 scoreListingTitlerow.nextAll().each(function(index, row){
3411 row = $(row);
3412 row.children().eq(2).after("<td></td>");
3413 row.children().last().before("<td></td>");
3414 });
3415 }
3416
3417 function modifyTableHeadersMaxcombo(){
3418 scoreListingTitlerow.children().eq(6).html("<strong>Max combo" + (beatmapInfo.max_combo === null ? "" : " (" + beatmapInfo.max_combo + ")") + "</strong>");
3419 }
3420
3421 function clearScoresTable(){
3422 result = [];
3423 updateScoresTable(function(){
3424 tableLoadingNotice.show();
3425 });
3426 }
3427
3428 function updateScoresTable(callback){
3429 var tableRef = scoreListingTitlerow.parent();
3430 var tableRows = [];
3431
3432 var usedUsers = [];
3433 var rank = 0;
3434 for(var i=0; i<result.length; i++){
3435 var greyedout = false;
3436 if(inArray(usedUsers, result[i].user_id)){
3437 greyedout = true;
3438 }else{
3439 usedUsers.push(result[i].user_id);
3440 rank += 1;
3441 }
3442 var tableRow = makeScoreTableRow(result[i], rank, greyedout);
3443 tableRows[i] = tableRow;
3444 }
3445
3446 scoreListingTitlerow.nextAll().remove();
3447 tableRef.append(tableRows);
3448 $(".timeago").timeago();
3449 updateShowDate();
3450 tableLoadingNotice.hide();
3451 if(callback) callback();
3452 }
3453
3454 function makeScoreTableRow(score, rankno, greyedout){
3455 var country = score.user.country.toLowerCase();
3456 var acc = calcAcc(score, mapMode);
3457 var rowclass, dateset;
3458 dateset = new Date(score.date.replace(' ','T') + "+0000"); // dates from API in GMT+0
3459
3460 // handle colour of the row, depending on you, friend, odd/even row etc
3461 if(localUser !== null && localUser.toString() === score.user_id){
3462 rowclass = "row3p";
3463 }else if(isFriend(score.user_id)){
3464 rowclass = "row4p";
3465 }else{
3466 rowclass = "row" + ((rankno+1)%2 + 1) + "p";
3467 }
3468
3469 if(greyedout){
3470 rowclass += " greyedout";
3471 }
3472
3473 var countryImg = "";
3474 if(country !== "")
3475 countryImg = "<img src=" + getCountryUrl(country) + ">";
3476 var userhref = "<a href='/u/" + score.user_id +"''>" + score.username + "</a>";
3477 var pprank = "";
3478 if(score.user.pp_rank === undefined){
3479 pprank = " <span class='pprank'></span>";
3480 }else{
3481 pprank = " <span class='pprank'>(#" + score.user.pp_rank + ")</span>";
3482 }
3483 var ppcalcData = {id: mapID, m: score.enabled_mods, c: score.maxcombo, acc: acc, miss: score.countmiss};
3484
3485 var row = $(`<tr class='${rowclass}'>
3486 <td>${score.replay_available == "1" ? `<a class='require-login' href='/web/osu-getreplay.php?c=${score.score_id}&m=${mapMode}'>#${rankno}</a>` :
3487 `#${rankno}`}</td>
3488 <td>${getRankImg(score.rank)}</td>
3489 <td>${rankno == 1 ? `<b>${commarise(score.score)}</b>` : commarise(score.score)}</td>
3490 <td class='${mapMode == 0 ? "ppcalc-pp" : ""}'>${parseFloat(score.pp).toFixed(settings.pp2dp ? 2 : 0)} <span></span></td>
3491 <td>${acc == 100 ? `<b>${acc.toFixed(2)}%</b>` : `${acc.toFixed(2)}%`}</td>
3492 <td>${countryImg}\n${userhref}${pprank}</td>
3493 <td>${score.maxcombo}</td>
3494 ${mapMode == 3 ?
3495 // Mania
3496 `<td>${score.countgeki}</td>
3497 <td>${score.count300}</td>
3498 <td>${score.countkatu}</td>
3499 <td>${score.count100}</td>
3500 <td>${score.count50}</td>` :
3501 // Standard/Taiko/CTB
3502 `<td>${score.count300} / ${score.count100} / ${score.count50}</td>
3503 <td>${score.countgeki}</td>
3504 <td>${score.countkatu}</td>`}
3505 <td>${score.countmiss}</td>
3506 <td>${getMods(score.enabled_mods)}</td>
3507 <td class='datecol'>
3508 <time class='timeago' datetime='${dateset.toISOString()}'>${dateset.toLocaleString()}</time>
3509 </td>
3510 <td><a onclick='reportscore(${score.score_id});'>Report</a></td>
3511 <td class='op-ppcalc-data' hidden>${JSON.stringify(ppcalcData)}</td>
3512 </tr>`);
3513 //ppcalc, only for std
3514 if(mapMode == 0){
3515 row.find(".ppcalc-pp").click(function(event){
3516 var me = $(this);
3517 me.find("span").text("(...)");
3518 var ppcalcData = JSON.parse(me.parent().find(".op-ppcalc-data").text());
3519 getPpCalc(ppcalcData).then((result) => {
3520 if(result.rql == "ranked" || result.rql == "qualified"){
3521 me.find("span").text(`(${result.pp_fc} if FC)`);
3522 }else{
3523 me.html(`<span>${result.pp} (${result.pp_fc} if FC)</span>`);
3524 }
3525 });
3526 });
3527 }
3528 return row;
3529 }
3530
3531 function dlReplay(score){
3532 getReplay({m: mapMode, b: mapID, u: score.user_id}, function(response){
3533 if(response.error){
3534 alert("Replay not available :(");
3535 }else{
3536 var playdata = atob(response.content);
3537 var replay = new Replayer.Replay(score, beatmapInfo, playdata, mapMode);
3538 var osr = replay.composeOsr();
3539 var modeName;
3540 if(mapMode === 0) modeName = "Osu";
3541 else if(mapMode === 1) modeName = "Taiko";
3542 else if(mapMode === 2) modeName = "CatchTheBeat";
3543 else modeName = "OsuMania";
3544 var filename = score.username + " - " +
3545 beatmapInfo.artist + " - " +
3546 beatmapInfo.title + " [" +
3547 beatmapInfo.version + "] (" +
3548 score.date.split(' ')[0] + ") " +
3549 modeName + ".osr";
3550 downloadFile(osr, filename);
3551 }
3552 });
3553 }
3554
3555 function minePlayerCountries(){
3556 scoreListingTitlerow.nextAll().each(function(index, ele){
3557 var data = $(ele).children().eq(4).children();
3558 if(data.length > 1){
3559 var temp = data.first().attr("src").split("/");
3560 var country = temp[temp.length-1].split(".")[0].toLowerCase();
3561 temp = data.last().attr("href").split("/");
3562 var uid = temp[temp.length-1];
3563 savePlayerCountry(uid, country);
3564 }
3565 });
3566 }
3567
3568 return {init: init};
3569 }
3570
3571 function osuplusNewBeatmap(){
3572 var mapID = null,
3573 scoresResult = null,
3574 mapMode = 0,
3575 jsonBeatmapset = null,
3576 jsonCountries = null,
3577 maxCombo = null,
3578 showDates = true,
3579 modsEnabled = true,
3580 timeDelay = 1000,
3581 timeoutID = null,
3582 friends = [],
3583 scoreReqs = [],
3584 tableLoadingNotice = null,
3585 tableWaiter = null,
3586 tableObserver = null,
3587 beatmapWaiter = null,
3588 beatmapObserver = null,
3589 subscribed = null,
3590 currentUser = null;
3591
3592 function addCss(){
3593 if(!$(".osuplus-new-beatmap-style").length){
3594 $(document.head).append($("<style class='osuplus-new-beatmap-style'></style>").html(
3595 `.modIconGroup {display: inline-block; margin: 2px;}
3596 .modIcon {overflow: hidden; position: relative; width: 46px; height: 46px;}
3597 .modIconOption, .modIconOption img, .modIcon img { width: 100%; height: 100%; }
3598 .modIconOption {overflow: hidden; position: absolute; transform: skewX(-45deg);}
3599 .modIconOption:first-child {left: 0px; transform-origin: 100% 0;}
3600 .modIconOption:last-child {right: 0px; transform-origin: 0 100%;}
3601 .modIconOption img {transform: skewX(45deg); transform-origin: inherit;}
3602 .notSelected {border: 3px solid transparent;}
3603 .isSelected {border: 3px solid red;}
3604 .partialSelected {border: 3px dashed red;}
3605 .osupreview {width: 425px; height: 344px;}
3606 #opslider {width: 250px; display: inline-block; margin: 10px;}
3607 .recentscore > td {background-color: greenyellow !important;}
3608 .centered {display: block; margin-left: auto; margin-right: auto;}
3609 .greyedout {opacity: 0.5}
3610 #rankingtype label {padding: 8px}
3611 .search-beatmap-scoreboard-table__table {width: 100%; min-width: 800px; font-size: 12px;}
3612 #searchuser {margin-bottom: 10px;}
3613 .osupreview-container {padding: 30px;}
3614 .beatmap-scoreboard-table__header--miss {max-width: 45px; min-width: 30px; width: auto;}
3615 .beatmap-scoreboard-table__header a {cursor: pointer;}
3616 .sub-button {background-color: #29b; background-image: none;}
3617 .subbed {background-color: #ef77af;}
3618 #searchusertxt {color: black;}
3619 .beatmap-scoreboard-table__cell--grade {width: auto; height: auto; display: table-cell;}
3620 .beatmap-scoreboard-table__cell--grade .score-rank {width: 100%;}
3621 .ppcalc-pp {cursor: pointer;}`
3622 ));
3623 }
3624 }
3625
3626 function init(){
3627 if($("#osuplusloaded").length) return;
3628 $("body").append("<a hidden id='osuplusloaded'></a>");
3629 addCss();
3630 jsonCountries = JSON.parse($("#json-countries").text());
3631 jsonBeatmapset = JSON.parse($("#json-beatmapset").text());
3632 showDates = settings.showDates;
3633 currentUser = JSON.parse(JSON.stringify(unsafeWindow.currentUser));
3634 friends = extractFriends();
3635
3636 beatmapWaiter = waitForEl(".beatmapset-beatmap-picker", function(el){
3637 if(settings.showMirror)
3638 addBloodcatMirror();
3639 if(settings.showSubscribeMap)
3640 addSubBtn();
3641 refreshBeatmapsetHeader();
3642 beatmapObserver = whenBeatmapChange(() => {
3643 refreshBeatmapsetHeader();
3644 });
3645 tableWaiter = waitForEl(".beatmap-scoreboard-table", function(el){
3646 refreshTable();
3647 tableObserver = whenScoreboardChange(refreshTable);
3648 });
3649 });
3650 }
3651
3652 function whenBeatmapChange(callback){
3653 var obs = new MutationObserver(callback);
3654 obs.observe($(".beatmapset-beatmap-picker")[0], {attributes: true, subtree: true});
3655 return obs;
3656 }
3657
3658 function whenScoreboardChange(onchange){
3659 var observer = new MutationObserver(function(mutationList, observer){
3660 for(var mutation of mutationList){
3661 if(mutation.type == "childList" && mutation.addedNodes.length){
3662 var ele = $(":not(.osuplus-table).beatmap-scoreboard-table__table")[0];
3663 if(ele){
3664 observer.observe(ele, {characterData: true, subtree: true});
3665 }
3666 }
3667 }
3668 onchange();
3669 });
3670 observer.observe($(":not(.osuplus-table).beatmap-scoreboard-table__table")[0], {characterData: true, subtree: true});
3671 observer.observe($(".beatmapset-scoreboard__main")[0], {childList: true});
3672 return observer;
3673 }
3674
3675 function refreshBeatmapsetHeader(){
3676 $(".osuplus-header").remove();
3677 addOsuPreview();
3678 }
3679
3680 function refreshTable(){
3681 $(".osuplus-table").remove();
3682 mapID = $(".beatmapset-beatmap-picker__beatmap--active").attr("href").split("/")[1];
3683 mapMode = getMapmode();
3684 maxCombo = getMaxCombo(jsonBeatmapset, mapID, mapMode);
3685 minePlayerCountries();
3686
3687 if($(".page-tabs__tab--active").text() == "Global Ranking" && ($(".beatmapset-scoreboard__mods--initial").length || $(".beatmapset-scoreboard__mods").length==0)){
3688 if(hasKey){
3689 addSearchUser();
3690 putRankingType();
3691 putModButtons();
3692 addSlider();
3693 tableLoadingNotice = addTableLoadingNotice();
3694 tableLoadingNotice.show();
3695 replicateTable();
3696 modifyTableHeaders();
3697
3698 getScoresWithPlayerInfo({b:mapID, m:mapMode, limit:settings.displayTopNum}, settings.showPpRank, function(response){
3699 scoresResult = response;
3700 updateScoresTable();
3701 });
3702 }
3703 }else{
3704 $(".beatmap-scoreboard-table__table").show();
3705 }
3706 }
3707
3708 function destroy(){
3709 if(tableObserver) tableObserver.disconnect();
3710 if(tableWaiter) tableWaiter.abort();
3711 if(beatmapObserver) beatmapObserver.disconnect();
3712 if(beatmapWaiter) beatmapWaiter.abort();
3713 $(".osuplus-new-beatmap-style").remove();
3714 GMX.setValue("playerCountries", JSON.stringify(playerCountries));
3715 }
3716
3717 function getMaxCombo(jsonBeatmapset, mapID, mapMode){
3718 var beatmaps = jsonBeatmapset.beatmaps.filter(function(map){
3719 return map.id.toString() == mapID && map.mode_int == mapMode;
3720 });
3721 if(beatmaps.length){
3722 if(beatmaps[0].max_combo){
3723 return beatmaps[0].max_combo[0];
3724 }
3725 }
3726 return null;
3727 }
3728
3729 function replicateTable(){
3730 var oldTable = $(".beatmap-scoreboard-table__table");
3731 var newTable = $("<table class='osuplus-table beatmap-scoreboard-table__table'></table>");
3732 newTable.html(oldTable.html());
3733 oldTable.hide();
3734 oldTable.after(newTable);
3735 }
3736
3737 function extractFriends(){
3738 return currentUser.friends.map((friend) => friend.target_id.toString());
3739 }
3740
3741 function countryNameFromCode(code, jsonCountries){
3742 for(var c of jsonCountries){
3743 if(c.code === code){
3744 return c.name;
3745 }
3746 }
3747 return "Unknown";
3748 }
3749
3750 function getMapmode(){
3751 var modeStr = $(".beatmapset-beatmap-picker__beatmap--active").attr("href").slice(1).split("/")[0];
3752 return modeToInt(modeStr);
3753 }
3754
3755 function makeZeroableEntry(num){
3756 var cellClass = "beatmap-scoreboard-table__cell";
3757 return `<td class='${cellClass} ${num === "0" ? `${cellClass}--zero` : ""}'>${commarise(num)}</td>`;
3758 }
3759
3760 function makeScoreTableRow(score, rankno, greyedout){
3761 var country = score.user.country.toLowerCase();
3762 var countryUpper = country.toUpperCase();
3763 var acc = calcAcc(score, mapMode);
3764 var rowclass, dateset;
3765 dateset = new Date(score.date.replace(' ','T') + "+0000"); // dates from API in GMT+0
3766
3767 rowclass = "beatmap-scoreboard-table__body-row beatmap-scoreboard-table__body-row--highlightable";
3768 if(currentUser !== null && currentUser.id.toString() === score.user_id){ // self
3769 rowclass += " beatmap-scoreboard-table__body-row--self";
3770 }else if(isFriend(score.user_id)){
3771 rowclass += " beatmap-scoreboard-table__body-row--friend";
3772 }
3773 if(rankno === 1){
3774 rowclass += " beatmap-scoreboard-table__body-row--first";
3775 }
3776 if(greyedout){
3777 rowclass += " greyedout";
3778 }
3779
3780 var countryName = countryNameFromCode(countryUpper, jsonCountries);
3781 var countryImg = "";
3782 if(country !== ""){
3783 countryImg = `<a href='/rankings/osu/performance?country=${countryUpper}'><span class='flag-country flag-country--scoreboard flag-country--small-box' \
3784 style='background-image: url("/images/flags/${countryUpper}.png");' title='${countryName}'></span></a>`;
3785 }
3786 var userhref = `<a class='beatmap-scoreboard-table__user-link js-usercard' data-user-id='${score.user_id}' href='/users/${score.user_id}'>${score.username}</a>`;
3787 var pprank;
3788 if(score.user.pp_rank === undefined){
3789 pprank = " <span class='pprank'></span>";
3790 }else{
3791 pprank = ` <span class='pprank'>(#${score.user.pp_rank})</span>`;
3792 }
3793 var ppcalcData = {id: mapID, m: score.enabled_mods, c: score.maxcombo, acc: acc, miss: score.countmiss};
3794
3795 var cellClass = "beatmap-scoreboard-table__cell";
3796 var row = $(`<tr class='${rowclass}'>
3797 <td class='${cellClass} ${cellClass}--rank'>
3798 ${score.replay_available == "1" ? `<a class='require-login' href='/scores/${intToMode(mapMode)}/${score.score_id}/download'>#${rankno}</a>` : `#${rankno}`}</td>
3799 <td class='${cellClass} ${cellClass}--grade'><div class='score-rank score-rank--tiny score-rank--${score.rank}'></div></td>
3800 <td class='${cellClass} ${cellClass}--score'>${commarise(score.score)}</td>
3801 <td class='${cellClass} ${acc==100 ? `${cellClass}--perfect'` : ""}'>${acc.toFixed(2)}%</td>
3802 <td class='${cellClass}'>${countryImg}</td>
3803 <td class='${cellClass}'>${userhref}${pprank}</td>
3804 <td class='${cellClass} ${score.perfect=="1" ? `${cellClass}--perfect` : ""}'>${commarise(score.maxcombo)}x</td>
3805 ${mapMode == 3 ?
3806 // Mania
3807 [makeZeroableEntry(score.countgeki),
3808 makeZeroableEntry(score.count300),
3809 makeZeroableEntry(score.countkatu),
3810 makeZeroableEntry(score.count100),
3811 makeZeroableEntry(score.count50)].join("") :
3812 mapMode == 1 ?
3813 // Taiko
3814 [makeZeroableEntry(score.count300),
3815 makeZeroableEntry(score.count100)].join("") :
3816 // Standard/CTB
3817 [makeZeroableEntry(score.count300),
3818 makeZeroableEntry(score.count100),
3819 makeZeroableEntry(score.count50)].join("")}
3820 ${makeZeroableEntry(score.countmiss)}
3821 <td class='${cellClass}${mapMode == 0 ? " ppcalc-pp" : ""}'>${parseFloat(score.pp).toFixed(settings.pp2dp ? 2 : 0)} <span></span></td>
3822 <td class='${cellClass} ${cellClass}--mods'><div class='mods mods--scoreboard'>${getNewMods(score.enabled_mods)}</div></td>
3823 <td class='${cellClass} datecol'>
3824 <time class='timeago' datetime='${dateset.toISOString()}'>${dateset.toLocaleString()}</time></td>
3825 <td class="beatmap-scoreboard-table__play-detail-menu"></td>
3826 <td class='op-ppcalc-data' hidden>${JSON.stringify(ppcalcData)}</td>
3827 </tr>`);
3828 //doesn't work on greasemonkey, unfixable?
3829 //ReactDOM.render(React.createElement(_exported.PlayDetailMenu, {score: newify(score)}), row.find(".beatmap-scoreboard-table__play-detail-menu")[0]);
3830
3831 //ppcalc, only for std
3832 if(mapMode == 0){
3833 row.find(".ppcalc-pp").click(function(event){
3834 var me = $(this);
3835 me.find("span").text("(...)");
3836 var ppcalcData = JSON.parse(me.parent().find(".op-ppcalc-data").text());
3837 getPpCalc(ppcalcData).then((result) => {
3838 if(result.rql == "ranked" || result.rql == "qualified"){
3839 me.find("span").text(`(${result.pp_fc} if FC)`);
3840 }else{
3841 me.html(`<span>${result.pp} (${result.pp_fc} if FC)</span>`);
3842 }
3843 });
3844 });
3845 }
3846 return row;
3847 }
3848
3849 function newify(score){
3850 score.replay = (score.replay_available == '1');
3851 score.user = {username: score.username};
3852 score.beatmap = {mode: intToMode(mapMode)};
3853 score.id = score.score_id;
3854 return score;
3855 }
3856
3857 function updateScoresTable(callback){
3858 var tableRows = [];
3859 var usedUsers = [];
3860 var rank = 0;
3861 for(var i=0; i<scoresResult.length; i++){
3862 var greyedout = false;
3863 if(inArray(usedUsers, scoresResult[i].user_id)){
3864 greyedout = true;
3865 }else{
3866 usedUsers.push(scoresResult[i].user_id);
3867 rank += 1;
3868 }
3869 var tableRow = makeScoreTableRow(scoresResult[i], rank, greyedout);
3870 tableRows[i] = tableRow;
3871 }
3872
3873 $(".osuplus-table.beatmap-scoreboard-table__table .beatmap-scoreboard-table__body-row").remove();
3874 $(".osuplus-table.beatmap-scoreboard-table__table .beatmap-scoreboard-table__body").append(tableRows);
3875 $(".timeago").timeago();
3876 //updateShowDate();
3877 tableLoadingNotice.hide();
3878 if(callback) callback();
3879 }
3880
3881 function putModButtons(){
3882 $("#mod-buttons").remove();
3883 function genModBtns(modArray){
3884 var modgroup = $("<div></div>").addClass("modIconGroup"),
3885 modgroupArr = [];
3886 for(var i=0; i<modArray.length; i++){
3887 var modinfo = modArray[i];
3888 var modimg;
3889 if(modinfo.mods.length === 1){
3890 modimg = $("<div></div>").addClass("modIcon")
3891 .append($("<img>").attr("src", modIconImgs[modinfo.mods[0]]))
3892 .attr("value", modinfo.mods[0]);
3893 }else{
3894 modimg = $("<div></div>").addClass("modIcon").append(
3895 $("<div></div>").addClass("modIconOption").append(
3896 $("<img>").attr("src", modIconImgs[modinfo.mods[0]])
3897 ),
3898 $("<div></div>").addClass("modIconOption").append(
3899 $("<img>").attr("src", modIconImgs[modinfo.mods[1]])
3900 )
3901 )
3902 .attr("value", modinfo.mods.join(","));
3903 }
3904 if(i > 0) modimg.hide();
3905
3906 if(modinfo.mods[0] === "NM"){
3907 modimg.click(nomodIconClick);
3908 modimg.addClass("nomodIcon");
3909 }else{
3910 modimg.click(modIconClick);
3911 }
3912
3913 if(modinfo.selection === 0){
3914 modimg.addClass("notSelected");
3915 modimg.attr("value", "XX");
3916 }else if(modinfo.selection === 1){
3917 modimg.addClass("isSelected");
3918 modimg.attr("value", modinfo.mods.join(','));
3919 }else{ // modinfo.selection === 2
3920 modimg.addClass("partialSelected");
3921 modimg.attr("value", ["XX"].concat(modinfo.mods).join(','));
3922 }
3923 modgroupArr.push(modimg);
3924 }
3925 modgroup.html(modgroupArr);
3926 return modgroup;
3927 }
3928
3929 $(".beatmap-scoreboard-table").before($("<div class='osuplus-table' id='mod-buttons'></div>").append(
3930 genModBtns([
3931 {mods: ["NM"], selection: 0},
3932 {mods: ["NM"], selection: 1}]),
3933 mapMode < 3 ? // HD for non-mania
3934 genModBtns([
3935 {mods: ["HD"], selection: 0},
3936 {mods: ["HD"], selection: 1},
3937 {mods: ["HD"], selection: 2}]) :
3938 genModBtns([ // FI, HD for mania
3939 {mods: ["FI"], selection: 0},
3940 {mods: ["FI"], selection: 1},
3941 {mods: ["HD"], selection: 1},
3942 {mods: ["FI", "HD"], selection: 1},
3943 {mods: ["FI", "HD"], selection: 2}]),
3944 genModBtns([
3945 {mods: ["HR"], selection: 0},
3946 {mods: ["HR"], selection: 1},
3947 {mods: ["HR"], selection: 2},
3948 {mods: ["EZ"], selection: 1},
3949 {mods: ["EZ"], selection: 2}]),
3950 genModBtns([
3951 {mods: ["DT"], selection: 0},
3952 {mods: ["DT"], selection: 1},
3953 {mods: ["NC"], selection: 1},
3954 {mods: ["DT", "NC"], selection: 1},
3955 {mods: ["HT"], selection: 1}]),
3956 genModBtns([
3957 {mods: ["SD"], selection: 0},
3958 {mods: ["SD"], selection: 1},
3959 {mods: ["PF"], selection: 1},
3960 {mods: ["SD", "PF"], selection: 1},
3961 {mods: ["SD", "PF"], selection: 2},
3962 {mods: ["NF"], selection: 1},
3963 {mods: ["NF"], selection: 2}]),
3964 genModBtns([
3965 {mods: ["FL"], selection: 0},
3966 {mods: ["FL"], selection: 1},
3967 {mods: ["FL"], selection: 2}]),
3968 mapMode < 3 ? [] : //mania keys
3969 genModBtns([
3970 {mods: ["4K"], selection: 0},
3971 {mods: ["4K"], selection: 1},
3972 {mods: ["5K"], selection: 1},
3973 {mods: ["6K"], selection: 1},
3974 {mods: ["7K"], selection: 1},
3975 {mods: ["8K"], selection: 1},
3976 {mods: ["9K"], selection: 1}]),
3977 mapMode > 0 ? [] : //SO only for standard
3978 [genModBtns([
3979 {mods: ["SO"], selection: 0},
3980 {mods: ["SO"], selection: 1},
3981 {mods: ["SO"], selection: 2}]),
3982 genModBtns([
3983 {mods: ["TD"], selection: 0},
3984 {mods: ["TD"], selection: 1},
3985 {mods: ["TD"], selection: 2}])]
3986 ));
3987 }
3988
3989 function nomodIconClick(){
3990 if(!modsEnabled) return;
3991 $(".modIcon").each(function(){
3992 $(this).hide();
3993 $(this).parent().children().first().show();
3994 });
3995 modIconClick.bind(this)();
3996 }
3997
3998 function modIconClick(){
3999 if(!modsEnabled) return;
4000 if(!$(this).hasClass("nomodIcon")){
4001 $(".nomodIcon").hide().parent().children().first().show();
4002 }
4003 var parent = $(this).parent();
4004 $(this).hide();
4005 if($(this).next().length === 0){
4006 parent.children().first().show();
4007 }else{
4008 $(this).next().show();
4009 }
4010
4011 timeoutUpdate();
4012 }
4013
4014 function timeoutUpdate(){
4015 if(timeoutID !== null){
4016 clearTimeout(timeoutID);
4017 }
4018 timeoutID = setTimeout(function(){
4019 timeoutID = null;
4020 updateModScores();
4021 }, timeDelay);
4022 }
4023
4024 function updateModScores(){
4025 clearScoresTable();
4026 abortReqs();
4027
4028 var modvals = getSelectedMods();
4029 var funs = [];
4030 scoresResult = [];
4031 for(var i=0; i<modvals.length; i++){
4032 var modval = modvals[i];
4033 if(modval < 0){
4034 funs.push(function(callback){
4035 getScoresWithPlayerInfo({b:mapID, m:mapMode, limit:settings.displayTopNum}, settings.showPpRank, function(response){
4036 scoresResult = scoresResult.concat(response);
4037 callback();
4038 }, scoreReqs);
4039 });
4040 }else{
4041 funs.push(function(modval){
4042 return function(callback){
4043 getScoresWithPlayerInfo({b:mapID, m:mapMode, limit:settings.displayTopNum, mods:modval}, settings.showPpRank, function(response){
4044 scoresResult = scoresResult.concat(response);
4045 callback();
4046 }, scoreReqs);
4047 };
4048 }(modval));
4049 }
4050 }
4051 doManyFunc(funs, function(){
4052 sortResult("score");
4053 scoresResult.splice(settings.displayTopNum);
4054 updateScoresTable();
4055 });
4056
4057 }
4058
4059 function getSelectedMods(){
4060 var selected = [[]];
4061 $(".modIcon:visible").each(function(){
4062 var modarray = $(this).attr("value").split(',');
4063 selected = cartesianProd(selected, modarray);
4064 });
4065
4066 // handle doublemods
4067 for(var si=0; si<selected.length; si++){
4068 for(var i=0; i<doublemods.length; i++){
4069 if(selected[si].indexOf(doublemods[i][0]) >= 0){
4070 if(selected[si].indexOf(doublemods[i][1]) < 0){
4071 selected[si].push(doublemods[i][1]);
4072 }
4073 }
4074 }
4075 }
4076
4077 var modvals = [];
4078 for(var si=0; si<selected.length; si++){
4079 var modval = 0;
4080 for(var i=0; i<modnames.length; i++){
4081 if(selected[si].indexOf(modnames[i].short) >= 0){
4082 modval += modnames[i].val;
4083 }
4084 }
4085 modvals.push(modval);
4086 }
4087 if(selected.length === 1 && modvals[0] === 0 && selected[0].indexOf("NM") < 0){ //get all scores
4088 return [-1];
4089 }
4090 return modvals;
4091 }
4092
4093 function putRankingType(){
4094 $(".beatmap-scoreboard-table").before(
4095 $("<div class='osuplus-table' id='rankingtype'></div>").append(
4096 $("<label></label>").append($("<input>").attr({type: "radio",
4097 name: "rankingtype",
4098 value: "global"})
4099 .prop("checked", true)
4100 .change(rankingTypeChanged),
4101 "Global"),
4102 $("<label></label>").append($("<input>").attr({type: "radio",
4103 name: "rankingtype",
4104 value: "friends"})
4105 .change(rankingTypeChanged),
4106 "Friends"),
4107 //Show date button
4108 $("<label></label>").append($("<input>").attr({type: "checkbox",
4109 id: "showdatebox"})
4110 .change(showDateChanged)
4111 .prop("checked", showDates),
4112 "Show date")
4113 )
4114 );
4115 }
4116
4117 function showDateChanged(){
4118 showDates = $("#showdatebox").prop("checked");
4119 updateShowDate();
4120 }
4121
4122 function updateShowDate(){
4123 if(showDates) $(".datecol").show();
4124 else $(".datecol").hide();
4125 }
4126
4127 function rankingTypeChanged(){
4128 var rankingType = $("input[name=rankingtype]:checked").val();
4129
4130 if(rankingType == "global"){
4131 modsEnabled = true;
4132 if(timeoutID !== null) clearTimeout(timeoutID);
4133 updateModScores();
4134 }else if(rankingType == "friends"){
4135 modsEnabled = false;
4136 if(timeoutID !== null) clearTimeout(timeoutID);
4137 updateFriendsScores();
4138 }
4139 }
4140
4141 function updateFriendsScores(){
4142 clearScoresTable();
4143 abortReqs();
4144
4145 // Make copy of friends including yourself
4146 var friends2 = friends.slice(0);
4147 if(!inArray(friends2, currentUser.id.toString())){
4148 friends2.push(currentUser.id.toString());
4149 }
4150
4151 var funs = [];
4152 for(var i=0; i<friends2.length; i++){
4153 funs.push(function(uid){
4154 return function(callback){
4155 getScoresWithPlayerInfo({b:mapID, u:uid, m:mapMode, type:"id"}, settings.showPpRank, function(response){
4156 if(response.length > 0){
4157 scoresResult.push(response[0]);
4158 }
4159 callback();
4160 }, scoreReqs);
4161 };
4162 }(friends2[i]));
4163 }
4164 doManyFunc(funs, function(){
4165 sortResult("score");
4166 updateScoresTable();
4167 });
4168 }
4169
4170 function addBloodcatMirror(){
4171 $(".beatmapset-header__buttons").append(
4172 `<a href="http://bloodcat.com/osu/s/${jsonBeatmapset.id}" data-turbolinks="false" class="btn-osu-big btn-osu-big--beatmapset-header js-beatmapset-download-link">
4173 <span class="btn-osu-big__content ">
4174 <span class="btn-osu-big__left">
4175 <span class="btn-osu-big__text-top">Bloodcat mirror</span>
4176 </span><span class="btn-osu-big__icon">
4177 <span class="fa-fw"><i class="fas fa-download"></i></span></span></span></a>`
4178 );
4179 }
4180
4181 async function addSubBtn(){
4182 subscribed = await subscriberManager.map.isSubscribed(jsonBeatmapset.id.toString());
4183 $(".beatmapset-header__buttons").append(
4184 `<a class='btn-osu-big btn-osu-big--beatmapset-header sub-button' title='Subscribe map'>
4185 <span class='btn-osu-big__content'>
4186 <span class='btn-osu-big__left'>
4187 <span class="btn-osu-big__text-top">Subscribe</span>
4188 </span>
4189 <span class="btn-osu-big__icon">
4190 <span class="fa-fw"><i class="far fa-heart"></i></span>
4191 </span>
4192 </span>
4193 </a>`
4194 );
4195 $(".sub-button").click(function(){
4196 if(subscribed){
4197 subscriberManager.map.remove(jsonBeatmapset.id.toString()).then(() => {
4198 subscribed = false;
4199 refreshSubImg(subscribed);
4200 });
4201 }else{
4202 subscriberManager.map.add(jsonBeatmapset.id.toString(), jsonBeatmapset.artist, jsonBeatmapset.title, jsonBeatmapset.creator, jsonBeatmapset.user_id.toString()).then(() => {
4203 subscribed = true;
4204 refreshSubImg(subscribed);
4205 });
4206 }
4207 });
4208 refreshSubImg(subscribed);
4209 }
4210
4211 function refreshSubImg(subscribed){
4212 if(subscribed){
4213 $(".sub-button").addClass("subbed");
4214 $(".sub-button").find(".btn-osu-big__text-top").text("Unsubscribe");
4215 }else{
4216 $(".sub-button").removeClass("subbed");
4217 $(".sub-button").find(".btn-osu-big__text-top").text("Subscribe");;
4218 }
4219 }
4220
4221 function addOsuPreview(){
4222 $(".beatmapset-info").after(
4223 $("<div class='osupreview-container osuplus-header'><div class='js-spoilerbox bbcode-spoilerbox'>\
4224 <a class='js-spoilerbox__link bbcode-spoilerbox__link' href='#'><i class='fas fa-chevron-right bbcode-spoilerbox__arrow'></i>osu!preview</a>\
4225 <div class='bbcode-spoilerbox__body'><div id='osupreview'></div></div></div>"
4226 ).click(function(){
4227 var osupreviewEle = $(this).find("#osupreview");
4228 if(osupreviewEle.data("loaded")) return;
4229 osupreviewEle.html(
4230 `If below doesn't work, <a href='http://bloodcat.com/osu/preview.html#${mapID}' target='_blank'>open in new tab</a><br>
4231 <iframe class='osupreview' src='https://bloodcat.com/osu/preview.html#${mapID}' allowfullscreen></iframe>`
4232 );
4233 osupreviewEle.data("loaded", true)
4234 })
4235 );
4236 }
4237
4238 function addSlider(){
4239 $(".beatmap-scoreboard-table").before(
4240 createSlider(function(val, checked){
4241 var rows = $(".osuplus-table.beatmap-scoreboard-table__table .beatmap-scoreboard-table__body-row");
4242 if(checked){
4243 var curTime = new Date();
4244 rows.each(function(index, row){
4245 row = $(row);
4246 var scoreTime = new Date(row.find("time").attr("datetime"));
4247 var diff = (curTime - scoreTime) / (1000*60*60*24);
4248 if(diff < val){
4249 row.addClass("recentscore");
4250 }else{
4251 row.removeClass("recentscore");
4252 }
4253 });
4254 }else{
4255 rows.removeClass("recentscore");
4256 }
4257 }).addClass("osuplus-table")
4258 );
4259 }
4260
4261 function abortReqs(){
4262 while(scoreReqs.length){
4263 scoreReqs.pop().abort();
4264 }
4265 }
4266
4267 function sortResult(sortby){
4268 if(sortby === "score"){
4269 scoresResult.sort(function(a,b){
4270 var ascore = parseInt(a.score),
4271 bscore = parseInt(b.score);
4272 if(ascore < bscore){
4273 return 1;
4274 }else if(ascore > bscore){
4275 return -1;
4276 }else{
4277 return getTime(a.date) - getTime(b.date);
4278 }
4279 });
4280 }else if(sortby === "pp"){
4281 scoresResult.sort(function(a,b){
4282 var ascore = parseFloat(a.pp),
4283 bscore = parseFloat(b.pp);
4284 if(ascore < bscore){
4285 return 1;
4286 }else if(ascore > bscore){
4287 return -1;
4288 }else{
4289 ascore = parseFloat(a.score);
4290 bscore = parseFloat(b.score);
4291 if(ascore < bscore){
4292 return 1;
4293 }else if(ascore > bscore){
4294 return -1;
4295 }else{
4296 return getTime(a.date) - getTime(b.date);
4297 }
4298 }
4299 });
4300 }
4301 }
4302
4303 function addTableLoadingNotice(){
4304 var tableLoadingNotice = $(`<div class='osuplus-table' id='table-loading-notice'><img src='${loaderImg}' class='centered'></div>`);
4305 $(".beatmap-scoreboard-table__table").before(tableLoadingNotice);
4306 return tableLoadingNotice;
4307 }
4308
4309 function modifyTableHeaders(){
4310 // Add click scores/pp to sort
4311 $(".osuplus-table.beatmap-scoreboard-table__table .beatmap-scoreboard-table__header--score").text("").append(
4312 $("<a>Score</a>")
4313 .click(function(){
4314 sortResult("score");
4315 updateScoresTable();
4316 })
4317 );
4318 $(".osuplus-table.beatmap-scoreboard-table__table .beatmap-scoreboard-table__header--pp").text("").append(
4319 $("<a>pp</a>")
4320 .click(function(){
4321 sortResult("pp");
4322 updateScoresTable();
4323 })
4324 );
4325
4326 // Add date column
4327 $(".osuplus-table.beatmap-scoreboard-table__table thead tr").children().last().before("<th class='beatmap-scoreboard-table__header beatmap-scoreboard-table__header--date datecol'>Date</th>");
4328 $(".search-beatmap-scoreboard-table__table thead tr").children().last().before("<th class='beatmap-scoreboard-table__header beatmap-scoreboard-table__header--date datecol'>Date</th>");
4329
4330 // Add max combo
4331 $(".osuplus-table .beatmap-scoreboard-table__header--maxcombo").text(`Max Combo${maxCombo ? ` (${maxCombo})` : ""}`);
4332 }
4333
4334 function clearScoresTable(){
4335 scoresResult = [];
4336 updateScoresTable(function(){
4337 tableLoadingNotice.show();
4338 });
4339 }
4340
4341 function addSearchUser(){
4342 $(".beatmap-scoreboard-table").before(
4343 $("<div class='osuplus-table'></div>").attr("id", "searchuser")
4344 .append(
4345 $("<strong>Search user: </strong>"),
4346 $("<input>").attr({type: "text",
4347 id: "searchusertxt"})
4348 .val(currentUser.username)
4349 .bind("enterKey", searchUserEnter)
4350 .keyup(function(e){
4351 if(e.keyCode == 13)
4352 {
4353 $(this).trigger("enterKey");
4354 }
4355 }),
4356 $("<div></div>").attr("id", "searchuserinfo").text("Searching...").hide(),
4357 $("<div></div>").attr("class", "search-beatmap-scoreboard-table")
4358 .attr("id", "searchuserresult")
4359 .append(
4360 $("<table class='search-beatmap-scoreboard-table__table'></table>").append(
4361 $("<thead></thead>").append(
4362 $(".beatmap-scoreboard-table__table thead tr").clone()
4363 )
4364 ).append("<tbody class='beatmap-scoreboard-table__body'></tbody>")
4365 ).hide()
4366 )
4367 );
4368 }
4369
4370 function searchUserEnter(){
4371 $("#searchuserinfo").text("Searching...").show();
4372 $("#searchuserresult").hide();
4373 var searchusernames = $("#searchusertxt").val().split(',');
4374 var promises = searchusernames.map((username) => new Promise(function(resolve, reject){
4375 getScoresWithPlayerInfo({b:mapID, u:username, m:mapMode, type:"string"}, settings.showPpRank, resolve);
4376 }));
4377 Promise.all(promises).then((responses) => {
4378 var response = [];
4379 for(let r of responses){
4380 response = response.concat(r);
4381 }
4382 response.sort((a,b) => parseInt(b.score) - parseInt(a.score));
4383 if(response.length > 0){
4384 $("#searchuserresult .beatmap-scoreboard-table__body").children().remove();
4385 response.forEach(function(score, index){
4386 var tableRow = makeScoreTableRow(score, index+1);
4387 $("#searchuserresult .beatmap-scoreboard-table__body").append(tableRow);
4388 });
4389
4390 $(".timeago").timeago();
4391 $("#searchuserinfo").hide();
4392 $("#searchuserresult").show();
4393 }else{
4394 $("#searchuserinfo").text("No scores found :(");
4395 }
4396 });
4397 }
4398
4399 function isFriend(uid){
4400 var friends = currentUser.friends;
4401 for(var i=0; i<friends.length; i++){
4402 if(friends[i].target_id.toString() === uid) return true;
4403 }
4404 return false;
4405 }
4406
4407 function minePlayerCountries(){
4408 $(".beatmap-scoreboard-table__body").children().each(function(index, ele){
4409 var country = $(ele).children().eq(4).children().first().attr("href").split("=")[1].toLowerCase();
4410 var uid = $(ele).children().eq(5).children().first().attr("href").split("/")[2]
4411 savePlayerCountry(uid, country);
4412 });
4413 }
4414
4415 return {init: init, destroy: destroy};
4416 }
4417
4418
4419 function getPlayerCountry(playerid, callback){
4420 if(playerid in playerCountries){
4421 callback(playerCountries[playerid]);
4422 }else{
4423 if(settings.fetchPlayerCountries){
4424 getUser({u:playerid, type:"id"}, function(response){
4425 if(response.length){
4426 var country = response[0].country.toLowerCase();
4427 }else{
4428 var country = "xx";
4429 }
4430 savePlayerCountry(playerid, country);
4431 callback(country);
4432 });
4433 }else{
4434 callback("");
4435 }
4436 }
4437 }
4438
4439 function savePlayerCountry(playerid, country){
4440 playerCountries[playerid] = country;
4441 }
4442
4443 function createSlider(valueChange){
4444 var slider, sliderLbl, sliderCB;
4445 function getValue(){
4446 var rawval = slider.val();
4447 if(rawval <= 7){
4448 return rawval;
4449 }else if(rawval <= 9){
4450 return (rawval - 6) * 7;
4451 }else{
4452 return (rawval - 9) * 30;
4453 }
4454 }
4455
4456 var changeFun = function(){
4457 sliderLbl.text(getValue());
4458 valueChange(getValue(), sliderCB.prop("checked"));
4459 }
4460
4461 slider = $("<input type='range' min=1 max=21 id='opslider' value=7>").on("input", changeFun);
4462 sliderLbl = $("<span>7</span>");
4463 sliderCB = $("<input type=checkbox id='topcb'/>").change(changeFun);
4464 return $("<div></div>").append(
4465 slider,
4466 $("<label></label>").append(
4467 sliderCB, "Highlight past ", sliderLbl, " days"
4468 )
4469 );
4470 }
4471
4472 function displayGetKey(){
4473 $(document.body).prepend($("<div style='text-align: center; background-color: red;'></div>")
4474 .append($("<h1 style='color: white;'></h1>")
4475 .attr("id", "osuplusnotice")
4476 .append("[osuplus] Click ",
4477 $("<a>here</a>").click(promptKey),
4478 " to use your osu!API key.<br>"+
4479 "Don't have API key? Get from ",
4480 $("<a href='/p/api'>here</a>"))
4481 )
4482 );
4483 }
4484
4485 function promptKey(){
4486 var yourKey = prompt("Enter your API key");
4487 if(yourKey !== null){
4488 testKey(yourKey, function(islegit){
4489 if(islegit){
4490 GMX.setValue("apikey", yourKey).then(() => {
4491 apikey = yourKey;
4492 hasKey = true;
4493 alert("API key worked! Your page will now refresh");
4494 location.reload();
4495 });
4496 }else{
4497 alert("API key failed :(");
4498 }
4499 });
4500 }
4501 }
4502
4503 function testKey(key, callback){
4504 var url = "https://osu.ppy.sh/api/get_user?k=" + key;
4505 GetPage(url, function(response){
4506 var jresponse = JSON.parse(response);
4507 if("error" in jresponse){
4508 callback(false);
4509 }else{
4510 callback(true);
4511 }
4512 });
4513 }
4514
4515 function getModsArray(modnum){
4516 modnum = parseInt(modnum);
4517 var mods = [];
4518 for(let mod of modnames){
4519 if(mod.val & modnum){
4520 mods.push(mod)
4521 }
4522 }
4523 // handle doublemods
4524 for(var i=0; i<doublemods.length; i++){
4525 for(var j=0; j<mods.length; j++){
4526 if(mods[j].short === doublemods[i][0]){
4527 for(var k=0; k<mods.length; k++){
4528 if(mods[k].short === doublemods[i][1]){
4529 mods.splice(k, 1);
4530 break;
4531 }
4532 }
4533 break;
4534 }
4535 }
4536 }
4537 return mods;
4538 }
4539
4540 function getMods(modnum){
4541 var modsArray = getModsArray(modnum);
4542 if(modsArray.length === 0) return "None";
4543 else{
4544 return modsArray.map(function(mod){ return mod.short; }).join(",");
4545 }
4546 }
4547
4548 function getNewMods(modnum){
4549 var modsArray = getModsArray(modnum);
4550 var modsHtml = modsArray.map(function(mod){
4551 return `<div class='mods__mod'><div class='mods__mod-image'><div class='mod mod--${mod.short}' title='${mod.name}'></div></div></div>`;
4552 })
4553 return modsHtml.join("");
4554 }
4555
4556 function calcAcc(score, mode){
4557 var c50 = parseInt(score.count50),
4558 c100 = parseInt(score.count100),
4559 c300 = parseInt(score.count300),
4560 cmiss = parseInt(score.countmiss),
4561 cgeki = parseInt(score.countgeki),
4562 ckatu = parseInt(score.countkatu);
4563 if(mode === 0){
4564 // Standard
4565 return 100.0 * (6*c300 + 2*c100 + c50) / (6*(c50 + c100 + c300 + cmiss));
4566 }else if(mode === 1){
4567 // Taiko
4568 return 100.0 * (2*c300 + c100) / (2*(c300 + c100 + cmiss));
4569 }else if(mode === 2){
4570 //CTB
4571 return 100.0 * (c300 + c100 + c50) / (c300 + c100 + c50 + ckatu + cmiss);
4572 }else{
4573 //Mania
4574 return 100.0 * (6*cgeki + 6*c300 + 4*ckatu + 2*c100 + c50) / (6*(c50 + c100 + c300 + cmiss + cgeki + ckatu));
4575 }
4576 }
4577
4578 function getTime(datestring){
4579 return new Date(datestring).getTime();
4580 }
4581
4582 function getCountryUrl(country){
4583 return "//s.ppy.sh/images/flags/" + country + ".gif";
4584 }
4585
4586 function commarise(num){
4587 num = num.toString();
4588 var numarray = [];
4589 while(num !== ""){
4590 numarray.push(num.slice(-3));
4591 num = num.slice(0,-3);
4592 }
4593 return numarray.reverse().join(",");
4594 }
4595
4596 function secsToMins(secs){
4597 var s = "00"+(secs % 60);
4598 return (secs/60>>0) + ":" + s.substr(s.length-2);
4599 }
4600
4601 function getRankImg(rank){
4602 var ranksrc;
4603 if(rank === "F"){
4604 ranksrc = FImg;
4605 }else{
4606 ranksrc = "//s.ppy.sh/images/" + rank + "_small.png";
4607 }
4608 return "<img src='" + ranksrc + "'>";
4609 }
4610
4611 function modeToInt(mode){
4612 switch(mode){
4613 case "osu":
4614 return 0;
4615 case "taiko":
4616 return 1;
4617 case "fruits":
4618 return 2;
4619 case "mania":
4620 return 3;
4621 default:
4622 return 0;
4623 }
4624 }
4625
4626 function intToMode(modeint){
4627 switch(modeint){
4628 case 0:
4629 return "osu";
4630 case 1:
4631 return "taiko";
4632 case 2:
4633 return "fruits";
4634 case 3:
4635 return "mania";
4636 default:
4637 return "osu";
4638 }
4639 }
4640
4641 function getPpCalc(params){
4642 /* see https://osu-pp-calc-api.glitch.me/
4643 id - beatmap id
4644 m - mods number
4645 c - combo
4646 acc - accuracy
4647 miss - number of misses
4648 */
4649 var promise = new Promise((resolve, reject) => {
4650 var ppcalcurl = getUrl("https://osu-pp-calc-api.glitch.me", params);
4651 getRequest(ppcalcurl, function(response){
4652 var ans = {
4653 pp: parseFloat(response.pp),
4654 pp_fc: parseFloat(response.pp_fc),
4655 rql: response.rql // ranked|qualified|loved|pending|graveyard etc
4656 };
4657 resolve(ans);
4658 });
4659 });
4660 return promise;
4661 }
4662
4663 function getRequest(url, callback, reqTracker){
4664 // reqTracker is a list of requests to keep track whether the request is ongoing, to allow abortion
4665 if(reqTracker){
4666 var req = persistGetPageJSON(url, function(response){
4667 removeFromArray(reqTracker, req);
4668 callback(response);
4669 });
4670 reqTracker.push(req);
4671 }else{
4672 persistGetPageJSON(url, callback);
4673 }
4674 }
4675
4676 function getScoresWithPlayerInfo(params, showPpRank, callback, reqTracker){
4677 var mode = params.m || 0;
4678 getScores(params, function(response){
4679 var funs = [];
4680 response.forEach(function(score, index){
4681 if(showPpRank){
4682 funs.push(function(donecb){
4683 getUser({u: score.user_id, type: "id", m: mode}, function(userInfo){
4684 response[index].user = userInfo[0];
4685 savePlayerCountry(score.user_id, userInfo[0].country)
4686 donecb();
4687 }, reqTracker);
4688 });
4689 }else{
4690 funs.push(function(donecb){
4691 getPlayerCountry(score.user_id, function(country){
4692 response[index].user = {country: country};
4693 donecb();
4694 });
4695 });
4696 }
4697 });
4698 doManyFunc(funs, function(){
4699 callback(response);
4700 })
4701 }, reqTracker);
4702 }
4703
4704 function getUserRecent(params, callback, reqTracker){
4705 /*
4706 k - api key (required).
4707 u - specify a user_id or a username to return recent plays from (required).
4708 m - mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania). Optional, default value is 0.
4709 limit - amount of results (range between 1 and 50 - defaults to 10).
4710 type - specify if u is a user_id or a username. Use string for usernames or id for user_ids. Optional, default behavior is automatic recognition (may be problematic for usernames made up of digits only).
4711 */
4712 var url = "https://osu.ppy.sh/api/get_user_recent";
4713 params.k = apikey;
4714 getRequest(getUrl(url, params), callback, reqTracker);
4715 }
4716
4717 function getReplay(params, callback, reqTracker){
4718 /*
4719 k - api key (required).
4720 m - the mode the score was played in (required).
4721 b - the beatmap ID (not beatmap set ID!) in which the replay was played (required).
4722 u - the user that has played the beatmap (required).
4723 */
4724 var url = "https://osu.ppy.sh/api/get_replay";
4725 params.k = apikey;
4726 getRequest(getUrl(url, params), callback, reqTracker);
4727 }
4728
4729 function getBeatmaps(params, callback, reqTracker){
4730 /*
4731 k - api key (required).
4732 since - return all beatmaps ranked since this date. Must be a MySQL date.
4733 s - specify a beatmapset_id to return metadata from.
4734 b - specify a beatmap_id to return metadata from.
4735 u - specify a user_id or a username to return metadata from.
4736 type - specify if u is a user_id or a username. Use string for usernames or id for user_ids. Optional, default behaviour is automatic recognition (may be problematic for usernames made up of digits only).
4737 m - mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania). Optional, maps of all modes are returned by default.
4738 a - specify whether converted beatmaps are included (0 = not included, 1 = included). Only has an effect if m is chosen and not 0. Converted maps show their converted difficulty rating. Optional, default is 0.
4739 h - the beatmap hash. It can be used, for instance, if you're trying to get what beatmap has a replay played in, as .osr replays only provide beatmap hashes (example of hash: a5b99395a42bd55bc5eb1d2411cbdf8b). Optional, by default all beatmaps are returned independently from the hash.
4740 limit - amount of results. Optional, default and maximum are 500.
4741 */
4742 var url = "https://osu.ppy.sh/api/get_beatmaps";
4743 params.k = apikey;
4744 getRequest(getUrl(url, params), callback, reqTracker);
4745 }
4746
4747 function getScores(params, callback, reqTracker){
4748 /*
4749 k - api key (required).
4750 b - specify a beatmap_id to return score information from (required).
4751 u - specify a user_id or a username to return score information for.
4752 m - mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania). Optional, default value is 0.
4753 mods - specify a mod or mod combination (See the bitwise enum)
4754 type - specify if u is a user_id or a username. Use string for usernames or id for user_ids. Optional, default behaviour is automatic recognition (may be problematic for usernames made up of digits only).
4755 limit - amount of results from the top (range between 1 and 100 - defaults to 50).
4756 */
4757 var url = "https://osu.ppy.sh/api/get_scores";
4758 params.k = apikey;
4759 getRequest(getUrl(url, params), callback, reqTracker);
4760 }
4761
4762 function getUser(params, callback, reqTracker){
4763 /*
4764 k - api key (required).
4765 u - specify a user_id or a username to return metadata from (required).
4766 m - mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania). Optional, default value is 0.
4767 type - specify if u is a user_id or a username. Use string for usernames or id for user_ids. Optional, default behaviour is automatic recognition (may be problematic for usernames made up of digits only).
4768 event_days - Max number of days between now and last event date. Range of 1-31. Optional, default value is 1.
4769 */
4770 var url = "https://osu.ppy.sh/api/get_user";
4771 params.k = apikey;
4772 getRequest(getUrl(url, params), callback, reqTracker);
4773 }
4774
4775 function getUserBest(params, callback, reqTracker){
4776 /*
4777 k - api key (required).
4778 u - specify a user_id or a username to return best scores from (required).
4779 m - mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania). Optional, default value is 0.
4780 limit - amount of results (range between 1 and 100 - defaults to 10).
4781 type - specify if u is a user_id or a username. Use string for usernames or id for user_ids. Optional, default behavior is automatic recognition (may be problematic for usernames made up of digits only).
4782 */
4783 var url = "https://osu.ppy.sh/api/get_user_best";
4784 params.k = apikey;
4785 getRequest(getUrl(url, params), callback, reqTracker);
4786 }
4787
4788 function getUrl(url, params){
4789 if(params){
4790 var paramarray = [];
4791 for(var k in params){
4792 paramarray.push(k + "=" + encodeURIComponent(params[k]));
4793 }
4794 return url + "?" + paramarray.join("&");
4795 }else{
4796 return url;
4797 }
4798 }
4799
4800 function persistGetPageJSON(url, callback){
4801 var rtn = {};
4802 function repeatGetPage(url, callback){
4803 var reqobj = GetPage(url, function(response){
4804 try{
4805 var responseJSON = JSON.parse(response);
4806 }catch(e){ // if server having problems and not returning json
4807 var timeoutID = setTimeout(function(){
4808 repeatGetPage(url, callback);
4809 }, 5000);
4810 rtn.abort = function(){
4811 clearTimeout(timeoutID);
4812 };
4813 }
4814 callback(responseJSON);
4815 });
4816 rtn.abort = reqobj.abort.bind(reqobj);
4817 }
4818 repeatGetPage(url, callback);
4819 return rtn;
4820 }
4821
4822 function GetPage(url, callback) {
4823 if(debug) console.log(url);
4824 return abortableXHR({
4825 method: "GET",
4826 url: url,
4827 synchronous: false,
4828 timeout: 0,
4829 onload: function (resp) {
4830 callback(resp.responseText);
4831 },
4832 ontimeout: function () {
4833 callback(null);
4834 }
4835 });
4836 }
4837
4838 function abortableXHR(details){
4839 var aborted = false;
4840 var abortableOnload = (resp) => {
4841 if(!aborted && details.onload){
4842 details.onload(resp);
4843 }
4844 };
4845 var abortableOntimeout = (resp) => {
4846 if(!aborted && details.ontimeout){
4847 details.ontimeout(resp);
4848 }
4849 };
4850 GMX.xmlHttpRequest({
4851 method: details.method,
4852 url: details.url,
4853 synchronous: details.synchronous,
4854 timeout: details.timeout,
4855 onload: abortableOnload,
4856 ontimeout: abortableOntimeout
4857 });
4858 return {abort: () => { aborted = true; }};
4859 }
4860
4861 //-------------------------------
4862 // javascript helper functions
4863 //-------------------------------
4864
4865 function waitForEl(selector, callback){
4866 var poller = setInterval(() => {
4867 var el = $(selector);
4868 if(el.length){
4869 clearInterval(poller);
4870 callback(el);
4871 }
4872 }, 100);
4873 return {abort: () => { clearInterval(poller); }};
4874 }
4875
4876 function inArray(arr, ele){
4877 for(var i=0; i<arr.length; i++){
4878 if(arr[i] == ele) return true;
4879 }
4880 return false;
4881 }
4882
4883 function removeFromArray(arr, ele){
4884 var _index = arr.indexOf(ele);
4885 if(_index > -1) arr.splice(_index, 1);
4886 }
4887
4888 // returns [a1.push(e) for a1 in arr1, e in arr2]
4889 function cartesianProd(arr1, arr2){
4890 var ans = [];
4891 for(var i=0; i<arr1.length; i++){
4892 for(var j=0; j<arr2.length; j++){
4893 ans.push(arr1[i].concat([arr2[j]]));
4894 }
4895 }
4896 return ans;
4897 }
4898
4899 // runs array of functions funs asynchronously and calls finalcallback() when all are done
4900 // each fun in funs must be function(callback) and must call callback() when done
4901 function doManyFunc(funs, finalcallback){
4902 var done = 0, total = funs.length;
4903 for(var i=0; i<total; i++){
4904 funs[i](function(){
4905 done++;
4906 if(done == total){
4907 finalcallback();
4908 }
4909 });
4910 }
4911 if(total === 0){
4912 finalcallback();
4913 }
4914 }
4915
4916 function downloadFile(data, filename){
4917 var link = document.createElement("a");
4918 link.download = filename;
4919 link.href = "data:application/octet-stream;base64," + btoa(data);
4920 link.dispatchEvent(new MouseEvent("click"));
4921 }
4922})();