├── README.md └── osugame_func+.user.js /README.md: -------------------------------------------------------------------------------- 1 | # osugame_func+ 2 | Userscript adding osu! related functionality to /r/osugame 3 | 4 | ## Features 5 | * Turns URLs in user flairs to clickable links 6 | * Displays player name and pp/rank on flair hover 7 | * Adds live twitch streams to the sidebar 8 | * Adds song preview buttons to beatmap /s/ links 9 | * Adds Bloodcat preview buttons to beatmap /b/ links 10 | * Converts beatmap download links to chosen mirror (osu!direct or Bloodcat) 11 | 12 | ## Screenshots 13 | ![flairs](http://i.imgur.com/vqFPUPK.png) ![streams](http://i.imgur.com/VWOJuwr.png) ![previews](https://i.imgur.com/v5JOXy0.png) 14 | 15 | ## Installation 16 | 1. You will need a userscript manager add-on for your browser such as Greasemonkey/Scriptish for Firefox, or Tampermonkey for Chrome/Chromium. 17 | 2. To install this script, [click here](https://github.com/v0x76/osugame_funcp/raw/master/osugame_func+.user.js) or open the raw data of the script. 18 | 3. Done! For updates, the same link can be opened from the User Script Commands menu, within the menu of your userscript add-on (click on the icon). 19 | 20 | ## Other info 21 | You can toggle features in the User Script Commands menu of your userscript manager add-on. Here you can also download the latest version of the script. 22 | -------------------------------------------------------------------------------- /osugame_func+.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name osugame_func+ 3 | // @namespace /r/osugame 4 | // @author /u/N3G4 5 | // @description Adds osu! related functionality to /r/osugame 6 | // @include *reddit.com/r/osugame* 7 | // @version 1.5.10 8 | // @require https://openuserjs.org/src/libs/sizzle/GM_config.js 9 | // @run-at document-end 10 | // @grant GM_openInTab 11 | // @grant GM_addStyle 12 | // @grant GM_registerMenuCommand 13 | // @grant GM_getValue 14 | // @grant GM_setValue 15 | // @grant GM_log 16 | // @grant GM_xmlhttpRequest 17 | // ==/UserScript== 18 | 19 | // ~ ~~ Features ~~ ~ 20 | // * Turns URLs in user flairs to clickable links 21 | // * Displays player name and pp/rank on flair hover 22 | // * Adds live twitch streams to the sidebar 23 | // * Adds song preview buttons to beatmap /s/ links 24 | // * Adds Bloodcat preview buttons to beatmap /b/ links 25 | // * Converts beatmap download links to chosen mirror 26 | // 27 | // ~ ~~~~ ~ 28 | 29 | var githubURL = "https://github.com/v0x76/osugame_funcp"; 30 | var mirrors = { 31 | "osu!direct": "osu://dl/", 32 | "Bloodcat": "http://bloodcat.com/osu/s/" 33 | }; 34 | var twitchkey = "nerivr8xh8fff696oyomj0ghlxqnvtb"; 35 | 36 | var g_flairs, g_streams, g_refreshstreams, g_refreshrate, g_songs, g_downloadmirror, g_parallax, g_debug; 37 | 38 | var loadingimg = "data:image/gif;base64,R0lGODlhCQAJAIAAAP///////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgABACwAAAAACQAJAAACCYyPBpvtD6OMBQAh+QQJCgABACwAAAAACQAJAAACCoyPCKl866KctAAAIfkECQoAAQAsAAAAAAkACQAAAgqMj6mrwL4gm7MAACH5BAkKAAEALAAAAAAJAAkAAAIJjI+py73wogsFACH5BAkKAAEALAAAAAAJAAkAAAIJjI+py+0P4gsFACH5BAkKAAEALAAAAAAJAAkAAAIKjI+pyx3QIJPOFAAh+QQJCgABACwAAAAACQAJAAACCoyPqQsbzZycqwAAIfkECQoAAQAsAAAAAAkACQAAAgqMjwapHOuinJQVADs="; 39 | 40 | function setupConfig() { 41 | GM_config.init({ 42 | "id": "ofpconf", 43 | "title": "osugame_func+ settings", 44 | "fields": 45 | { 46 | "flairs": { 47 | "label": "Player info fetching", 48 | "type": "checkbox", 49 | "default": true 50 | }, 51 | 52 | "streams": { 53 | "label": "Live twitch streams", 54 | "type": "checkbox", 55 | "default": true 56 | }, 57 | 58 | "refreshstreams": { 59 | "label": "Automatically refresh stream list", 60 | "type": "checkbox", 61 | "default": false 62 | }, 63 | 64 | "refreshrate": { 65 | "label": "Stream list refresh rate (seconds)", 66 | "type": "int", 67 | "min": 1, 68 | "max": 600, 69 | "default": 20 70 | }, 71 | 72 | "songs": { 73 | "label": "Add preview buttons to beatmap links", 74 | "type": "checkbox", 75 | "default": true 76 | }, 77 | 78 | "downloadmirror": { 79 | "label": "Beatmap download links", 80 | "type": "select", 81 | "options": ["Default"].concat(Object.keys(mirrors)), 82 | "default": "Default" 83 | }, 84 | 85 | "parallax": { 86 | "label": "Parallax effect in header", 87 | "type": "checkbox", 88 | "default": true 89 | }, 90 | 91 | "debug": { 92 | "label": "Debug mode", 93 | "type": "checkbox", 94 | "default": false 95 | } 96 | }, 97 | "css": "#ofpconf { background-color: #F6F6FE !important; }" + 98 | "#ofpconf .config_header { color: #369 !important; }" 99 | }); 100 | 101 | g_flairs = GM_config.get("flairs"); 102 | g_streams = GM_config.get("streams"); 103 | g_refreshstreams = GM_config.get("refreshstreams"); 104 | g_refreshrate = GM_config.get("refreshrate"); 105 | g_songs = GM_config.get("songs"); 106 | g_downloadmirror = GM_config.get("downloadmirror"); 107 | g_parallax = GM_config.get("parallax"); 108 | g_debug = GM_config.get("debug"); 109 | 110 | GM_registerMenuCommand("Open settings", function(){ GM_config.open(); }, "s"); 111 | GM_registerMenuCommand("Open github repo", function(){ GM_openInTab(githubURL); }, "g"); 112 | GM_registerMenuCommand("DL latest version of script", 113 | function(){ GM_openInTab(githubURL + "/raw/master/osugame_func+.user.js"); }, "v"); 114 | } 115 | 116 | function makeStylesheet() { 117 | GM_addStyle( 118 | "#ofpconf { border-radius: 2px !important; border-color: #DDD !important; " + 119 | "box-shadow: 0 3px 12px rgba(85, 85, 85, 0.1); }" + 120 | "#ofp-infobox { position: absolute; padding: 2px 5px; " + 121 | "background-color: #A9A9FF; opacity: 0.9; color: #FFF; " + 122 | "font-size: 11px; }" + 123 | "#ofp-infobox::before { content: ''; position: absolute; " + 124 | "bottom: -8px; font-size: 0; opacity: 0.9; " + 125 | "border-style: solid; border-width: 4px; " + 126 | "border-color: #A9A9FF transparent transparent; }" + 127 | ".ofp-streaminfo { width: 100%; min-height: 45px; " + 128 | "margin: 4px 0; padding: 2px; background: #EEF; " + 129 | "color: #888; }" + 130 | ".ofp-streaminfo img { float: left; margin-right: 4px; }" + 131 | ".ofp-streaminfo strong { color: #369; font-size: 12px; }" + 132 | ".ofp-streaminfo p { margin: 3px 0 0 !important; }" + 133 | ".ofp-streaminfo span { color: #333; }" + 134 | ".ofp-preview { cursor: pointer; font-size: 0.7em; }" + 135 | "#ofp-iframe { border: solid 1px #DDD; border-radius: 2px; " + 136 | "box-shadow: 0 3px 12px rgba(85, 85, 85, 0.1); }" + 137 | "#ofp-iframe-btn { float: right; padding: 1px 4px 1px 6px; " + 138 | "background-color: #F6F6FE; border-style: solid; " + 139 | "border-width: 1px 1px 1px 0; border-color: #DDD; " + 140 | "border-radius: 0 2px 2px 0; font-size: 13px; font-weight: 600; " + 141 | "color: #222; box-shadow: 0 3px 12px rgba(85, 85, 85, 0.1); " + 142 | "cursor: pointer; }" 143 | ); 144 | } 145 | 146 | // take sum of offsets up the node tree 147 | function getXYPos(element) { 148 | var leftval = 0; 149 | var topval = 0; 150 | while(element) { 151 | leftval += element.offsetLeft; 152 | topval += element.offsetTop; 153 | element = element.offsetParent; 154 | } 155 | 156 | return { 157 | x: leftval, 158 | y: topval 159 | }; 160 | } 161 | 162 | function Flairbox() { 163 | var timer; 164 | var waiting = false; 165 | 166 | var clickify = function(flair) { 167 | var flairtext = flair.innerHTML; 168 | flair.innerHTML = flairtext.link(flairtext); 169 | flair.firstChild.title = ""; // prevent tooltip getting in the way 170 | }; 171 | 172 | var createInfoBox = function() { 173 | var html = ""; 174 | var newelement = document.createElement("div"); 175 | newelement.innerHTML = html; 176 | document.getElementsByTagName("body")[0].appendChild(newelement.firstChild); 177 | }; 178 | 179 | var setHover = function(flair) { 180 | var box = document.getElementById("ofp-infobox"); 181 | flair.firstChild.onmouseover = function(){ 182 | timer = setTimeout(showInfoBox, 200, box, flair); 183 | }; 184 | flair.firstChild.onmouseout = function(){ 185 | clearTimeout(timer); 186 | hideInfoBox(box, flair); 187 | }; 188 | }; 189 | 190 | var showInfoBox = function(box, flair) { 191 | waiting = true; 192 | 193 | reposInfoBox(box, flair); 194 | 195 | var loadel = document.createElement("img"); 196 | loadel.src = loadingimg; 197 | box.innerHTML = loadel.outerHTML; 198 | box.style.display = "block"; // display info box 199 | 200 | GM_xmlhttpRequest({ 201 | method: "GET", 202 | url: flair.firstChild.innerHTML, 203 | onload: function(response){ onUserpageLoad(response, box, flair); }, 204 | onerror: function(){ console.error("Error on HTTP GET request."); } 205 | }); 206 | }; 207 | 208 | var hideInfoBox = function(box) { 209 | waiting = false; 210 | box.style.display = "none"; // hide info box 211 | }; 212 | 213 | var onUserpageLoad = function(response, box, flair) { 214 | var resdom = document.createElement("html"); 215 | resdom.innerHTML = response.responseText; 216 | 217 | var playernameel = resdom.getElementsByClassName("profile-username"); 218 | if(playernameel.length === 0) { 219 | box.innerHTML = "Player not found"; 220 | return; 221 | } else { 222 | var playername = playernameel[0].innerHTML; 223 | } 224 | 225 | var idregex = /^[0-9]+$/; 226 | var userid = response.finalUrl.split("/").pop(); 227 | // need userId to grab rank data 228 | if(userid.search(idregex) === -1) { 229 | // grab from javascript variable 230 | userid = resdom.children[1].getElementsByTagName("script")[0] 231 | .innerHTML.split(";\n")[0].split("= ")[1]; 232 | 233 | // second route 234 | if(userid.search(idregex) === -1) { 235 | // grab from avatar filename 236 | var avatar = resdom.getElementsByClassName("avatar-holder")[0]; 237 | if(avatar) { 238 | userid = avatar.firstChild.src.split("_")[0].split("/").pop(); 239 | } 240 | 241 | // third route 242 | if(!avatar || userid.search(idregex) === -1) { 243 | // grab from friend button 244 | userid = resdom.getElementsByClassName("centrep")[1] 245 | .firstElementChild.getAttribute("href").split(/\/|\?/)[2]; 246 | } 247 | } 248 | } 249 | 250 | var statsurl = "https://osu.ppy.sh/pages/include/profile-general.php?u=" + 251 | userid + "&m=0"; 252 | 253 | if(waiting) { 254 | GM_xmlhttpRequest({ 255 | method: "GET", 256 | url: statsurl, 257 | onload: function(response){ onStatsLoad(response, box, playername); }, 258 | onerror: function(){ console.error("Error on HTTP GET request"); } 259 | }); 260 | } 261 | }; 262 | 263 | var onStatsLoad = function(response, box, infostring) { 264 | if(waiting) { 265 | var resdom = document.createElement("html"); 266 | resdom.innerHTML = response.responseText; 267 | 268 | var playerrank = resdom.getElementsByClassName("profileStatLine")[0] 269 | .firstElementChild.innerHTML.split(": ")[1]; 270 | 271 | infostring = infostring + " | " + playerrank; 272 | box.innerHTML = infostring; 273 | box.style.display = "block"; // display info box 274 | } 275 | }; 276 | 277 | var reposInfoBox = function(box, element) { 278 | const { x, y } = getXYPos(element); 279 | box.style.left = x+"px"; 280 | box.style.top = y-20+"px"; 281 | }; 282 | 283 | createInfoBox(); 284 | 285 | var allflairs = document.getElementsByClassName("flair"); 286 | if(g_debug) console.log("Found " + allflairs.length + " flairs."); 287 | 288 | var flairs = []; 289 | for(let i=allflairs.length-1; i>0; i--) { 290 | var flairtext = allflairs[i].innerHTML; 291 | 292 | // select only flairs with valid URLs 293 | if( flairtext.search("^https?://") !== -1 ) { 294 | if(g_debug) console.log("Found URL: " + flairtext); 295 | flairs.push(allflairs[i]); 296 | clickify(allflairs[i]); 297 | if( g_flairs && flairtext.search("^https?://osu\.ppy\.sh/u/") !== -1 ) { 298 | setHover(allflairs[i]); 299 | } 300 | } 301 | } 302 | 303 | } 304 | 305 | function Streambox() { 306 | var timer; 307 | var refreshing = true; 308 | 309 | var makeSidebarBox = function() { 310 | var refreshtext; 311 | if(g_refreshstreams) { 312 | refreshtext = "stop"; 313 | } else { 314 | refreshtext = "refresh"; 315 | } 316 | 317 | var html = 318 | "

