PSNprofiles.net enhancements

On guide pages: adds a button to hide earend trophies, their description and links and uses a new style for earned trophies, On all pages: adds update button, On game pages: persist search string

// ==UserScript==
// @name        PSNprofiles.net enhancements
// @author      Barokai | www.loigistal.at
// @icon        http://psnprofiles.com/forums/favicon.ico
// @namespace   http://www.loigistal.at/userscripts/
// @license     https://github.com/Barokai/psnprofiles-enhanced/blob/master/LICENSE
// @homepageURL http://barokai.github.io/psnprofiles-enhanced/
// @supportURL  https://github.com/Barokai/psnprofiles-enhanced/issues/new
// @version     0.86
// @description On guide pages: adds a button to hide earend trophies, their description and links and uses a new style for earned trophies, On all pages: adds update button, On game pages: persist search string
// @match       http*://psnprofiles.com/*
// @grant       GM_addStyle
// @require     http://code.jquery.com/jquery-latest.js
// @copyright   2016+, Barokai
// ==/UserScript==

// better visible green for earned trophies, used before: #57FF3B
/*jshint multistr: true */
GM_addStyle("\
.tableofcontents li.earned {	\
background: #bada55 !important;  \
} \
.roadmap-trophies li.earned { \
background: #bada55 !important; \
} \
.roadmap-trophies .trophy.earned { \
background: #bada55 !important; \
} \
.section-holder.earned{ \
border: 3px solid #bada55; \
} \
#toggleEarned { \
background-color: #bada55; \
}\
.invisible.tags a.earned{\
text-shadow: 1px 1px 1px #bada55;\
}\
");


/* Guide enhancements ------------------------------------------------------- */
// TODO barokai: fix scrolling behavior when trophies are hidden and a trophy link is clicked in Guide Contens column (ID="TOCWrapper")
function addToggleEarnedButton() {
	// add class earned to all links which match earned trophies in overview-info box
	$('.earned > a').each(function() {
		var trophyName = $(this).text().trim();
		$('nobr > a:contains(' + trophyName + ')').addClass("earned");
	});

	// adds class "earned" to sections to hide them with the same toggle function
	$("img[class*='earned']").each(function() {
		// .closest('li').parent().closest('li')
		// TODO check if class of img isn't .unearned!
		$($(this).closest("div[class*='section-holder']").parent().closest("div[class*='section-holder']")).addClass("earned");
	});

	// workaround, above scripts adds earned to all secionts as img with class .unearned is matched too.
	$("img[class*='unearned']").each(function() {
		// .closest('li').parent().closest('li')
		$($(this).closest("div[class*='section-holder']").parent().closest("div[class*='section-holder']")).removeClass("earned");
	});

	// workaround to hide trophies in overview completely
	$(".trophy.earned").each(function(){
		$($(this).closest(".col-xs-6")).addClass("earned");
		$($(this).closest(".col-xs-12")).addClass("earned");
	});

	// adds button for toggling to overview info box
	$(".overview-info").append('<span class="tag" id="toggleEarned" title="click to toggle visiblity of earned trophies"><span class="typo-top">toggle</span><br/><span class="typo-bottom">earned</span>');
	$(document).on("click", "#toggleEarned", toggleEarned);
}

// TODO: fix this, doesn't work in new version.
function addToggleTypeButton() {
	// get trophy types (without spaces) - used as classes to toggle them later.
	var trophyTypes = $('table.invisible .tag').map(function(i, el) {
		var type = $(el);
		var typeName = type.text().replace(/\s+/g, '');
		// add toggle visibility to type boxes
		type.click(function(e) {
			toggleClass(e, typeName);
		});
		return typeName;
	}).get();

	// get corresponding trophies - start with the ones in overview box
	$('table.invisible td small').each(function(index) {
		var typeClassName = trophyTypes[index];
		var trophyHref = $("nobr a", $(this));
		// add class to all found hrefs for this section
		trophyHref.addClass(typeClassName);

		trophyHref.each(function() {
			var trophyName = $(this).text();

			// contents of roadmap
			$(".roadmap-trophies li:contains(" + trophyName + ")").addClass(typeClassName);

			// all the sections
			$("div[class*='element section-holder']:contains(" + trophyName + ")").addClass(typeClassName);

			// next the ones in the guide contents on the right
			$('#TOCList li:contains(' + trophyName + ')').addClass(typeClassName);

			// add class to trophies which are found in multible type sections
			$('table.invisible nobr a:contains(' + trophyName + ')').addClass(typeClassName);
		});
	});
}

function toggleEarned(e) {
	toggleClass(e, "earned");
}

