steamgifts rating and platforms 2023

Show rating and supported platforms on steamgifts.com according information from Steam

// ==UserScript==
// @name         steamgifts rating and platforms 2023
// @namespace    Gidden
// @version      3.0.0
// @description  Show rating and supported platforms on steamgifts.com according information from Steam
// @require      http://code.jquery.com/jquery.min.js
// @author       Gidden, update by Титан
// @match        http://www.steamgifts.com/
// @match        http://www.steamgifts.com/giveaways/search?*
// @match        https://www.steamgifts.com/
// @match        https://www.steamgifts.com/giveaways/search?*
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

// I replaced find() funcs with my responseQuerySelector() and responseQuerySelectorAll() funcs because I don't get how to get find() to work with responseText
//TODO: redirect bundles and calculate rating as max of all games in bundle

let log = false;

// Storage existence test
function supports_html5_storage() {
	try {
		return 'localStorage' in window && window.localStorage !== null;
	} catch (e) {
		return false;
	}
}

// Get color according percentage (0% = Red, 100% = Green)
function getColor(value){
	//value from 0 to 1
	var hue=((value)*120).toString(10);
	return ["hsl(",hue,",100%,42%)"].join("");
}

var platforms = ["win", "mac", "linux", "steamplay"];

// Get actual configuration from storage
function getFilterCfg(myItem)
{
	if (myItem == "percentage") {
		myValue = 0;
	} else {
		myValue = 1;
	}
	if (supports_html5_storage()) {
		if (localStorage.getItem(myItem) !== null) {
			myValue = localStorage.getItem(myItem);
		}
	}
	return myValue;
}

// Store information about game
function saveGameData(url, data) {
	if (supports_html5_storage()) {
		localStorage.setItem(url,  JSON.stringify(data));
	}
}

// Get stored information about game
function loadGameData(url) {
	var myValue = null;
	if (supports_html5_storage()) {
		myValue = localStorage.getItem(url);
		if (myValue) {
			myValue = JSON.parse(myValue);
		}
	}
	return myValue;
}

// Filtering functions
var filterFunction = "" +
	"var platforms = [\"win\", \"mac\", \"linux\", \"steamplay\"];" +
	"function supports_html5_storage() {" +
	"  try {" +
	"    return 'localStorage' in window && window.localStorage !== null;" +
	"  } catch (e) {" +
	"    return false;" +
	"  }" +
	"}" +
	"function saveFilterCfg()" +
	"{" +
	"    if (supports_html5_storage()) {" +
	"        for (var x in platforms){" +
	"            platform = platforms[x];" +
	"            if (document.getElementById(\"filter_\" + platform).checked) {" +
	"                localStorage.setItem(platform, 1);" +
	"            } else {" +
	"                localStorage.setItem(platform, 0);" +
	"            }" +
	"        }" +
	"    if (document.getElementById(\"filter_level\").checked) {" +
	"        localStorage.setItem(\"level\", 1);" +
	"    } else {" +
	"        localStorage.setItem(\"level\", 0);" +
	"    }" +
	"        localStorage.setItem(\"percentage\", document.getElementById(\"filter_percentage\").value);" +
	"    }" +
	"}" +
	"function changeFilter(){" +
	"" +
	"    platformCheck = [];" +
	"    for (var x in platforms){" +
	"        platform = platforms[x];" +
	"        if (document.getElementById(\"filter_\" + platform).checked) {" +
	"            platformCheck.push(platform);" +
	"        }" +
	"    }" +
	"    percCheck = document.getElementById(\"filter_percentage\").value;" +
	"" +
	"    var headers = $(\"div.giveaway__summary h2.giveaway__heading\");" +
	"" +
	"    headers.each(function() {" +
	"        var header = $(this);" +
	"        var showEl = true;" +
	"" +
	"        if (document.getElementById(\"filter_level\").checked){" +
	"            if (header.parent().find(\"div.giveaway__column--contributor-level--negative\").length) {" +
	"                header.parent().parent().parent().hide();" +
	"                return;" +
	"            }" +
	"        }" +
	"" +
	"        percE = header.find(\"span.percentage\");" +
	"" +
	"        if (percE.length)" +
	"        {" +
	"            var perc2 = percE[0].getAttribute(\"perc\");" +
	"            if (perc2 < percCheck){" +
	"                showEl = false;" +
	"            } else {" +
	"                showEl = false;" +
	"                for (var x in platformCheck){" +
	"                    actPlatformCheck = platformCheck[x];" +
	"                    if (header.find(\"span.\"+actPlatformCheck).length) {" +
	"                       showEl = true;" +
	"                    }" +
	"                }" +
	"            }" +
	"        }" +
	"" +
	"        if (showEl) {" +
	"            header.parent().parent().parent().show();" +
	"        } else {" +
	"            header.parent().parent().parent().hide();" +
	"        }" +
	"    });" +
	"    saveFilterCfg();" +
	"}";