Live streams " + 319 | "" + refreshtext + 320 | "

" + 321 | "
" + 322 | "

" + 323 | "More

"; 324 | 325 | var newelement = document.createElement("div"); 326 | newelement.innerHTML = html; 327 | var insertpoint = document.getElementsByClassName("titlebox")[0] 328 | .getElementsByClassName("usertext-body")[0] 329 | .getElementsByTagName("h2")[1]; 330 | 331 | var inserted = insertpoint.parentNode.insertBefore(newelement, insertpoint); 332 | 333 | if(g_refreshstreams) { 334 | document.getElementById("refreshbtn") 335 | .addEventListener("click", function(){ 336 | if(refreshing) { 337 | refreshing = false; 338 | window.clearInterval(timer); 339 | document.getElementById("refreshbtn").innerHTML = "start"; 340 | } else { 341 | refreshing = true; 342 | timer = window.setInterval(grabStreams, g_refreshrate*1000); 343 | document.getElementById("refreshbtn").innerHTML = "stop"; 344 | } 345 | }, false); 346 | 347 | timer = window.setInterval(grabStreams, g_refreshrate*1000); 348 | } else { 349 | document.getElementById("refreshbtn") 350 | .addEventListener("click", grabStreams, false); 351 | } 352 | 353 | return inserted; 354 | }; 355 | 356 | var grabStreams = function() { 357 | if(g_debug) console.count("Fetching from Twitch API"); 358 | GM_xmlhttpRequest({ 359 | method: "GET", 360 | url: "https://api.twitch.tv/kraken/streams?api_version=5&game=osu!&limit=3&client_id=" + twitchkey, 361 | onload: function(response){ extractInfo(response); }, 362 | onerror: function(){ console.error("Error on HTTP GET request"); } 363 | }); 364 | }; 365 | 366 | var extractInfo = function(response) { 367 | var jsondata = JSON.parse(response.responseText); 368 | 369 | var fullinfo = []; 370 | for(let i=0; i<3; i++) { 371 | fullinfo.push({ 372 | username: jsondata.streams[i].channel.display_name, 373 | title: jsondata.streams[i].channel.status, 374 | url: jsondata.streams[i].channel.url, 375 | viewercount: jsondata.streams[i].viewers, 376 | image: jsondata.streams[i].preview.small, 377 | language: jsondata.streams[i].channel.language 378 | }); 379 | } 380 | 381 | populateHTML(fullinfo); 382 | }; 383 | 384 | var populateHTML = function(streaminfo) { 385 | var container; 386 | for(let i=streaminfo.length-1; i>=0; i--) { 387 | container = streambox.firstElementChild.children[i+1]; 388 | 389 | var displayedtitle; 390 | if(streaminfo[i].title.length > 40) { 391 | displayedtitle = streaminfo[i].title.substr(0, 38) + "..."; 392 | } else { 393 | displayedtitle = streaminfo[i].title; 394 | } 395 | 396 | container.innerHTML = 397 | "" + 398 | "
" + 401 | displayedtitle.replace(/<|>/g, "") + " (" + 402 | streaminfo[i].language + ")

