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;
}
}());