function renderPluginGame(header, gameData) {

	if ((header === null) || (gameData === null) /*|| ("error" in gameData)*/){
		return;
	}

	// Remove old informations
	header.find("a.giveaway__icon span").remove();
	header.find("h2.giveaway__heading span").remove();

	var a_tag = header.find( "a.giveaway__icon" );
	var i_tag = header.find( "i.giveaway__icon" );
	if (! i_tag.length) {
		i_tag = a_tag;
	}
	a_tag.attr("style", "opacity:1");
	a_tag.find("i.fa-steam").attr("style", "opacity:.35");

	// Render perc + ppl
	if ("perc" in gameData) {
		var rating = gameData.perc + '% (' + gameData.ppl + ') ';
		header.append(" <span style=\"color:" + getColor(gameData.perc /100.0) + "\" class=\"percentage\" perc=\"" + gameData.perc + "\">" + rating + "</span>");
	}

	// Render platforms
	if ("platforms" in gameData)
	{
		//a_tag.append(" <span>" + platform.html() + "</span>");
	}
	var platformsHtml = "<span>";
	for (var platf in gameData.platforms) {
		platformsHtml = platformsHtml + "<span class=\"platform_img " + platf + "\"></span>";
	}
	a_tag.append(platformsHtml + "</span>");

	// Render cards
	if (("cards" in gameData) && (gameData.cards)) {
		i_tag.after("<span class=\"platform_img right_allign\"><img src=\"http://store.akamai.steamstatic.com/public/images/v6/ico/ico_cards.png\"></span>");
	}

	if ((gameData.perc < getFilterCfg("percentage"))) {
		header.parent().parent().parent().hide();
	}

	hide = gameData.platforms?.length > 0;
	for (var x in platforms) {
		if (hide) break;
		platformCheck = platforms[x];
		if ((gameData.platforms[platformCheck] || false) && (getFilterCfg(platformCheck) == 1)) {
			hide = false;
		}
	}
	if (hide) {
		header.parent().parent().parent().hide();
	}
}

/**
 * Looks for a HTMLElement with {@link selector} in the responseText
 * @param responseText array of HTMLElements and shit
 * @param selector css selector
 * @returns {null|HTMLElement} first HTMLElement matching {@link selector} or null if not found
 */
function responseQuerySelector(responseText, selector) {
	for (var i = responseText.length-1; i >= 0 ; i--) {
		var currentResponse = responseText[i];
		if (/*currentResponse.classList?.contains("responsive_page_frame") &&*/ currentResponse.querySelector) { // if it's a HTMLElement /*and is a responsive_page_frame (basically a whole page)*/
			var node = currentResponse.querySelector(selector);
			if (node) {
				return node;
			}
		}
	}
	return null;
}

/**
 * Looks for a HTMLElements with {@link selector} in the responseText
 * @param responseText array of HTMLElements and shit
 * @param selector css selector
 * @returns {null|HTMLElement} array of HTMLElements matching {@link selector} or empty array if not found
 */
function responseQuerySelectorAll(responseText, selector) {
	var nodes = [];
	for (var i = responseText.length-1; i >= 0 ; i--) {
		var currentResponse = responseText[i];
		if (currentResponse.classList?.contains("responsive_page_frame") && currentResponse.querySelectorAll) { // if it's a HTMLElement and is a responsive_page_frame (basically a whole page)
			var node = currentResponse.querySelectorAll(selector);
			if (node) {
				nodes.push(node);
			}
		}
	}
	return nodes;
}

function exportDataFromPage(steamURL, responseText, callFunction) {
	var gameData = {};
	gameData.ppl = false;
	gameData.perc = false;
	gameData.platforms = {};
	gameData.cards = false;
	gameData.fetchTime = new Date().getTime();

	var ratingFull = responseQuerySelector(responseText,".responsive_reviewdesc_short");

	if (!ratingFull === null) {
		gameData.error = true;
		saveGameData(steamURL, gameData);
	} else {
		let stage = "rating"
		let debugVar = ratingFull;
		try {
			// extract percentare review (perc) and peoples reviewed (ppl) variables.

			var ratingFullString = ratingFull.innerText.trim();
			var perc = ratingFullString.match("[0-9]+ ?%");
			if (perc === null) {
				perc = ratingFullString.match("% ?[0-9]+");
			}
			gameData.perc = perc[0].replace("%", "").replace(" ", "");
			ppl = ratingFullString.match(/[0-9,]+/g);
			if (ppl[0] == gameData.perc) {
				ppl = ppl[1];
			} else {
				ppl = ppl[0];
			}
			gameData.ppl = ppl.replace(",", ".");

			stage = "platforms"
			// extract platforms
			var platform = responseQuerySelector(responseText, "div.game_area_purchase_platform");
			debugVar = platform;
			if (platform) {
				for (var x in platforms) {
					var actPlatformCheck = platforms[x];
					if (platform.querySelector("span." + actPlatformCheck)) {
						gameData.platforms[actPlatformCheck] = true;
					}
				}
			}

			stage = "cards"
			// extract support of gaming cards
			var cards;
			cards = responseQuerySelector(responseText, "[class=\"game_area_details_specs_ctn\"][href=\"https://store.steampowered.com/search/?category2=29&snr=1_5_9__423\"]")
			debugVar = cards;
			gameData.cards = cards !== null;

			stage = "save"
			// Save
			saveGameData(steamURL, gameData);

			//renderPluginGame(header, gameData);
			if (callFunction !== null) {
				callFunction(steamURL, gameData);
			}
		} catch (e) {
			gameData.error = true;
			saveGameData(steamURL, gameData);
			if(log) {
				console.log(`[Steamgifts rating] Error on ${stage}: ` + e)
				console.log(debugVar)
				console.log(responseText)
			}
		}
	}
	return gameData;
}