" + 403 | streaminfo[i].username + " / " + 404 | streaminfo[i].viewercount + " viewers" + 405 | "

"; 406 | } 407 | }; 408 | 409 | var streambox = makeSidebarBox(); 410 | grabStreams(); 411 | } 412 | 413 | function Osulinkbox() { 414 | var iframecontainer, iframe; 415 | 416 | if(g_songs) { 417 | var audio = new Audio(); 418 | audio.volume = 0.45; 419 | var currentSong = -1; 420 | } 421 | 422 | var createIFrame = function() { 423 | var div = document.createElement("div"); 424 | div.id = "ofp-iframe-container"; 425 | div.style.cssText = "display: none; position: absolute;"; 426 | 427 | var buttonel = document.createElement("p"); 428 | buttonel.id = "ofp-iframe-btn"; 429 | buttonel.innerHTML = "x"; 430 | 431 | var button = div.appendChild(buttonel); 432 | button.addEventListener("click", function(){ 433 | iframe.src = ""; 434 | iframecontainer.style.display = "none"; 435 | }); 436 | 437 | var iframe = document.createElement("iframe"); 438 | iframe.id = "ofp-iframe"; 439 | iframe.width = 400; 440 | iframe.height = 300; 441 | 442 | div.appendChild(iframe); 443 | var inserted = document.getElementsByTagName("body")[0].appendChild(div); 444 | return inserted; 445 | }; 446 | 447 | var addPreview = function(link, type) { 448 | var element = document.createElement("a"); 449 | element.innerHTML = " (preview)"; 450 | element.className = "ofp-preview"; 451 | 452 | var inserted = link.parentNode.insertBefore(element, link.nextSibling); 453 | 454 | var beatmapid = link.href.match(/[0-9]+/)[0]; 455 | if(type === 0) { 456 | inserted.addEventListener("click", function(){ 457 | if(currentSong == beatmapid) { 458 | if(audio.paused) { audio.play(); } 459 | else { audio.pause(); audio.currentTime = 0; } 460 | } else { 461 | currentSong = beatmapid; 462 | 463 | audio.src = "https://b.ppy.sh/preview/" + beatmapid + ".mp3"; 464 | audio.play(); 465 | } 466 | }, false); 467 | } else { 468 | inserted.addEventListener("click", function(){ 469 | iframe.src = "https://bloodcat.com/osu/preview.html#" + beatmapid; 470 | reposIFrame(inserted); 471 | iframecontainer.style.display = "block"; 472 | }); 473 | } 474 | }; 475 | 476 | var reposIFrame = function(element) { 477 | const { x, y } = getXYPos(element); 478 | iframecontainer.style.left = x+"px"; 479 | iframecontainer.style.top = y+20+"px"; 480 | }; 481 | 482 | if(g_songs) { 483 | iframecontainer = createIFrame(); 484 | iframe = iframecontainer.getElementsByTagName("iframe")[0]; 485 | } 486 | 487 | var entries = document.getElementsByClassName("usertext-body"); 488 | for(let i=entries.length-1; i>=0; i--) { 489 | var links = entries[i].getElementsByTagName("a"); 490 | 491 | for(let j=links.length-1; j>=0; j--) { 492 | var url = links[j].href; 493 | 494 | if(g_songs) { 495 | if( url.search("^https?://osu\.ppy\.sh/s/") !== -1 ) { 496 | addPreview(links[j], 0); 497 | } 498 | else if( url.search("^https?://osu\.ppy\.sh/b/") !== -1 ) { 499 | addPreview(links[j], 1); 500 | } 501 | } 502 | if( g_downloadmirror !== "Default" && 503 | url.search("^https?://osu\.ppy\.sh/d/") !== -1 ) { 504 | links[j].href = url.replace(/^https?:\/\/osu\.ppy\.sh\/d\//, mirrors[g_downloadmirror]); 505 | } 506 | } 507 | } 508 | } 509 | 510 | var pippy, srheader, navtop; 511 | function parallax() { 512 | pippy.style["background-position"] = "0 58px, 0 "+ 513 | (79-window.pageYOffset*0.5)+"px"; 514 | 515 | if(window.pageYOffset < 50) { 516 | srheader.style["margin-top"] = -(window.pageYOffset*0.25)+"px"; 517 | navtop.style.top = (19-window.pageYOffset*0.25)+"px"; 518 | } 519 | } 520 | 521 | window.addEventListener("load", function(){ 522 | setupConfig(); 523 | makeStylesheet(); 524 | 525 | if(g_streams) { 526 | Streambox(); 527 | } 528 | 529 | if(g_songs || g_downloadmirror !== "Default") { 530 | Osulinkbox(); 531 | } 532 | 533 | Flairbox(); 534 | 535 | if(g_parallax){ 536 | pippy = document.getElementById("header-img"); 537 | srheader = document.getElementById("sr-header-area"); 538 | navtop = document.getElementById("header-bottom-right"); 539 | 540 | window.addEventListener("scroll", function(){ 541 | if(window.pageYOffset < 150) { 542 | window.requestAnimationFrame(parallax); 543 | } 544 | }, false); 545 | } 546 | }, false); 547 | 548 | --------------------------------------------------------------------------------