function toggleClass(e, className) {
	var element;

	// get correct element (togglebutton's layout would be destroyed otherwise)
	if (e.target.parentNode.id != "toggleEarned") {
		element = e.target;
	} else {
		element = e.target.parentNode;
	}

	if (element.innerHTML.indexOf(" *") >= 0) {
		element.innerHTML = element.innerHTML.slice(0, -2);
	} else {
		element.innerHTML += " *";
	}

	$("." + className).toggle("slow", function(e) { /* Animation complete */ });
}
/* Guide enhancements end --------------------------------------------------- */

/* Profile enhancements ----------------------------------------------------- */

// thanks to serverTimeout for sort by rank (his profile: http://psnprofiles.com/forums/user/80890-servertimeout/)
// see his post here: http://psnprofiles.com/forums/topic/24324-sort-by-rank/?view=findpost&p=647509
function addSortByRank() {
	var dropdown = $('.dropdown-toggle.order');
	var buttonNameAsc = "Rank E-S";
	dropdown.next().append(
		$('<li><a href="">' + buttonNameAsc + '</a></li>').on('click', function(ev) {
			sort(ev, '+', buttonNameAsc); // + for ascending sort
		})
	);
	var buttonNameDesc = "Rank S-E";
	dropdown.next().append(
		$('<li><a href="">' + buttonNameDesc + '</a></li>').on('click', function(ev) {
			sort(ev, '-', buttonNameDesc); // + for descending sort
		})
	);
}

function sort(e, order, buttonName) {
	e.preventDefault();
	var trophyOrder = ['F', 'E', 'D', 'C', 'B', 'A', 'S'];
	var r;
	if (order === '+') {
		for (r = 0; r <= trophyOrder.length; r++) {
			// TODO: sort ranks by percent before
			$('.' + trophyOrder[r]).each(function() {
				$('.'+trophyOrder[r]).each(function() {
					$('#content table.zebra').eq(0).append(jQuery(this).closest('tr'));
				});
			});
		}
	}
	if (order === '-') {
		for (r = trophyOrder.length; r >= 0; r--) {
			// TODO: sort ranks by percent before
			$('.' + trophyOrder[r]).each(function() {
				$('.'+trophyOrder[r]).each(function() {
					$('#content table.zebra').eq(0).append(jQuery(this).closest('tr'));
				});
			});
		}
	}

	// set dropdown button name when new order was set
	$('.dropdown-toggle.order').text("Order (" + buttonName + ")");
}
/* Profile enhancements end ------------------------------------------------- */

/* Global enhancements ------------------------------------------------------ */
function addUpdateButton() {
	$('.navigation > ul').append("<li><a href='/?update'>Update Profile</a></li>");
	// TODO barokai: check if successfully updated and redirect to the last visited page (where update was clicked)
}
/* Global enhancements end ---------------------------------------------------*/

/* Games page enhancements -------------------------------------------------- */

// thanks to dernop for this searchFix (his profile: http://psnprofiles.com/forums/user/45256-dernop/)
// see his post here: http://psnprofiles.com/forums/topic/32107-bugsoddities-in-the-games-search-feature/#entry777278
function gameSearchFix() {
	$('#searchGames').off('keyup');
	$('#searchGames').keyup(function() {
		window.clearTimeout('searchInt');
		var input = $(this);
		if (input.val().length > 1) {
			$('#loading').show();
			$('#closeButton').hide();

			var searchInt = window.setTimeout(
				function() {
					$('#pagination').hide();
					var q = encodeURIComponent(input.val());
					$.ajax({
						url: "/php/liveSearch.php?t=g&q=" + q,
						success: function(html) {
							window.location.hash = '#!' + q;
							$('#game_list').html(html);
							$('#loading').hide();
							$('#closeButton').show();
						}
					});
				}, 500);
		} else {
			$('#loading').hide();
		}
	});
	var query = decodeURIComponent(window.location.hash.replace('#!', ''));

	if (query.length > 0) {
		$('#searchGames').val(query).keyup();
	}
}