function fetchSteamDate(steamURL, callFunction) {
	var gameData = {};

	GM_xmlhttpRequest({
		method: "GET",
		url: steamURL,
		context: steamURL,
//        synchronous: true,
		onload: function(response) {
			var responseText = $(response.responseText);
			var ageCheck = responseQuerySelector(responseText, "div#agegate_box")
			if (ageCheck) {
				GM_xmlhttpRequest({
					method: "POST",
					data: "snr=1_agecheck_agecheck__age-gate&ageDay=1&ageMonth=January&ageYear=1980",
					headers: {
						"Content-Type": "application/x-www-form-urlencoded"
					},
					url: steamURL.replace("/app/","/agecheck/app/"),
					context: steamURL,
					onload: function(response) {
						var responseText = $(response.responseText);
						exportDataFromPage(steamURL, responseText, callFunction);
					}
				});
			} else {
				exportDataFromPage(steamURL, responseText, callFunction);
			}
		}
	});

	return gameData;
}

//! MAIN function
// Generate html string for checkboxes
var checkboxes = "";
for (var x in platforms){
	platform = platforms[x];
	var checked = "";
	if (getFilterCfg(platform) == 1) {
		checked = "checked";
	}
	checkboxes += "<input class=\"filter_checkboxes\" type=\"checkbox\" name=\"filter_" + platform + "\" id=\"filter_" + platform + "\" onclick=\"changeFilter();\" " + checked + "><label class=\"checkboxes\" for=\"filter_" + platform + "\"><span class=\"platform_img " + platform + "\"></span></label>";
}

// New headers. Mainly css styles
$("head link:last")
	.before("<link rel=stylesheet type=text/css href=https://steamstore-a.akamaihd.net/public/css/v6/store.css>")
	.after("<style>span.platform_img {background-color: black; height:20px; display: inline-block; opacity: 0.35;} input.filter_checkboxes {height:15px; width:20px;} input.filter_percentage {height:18px; width:40px; margin:5px; padding:0px 10px;} label.checkboxes {margin-right:5px;}</style>")
	.after("<style>span.percentage {font-size: medium; font-weight: bold;} h2.giveaway__heading {position:relative;} span img {width: 20px; height: 16px;} span.right_allign {position:absolute; right: 0; top: 0;}</style>")
	.after("<script type=\"text/javascript\">" + filterFunction + "</script>");

var checkedLevel = "";
if (getFilterCfg("level") == 1) {
	checkedLevel = "checked";
}

// Appending left filter panel
filterPanel = "<h3 class=\"sidebar__heading\">Filter</h3><div style=\"margin-left:10px;\">" +
	" <span style=\"font-weight: bold;\">Game</span> &gt;= <input type=\"text\" class=\"filter_percentage\" id=\"filter_percentage\" value=\"" + getFilterCfg("percentage") + "\" onchange=\"changeFilter()\" onkeyup=\"changeFilter()\">% <br>" +
	checkboxes + "</br>" +
	"<input type=\"checkbox\" class=\"filter_checkboxes\" id=\"filter_level\" onclick=\"changeFilter();\" " + checkedLevel + "><label class=\"checkboxes\" for=\"filter_level\"><span style=\"font-weight: bold;\">Hide higher level</span></label>" +
	"</div>";

$("div.sidebar__search-container").after(filterPanel);

// Fetching steam for games information
var needFetch = {};
var headers = $("div.giveaway__summary h2.giveaway__heading");
var actTime = new Date().getTime();
headers.each(function() {
	var header = $(this);
	var iconElement = header.find("a.giveaway__icon");
	if (iconElement.length) {
		var steamURL = iconElement.attr("href").trim();

		if ((getFilterCfg("level") == 1) && (header.parent().find("div.giveaway__column--contributor-level--negative").length)) {
			header.parent().parent().parent().hide();
		}

		var gameData = loadGameData(steamURL);

		if (gameData === null || gameData.error === true) {
			needFetch[steamURL] = true;
		} else {
			renderPluginGame(header, gameData);

			// If game info are older than one day then fetch new info.
			if ((actTime - gameData.fetchTime) > 24*60*60*1000) {
				needFetch[steamURL] = true;
			}
		}
	}
});

function afterFetch(steamURL, gameData) {
	var headers = $("a[href=\"" + steamURL + "\"]");
	headers.each(function() {
		var header = $(this).parent();
		renderPluginGame(header, gameData);
	});
}

for(var steamURL in needFetch) {
	var gameData = fetchSteamDate(steamURL, afterFetch);
}