NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Steam Play Community Rating Notice // @version 2.5 // @description Show Steam games' Proton compatibility user rating on supported websites. // @author DanMan // @match *://store.steampowered.com/app/* // @match *://steamdb.info/app/* // @match *://pcgamingwiki.com/wiki/* // @match *://www.fanatical.com/* // @match *://isthereanydeal.com/* // @match *://lutris.net/games/* // @connect protondb.max-p.me // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // @grant GM.getValue // @grant GM.setValue // @grant GM_getValue // @grant GM_setValue // @license MIT // @copyright 2018, DanMan (https://openuserjs.org/users/DanMan) // @supportURL https://openuserjs.org/scripts/DanMan/Steam_Play_Community_Rating_Notice/issues // @downloadURL https://openuserjs.org/install/DanMan/Steam_Play_Community_Rating_Notice.user.js // @updateURL https://openuserjs.org/meta/DanMan/Steam_Play_Community_Rating_Notice.meta.js // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // ==/UserScript== (function(){ const MS2DAY = 86400000; var matches, injectors={}, config = {}, cache = {}, memo = JSON.parse(sessionStorage.getItem('spcrn')) || {}, img = 'https://support.steampowered.com/images/custom/platform_steamplay.png', frame = document.createElement('div'), cfgFields = { 'gpuDriver':{ // label field first = before input element, otherwise after 'label': 'GPU driver', 'type': 'text', 'default': '', 'section': ['Only test results that match these filters will be considered', 'Text is case insensitive. You can use regular expressions.'] }, 'protonVersion':{ 'label': 'Proton version', 'type': 'text', 'default': '' }, 'os':{ 'label': 'Operating system', 'type': 'text', 'default': '' }, 'timestamp':{ 'label': 'Max. test age', 'type': 'int', 'default': 0, 'title':'In days' } }; GM_config.init({ 'id': 'spcrnFilters', 'title': 'Result filters', 'frame': frame, 'fields': cfgFields ,'css': '#spcrnFilters { width:550px !important; height:auto !important; right:0 !important; left:0 !important; margin:auto !important; }'+ '#spcrnFilters_wrapper { padding:1em; color:#333; }'+ '#spcrnFilters .field_label { display:inline-block; line-height:30px; min-width:105px; }' ,'events': { 'save': function(){ sessionStorage.removeItem('spcrn'); }, 'open': function(doc, win, frm){ let ageField = frm.querySelector('#spcrnFilters_field_timestamp'); let tstamp = GM_config.get('timestamp'); //seconds if(ageField.value !=0 && tstamp >0){ let diff = Date.now()-(tstamp * 1000); ageField.value = Math.floor(diff/MS2DAY); } }, 'close': daysToSeconds, 'init': daysToSeconds } }); function daysToSeconds(){ let days = GM_config.get('timestamp'), stamp; if(days < 36500){ stamp = (Date.now()/1000)-(days*24*3600); GM_config.set('timestamp', Math.floor(stamp)); //seconds } } function inject(appId, rating){ if(document.getElementById('spcrn')===null) injectors[document.location.hostname](appId,rating); } function createButton(){ let node = document.createElement('span'); node.textContent='⚙'; node.setAttribute('title', 'Configure'); node.setAttribute('style', [ 'font-size: 16px', 'padding: 0 4px', 'cursor: pointer' ].join(';')); node.addEventListener('click', function(){ GM_config.open() }, false); document.body.appendChild(frame); return node; } function createBadge(rating,appId){ let node = document.createElement('a'), lookup, title, has_filter=('filters' in config); if(rating[1]>0 && rating[2].length>0){ let lookup = Object.assign(...rating[2]); title = 'Most frequent' + (has_filter?', filtered':''); title+=' rating ('+lookup[rating[0]]+' out of '+rating[1]+' overall)'; } else title = (rating[1] == 1)? "The only rating" : "No ratings "+( has_filter? 'apply' : 'yet' ); node.textContent = rating[0].toUpperCase(); node.setAttribute('title', title); node.setAttribute('href', 'https://www.protondb.com/app/'+appId); node.setAttribute('id', 'spcrn'); node.setAttribute('style', [ 'background: #4d4b49 url('+img+') no-repeat -51px center', 'display:inline-block', 'line-height: 19px', 'color: #b0aeac', 'font-size: 10px', 'padding: 3px 10px 0 79px', 'margin-right: 1ex', 'vertical-align:bottom', 'font-family: "Motiva Sans", sans-serif' ].join(';')); return node; } injectors['store.steampowered.com']= function(appId, rating){ let node, target = document.querySelector('.game_area_purchase_platform'); if(target){ node = document.createElement('span'); node.style.verticalAlign='bottom'; node.appendChild(createButton()); node.appendChild(createBadge(rating,appId)); target.insertBefore(node,target.firstChild); } } injectors['steamdb.info']= function(appId, rating){ let node = document.createElement('tr'), sorted=[], sum=0, has_filter = ('filters' in config), target = document.querySelector('.app-row table.table tbody'), content = '<td>Steam Play Community Rating</td>'; if(has_filter) content=content.replace('</td>','*</td>') if(target){ if(rating[2].length>0){ sorted = rating[2].map((obj) => Object.keys(obj)[0]+': '+Object.values(obj)[0] ); sum = rating[2].map((obj) =>Object.values(obj)[0]).reduce((a,b) => a+b, 0); } content += '<td><a href="https://www.protondb.com/app/'+appId+'"'; if(sorted.length>0){ if(sum != rating[1] && has_filter) content += ' title="'+sum+' of '+rating[1]+' overall rating(s) apply">'; else content += ' title="'+rating[1]+' overall ratings">'; content += sorted.join(' / '); } else if(has_filter && rating[1]>0) { content += ">None of the "+rating[1]+" ratings apply"; } else content += ">No ratings yet"; content += '</a></td>'; node.setAttribute('id', 'spcrn'); node.innerHTML = content; node.firstElementChild.appendChild(createButton()); target.appendChild(node); } } injectors['pcgamingwiki.com']= function(appId, rating){ let row1 = document.createElement('tr'), row2 = document.createElement('tr'), target = document.querySelector('table#infobox-game tbody'), content; if(target){ row1.innerHTML = '<th colspan="2" class="template-infobox-header">Steam Play Community Rating</th>'; content = document.createElement('td'); content.setAttribute('colspan',2); content.setAttribute('class','template-infobox-info'); content.setAttribute('style','padding:4px 0'); content.appendChild(createBadge(rating,appId)); row2.appendChild(content); target.appendChild(row1); target.appendChild(row2); row1.firstElementChild.appendChild(createButton()); } } injectors['www.fanatical.com']= function(appId, rating){ let target = document.querySelector('.product h1'), badge; if(target) { badge = createBadge(rating,appId); badge.style.marginLeft = '20px'; badge.style.fontWeight = '450'; badge.style.verticalAlign = 'middle'; button = createButton(); button.style.verticalAlign= 'middle'; target.appendChild(badge); target.appendChild(button); } } injectors['isthereanydeal.com']= function(appId, rating){ let target = document.querySelector('#gameTitle, #popupContent .cntBoxTitleTriangle'), badge; if(target) { badge = createBadge(rating,appId); badge.style.marginLeft = '20px'; badge.style.fontWeight = '450'; badge.style.verticalAlign = 'middle'; button = createButton(); button.style.verticalAlign= 'middle'; target.appendChild(badge); target.appendChild(button); if(!target.id && target.nodeName==='DIV'){ target.style.float='right'; target.removeAttribute('class'); } } } injectors['lutris.net']= function(appId, rating){ let target = document.querySelector('.game-info ul'), content; if(target) { content = document.createElement('li'); content.style.textAlign = 'center'; content.appendChild(createBadge(rating,appId)); content.appendChild(createButton()); target.appendChild(content); } } function reqListener (response) { let data=[]; response = (this.hasOwnProperty('responseText'))? this.responseText : response.responseText; if(response.charAt(0) !== '<'){ data = JSON.parse(response); if(typeof data === 'object'){ cache[matches[1]]={'ratings': data, 'fetched': Date.parse(new Date().toUTCString())}; xdValue('spcrnCache',JSON.stringify(cache)); } } memorize(data); inject(matches[1], memo[matches[1]]); } function Ratings(data){ let rawData = data || [], scores = null, regexp, tstamp, filter = function(item){ // console.log(config); if('filters' in config) for(let fieldName in config.filters){ if(item.hasOwnProperty(fieldName) && config.filters[fieldName]!=''){ switch(fieldName){ case 'timestamp': tstamp = config.filters[fieldName]; if(tstamp > 0 && item[fieldName] > tstamp) return false; break; default: regexp = new RegExp(config.filters[fieldName],'i'); if(item[fieldName]!='' && item[fieldName].match(regexp)===null) return false; } } } return true; }; this.mostFrequent = function(){ if(scores === null) scores = this.groupedScores(); return Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b, 'N/A'); } this.groupedScores = function(){ if(scores === null){ let idx; scores={}; rawData.forEach(function(item){ idx = item.rating; if(filter(item)){ scores[idx]=(scores[idx] || 0); scores[idx]++; } }); } return Object.keys(scores).sort((a, b) => scores[b]-scores[a]).map(key => ({[key]:scores[key]}) ); } } function investigate(cb) { let regex=/\/(\d+)\/?/, tries=5, intv; switch (document.location.hostname) { case 'store.steampowered.com': if(document.querySelector('.platform_img.linux, .sysreq_tab[data-os=linux]') === null) matches = document.location.pathname.match(regex); break; case 'steamdb.info': if(document.querySelector('.scope-app .icon-linux') === null) matches = document.location.pathname.match(regex); break; case 'lutris.net': let gameInfo = document.querySelector('.game-info'); if(gameInfo && gameInfo.textContent.indexOf('Linux') === -1) matches = document.querySelector( '.game-info a.external-link[href*="://store.steampowered.com/app/"],' + '.game-info a.external-link[href*="://steamdb.info/app/"]' ) if(matches) matches=matches.href.match(regex); break; case 'pcgamingwiki.com': if(document.querySelector('#table-availability .os-linux') === null) matches = document.querySelector('#table-availability a[href^="https://store.steampowered.com/app/"]').href.match(regex); break; case 'isthereanydeal.com': var wait = 1000, check = function(){ let link, platforms = document.querySelector('.priceTable__platforms'); if(platforms && platforms.textContent.indexOf('Linux') === -1){ link = document.querySelector('.priceTable a[href^="https://store.steampowered.com/app/"]'); if(link){ matches = link.href.match(regex); if(matches){ clearInterval(intv); cb(); } } }; if(tries-- === 0) clearInterval(intv); }; intv = setInterval(check, wait); document.addEventListener('click', function(ev){ if(ev.target.hasAttribute('href') && ev.target.getAttribute('href').indexOf('#/page:game/info?plain') !== -1){ tries=5; clearInterval(intv); intv = setInterval(check, wait); } }, false); break; case 'www.fanatical.com': var check = function(){ if(document.querySelector('.system-requirements .fa-linux') === null){ let link = document.querySelector('.product-details a[href^="http://store.steampowered.com/app/"]'); if(link){ matches = link.href.match(regex); if(matches){ clearInterval(intv); cb(); } } }; if(tries-- === 0 || document.location.pathname.indexOf('/game/') === -1) clearInterval(intv); }; intv = setInterval(check, 1500); document.addEventListener('click', function(ev){ if(ev.target.hasAttribute('href') && ev.target.getAttribute('href').indexOf('/game/') !== -1){ tries=5; clearInterval(intv); intv = setInterval(check, 1500); } }, false); break; } cb(); } function memorize(rawData){ let rating='N/A', all=[], ratings; if(rawData.length>0){ ratings = new Ratings(rawData); all = ratings.groupedScores(); rating = ratings.mostFrequent(); } memo[matches[1]]=[rating, rawData.length, all]; sessionStorage.setItem('spcrn', JSON.stringify(memo)); } function setupConfig(){ let filterNames = Object.keys(cfgFields), value, filters=[]; filterNames.forEach(function(fname){ value = GM_config.get(fname); if(value !== '') filters.push({[fname]:value}); }); if(filters.length>0) config.filters=Object.assign(...filters); } function execute(){ if(matches && matches[1]){ xdValue('spcrnCache').then(function(data){ let is_stale = true, xhr, params, func, item; cache = JSON.parse(data || '{}'); // console.log(cache); if(matches[1] in cache){ item=cache[matches[1]]; is_stale = Date.parse(new Date().toUTCString()) - item.fetched > MS2DAY; // == 1 day } setupConfig(); if(is_stale === false ){ if(!(matches[1] in memo)) memorize(item.ratings); inject(matches[1], memo[matches[1]]); } else { //console.log('xhr'); params = { method: "GET", url: "https://protondb.max-p.me/games/"+matches[1]+"/reports", headers: { "Accept": "application/json", "User-Agent": navigator.userAgent + " UserJS/SPCRN" }, onload: reqListener }; func = ajaj(); if(typeof func === 'function') func(params); else { xhr = new XMLHttpRequest(); xhr.addEventListener("load", reqListener); xhr.open("GET", params.url); xhr.setRequestHeader('Accept', params.headers['Accept']); xhr.setRequestHeader('User-Agent', params.headers['User-Agent']); xhr.send(); } } }); } } investigate(execute); // compatibility stuff function ajaj(){ if(typeof GM !== 'undefined') return GM.xmlHttpRequest; else if(typeof GM_xmlhttpRequest === 'function') return GM_xmlhttpRequest; } function xdValue(name, value){ let get,set; if(typeof GM !== 'undefined'){ get = GM.getValue(name); set = GM.setValue; } else { get = {then:(cb)=> cb(GM_getValue(name))}; set = GM_setValue; } if(value !== undefined) set(name,value); return get; } }());