// TODO barokai: integrate other func in psnp
var psnp = {
	id: $('div.user-menu a.dropdown-toggle span').text(),
	_gamesTable: $('table#gamesTable'), // game table on user profile
	_gameList: $('table#game_list'),
	_profileBar: $('div.profile-bar')
};
// add percentage on mouseover or integrate into game row
// add mouse over information like percentage (last row), last played if available etc.
// thanks to dernop (again) - http://psnprofiles.com/forums/topic/35583-add-possibility-to-hide-earned-trophies-in-guides-with-userscript/#entry932561
$(function() {
	// initialize psnp page/DOM information

	// psnp properties
	$.extend(psnp, {
		isProfile: psnp._gamesTable[0] !== undefined && psnp._profileBar[0] !== undefined,
		isOwnProfile: $(psnp._profileBar).find('div.info').text().indexOf(psnp.id) > -1,
		isGameList: psnp._gameList.length == 1,
		myGames: JSON.parse(localStorage.getItem('_mygames')) || {}
	});

	psnp.updateMyGames = function(games) {
		var count = 0;
		$.each(games, function(i, e) {
			psnp.myGames[e.id] = e;
			count++;
		});
		localStorage.setItem('_mygames', JSON.stringify(psnp.myGames));
		console.log(count + " games added/updated to localstorage.");
	};

	// PROFILE / GAME LIST
	psnp.gameList = (function() {
		if (!psnp.isProfile)
			return undefined;
		var _games = [];
		// register mutationobserver for gameList to handle 'load-on-scroll'
		var obs = new MutationObserver(function(mutations) {
			parseGames(); // just re-parse all games if list has changed.
		});
		obs.observe(psnp._gamesTable[0], {
			childList: true,
			subtree: true
		});

		// parse PSNP games table (id, name, completion, # of trophies)
		function parseGames() {
			_games = [];
			psnp._gamesTable.find('tr:has(a.bold)').each(function(i, row) {
				var title = $(row).find('a.bold')[0];
				var game = {
					id: title.href.match(/\/trophies\/([^\/]+)/)[1],
					name: title.innerText,
					progress: $(row).find('div.progress_outer span').text(),
					completed: $(row).hasClass('completed'),
					platinum: $(row).hasClass('platinum'),
					gold: $(row).find('li.gold').text(),
					silver: $(row).find('li.silver').text(),
					bronze: $(row).find('li.bronze').text(),
				};

				if (!psnp.isOwnProfile && psnp.myGames[game.id]) {
					var img = $(row).find('img.trophy_image');
					img.removeClass('no-border').addClass('earned'); // mark owned games
				}
				_games.push(game);
			});

			// if this is our own profile, save the games list.
			if (psnp.isOwnProfile) {
				psnp.updateMyGames(_games);
			}
		}

		// parse game list initially
		parseGames();

		return {
			games: _games
		};
	})();

	// #GAMELIST#  global list of games (psnprofiles.com/games)

	// Mark owned game in "Games" list
	if (psnp.isGameList) {
		psnp._gameList.find('tr:has(a.bold)').each(function(i, row) {
			var title = $(row).find('a.bold')[0];
			var id = title.href.match(/\/trophies\/([^\/"]+)/)[1];
			var img = $(row).find('img.trophy_image');

			// mark owned games
			if (psnp.myGames[id]) {
				if (psnp.myGames[id].platinum)
					$(row).addClass('platinum'); // add platinum row style
				if (psnp.myGames[id].completed)
					$(row).addClass('completed'); // add completed row style

				img.removeClass('no-border').addClass('earned');
				if (psnp.myGames[id].progress != '100%')
					img.css('border-color', 'yellow'); //

				// add completion percentage
				var avgProgress = $(row).children('td:eq(4)').find('span.typo-top');
				console.log(avgProgress);
				avgProgress.text(psnp.myGames[id].progress + " / " + avgProgress.text());

			}
		});
	}
});

/* Games page enhancements end -----------------------------------------------*/

// helperfunction to determine if the url matches a certain segment
function matchesUrl(urlSegment) {
	return document.location.pathname.indexOf(urlSegment) === 0;
}

/* -------------------------------------------------------------------------- */
/* Apply enhancements to correct pages -------------------------------------- */
/* -------------------------------------------------------------------------- */

// add toggle button functionality to all guides (if any earned trophies were found
//matchesUrl("/guide/") && addToggleTypeButton(); //TODO: re-enable after fix
matchesUrl("/guide/") && $('.earned > a').length > 0 && addToggleEarnedButton();
// add searchFix to games page
matchesUrl("/games") && gameSearchFix();
// add update button to navigation on all psnprofile pages (if logged in)
psnp.id && addUpdateButton();
// add only on profile pages and others where a sort dropdown is
matchesUrl("/" + psnp.id) && addSortByRank();

// http://stackoverflow.com/a/27363569
(function() {
	var origOpen = XMLHttpRequest.prototype.open;

	XMLHttpRequest.prototype.open = function() {
		// console.log('request started!');
		this.addEventListener('load', function() {
			if (~this.responseURL.indexOf("status?user=")){
				// console.log('request completed!');
				//console.log(this.readyState); //will always be 4 (ajax is completed successfully)
				//console.log(this.responseText); //whatever the response was
				var obj = jQuery.parseJSON(this.responseText);
				var requestName = this.responseURL.substr(this.responseURL.lastIndexOf('/') + 1).replace(psnp.id, "");
				var status = obj.status.replace(psnp.id, "");
				if(requestName == "status?user=" && status === " has been updated"){
					window.location.href = "https://psnprofiles.com/" + psnp.id;
				}
			}
		});

		origOpen.apply(this, arguments);
	};
})();