Userscript with no name

Overhauls the new raffle page and enhances a few others

13.04.2016 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey to install this script.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name        Userscript with no name
// @namespace   NiGHTS
// @author	Jim [U:1:34673527]
// @description Overhauls the new raffle page and enhances a few others
// @include     http://tf2r.com/newraf.html*
// @include     http://tf2r.com/raffles.html*
// @include     http://tf2r.com/settings.html*
// @include     http://tf2r.com/k*.html*
// @include     http://tf2r.com/user/*.html*
// @version     1.0
// @grant		GM_xmlhttpRequest
// @require 	http://code.jquery.com/jquery-1.12.0.min.js
// @run-at      document-start
// @connect 	steamcommunity.com
// @noframes
// ==/UserScript==

window.NoName = {
	init: function() {
		console.log('---Userscript with no name for TF2r. Made with <3 by Jim :NiGHTS:---');
		this.Storage.init();
		this.Steam.init();
		this.UI.init();
		this.ScrapTF.init();

		if(!window.location.pathname.indexOf('/newraf.html')) {
			this.NewRaffle.init();
		}

		if(!window.location.pathname.indexOf('/settings.html')) {
			this.Settings.init();
		}

		if(!window.location.pathname.indexOf('/user/')) {
			this.Profile.init();
		}

		if(!window.location.pathname.indexOf('/raffles.html')) {
			this.RaffleList.init();
		} else if($('.participants').length) {
			this.Raffle.init();
		}
	},

	//Export override functions to unsafe window
	//This needs to be run both as early as possible and after page load
	//We can't know when our script runs relative to the scripts on the page so we need to cover both eventualities
	exportOverrides: function() {
		console.log('[NoName::exportOverrides] Exporting overrides');

		try {
			this.UI.exportOverrides();
			this.Raffle.exportOverrides();
			this.RaffleList.exportOverrides();
		} catch (e) {
			console.error(e);
		}
	},
};

window.NoName.Storage = {
	available: false,
	callbacks: {},

	init: function() {
		var that = this;

		if(!localStorage && localStorage.getItem) {
			console.warn('[Storage::init] localStorage not available. Settings will not be saved.');

			return;
		}

		this.available = true;

		window.addEventListener('storage', function(e) {
			if(that.callbacks[e.key]) {
				for(var i = 0; i < that.callbacks[e.key].length; i++) {
					that.callbacks[e.key][i](e.oldValue, e.newValue, e.url);
				}
			}
		});
	},

	get: function(key, defaultValue) {
		if(!this.available) {
			return defaultValue;
		}

		return (typeof localStorage[key] === 'undefined') ? defaultValue : localStorage[key];
	},

	set: function(key, value) {
		if(!this.available) {
			return false;
		}

		localStorage[key] = value;

		return true;
	},

	listen: function(key, callback) {
		this.callbacks[key] = this.callbacks[key] || [];
		this.callbacks[key].push(callback);
	}
};

//Generic ui changes
window.NoName.UI = {
	missingImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAMFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaPxwLAAAAEHRSTlMAzF8Ovno2lrEpG6NtUohEM5nYxgAAAURJREFUSMdjGIHALElJSS0Bp/SkEkEwaL2MVZrrhCAchGGTDxREApUY8sxgeQRQQFfwRRAVSBugyrMKooPNhBRIoznREUPFAVQVihgKpFAVcGAoEEVVwCaIAdD88RAi2q6kDnPOB1QFC0Fi4reALCZHrGHFBLJ2AZg5Eas3mBsFxRegBEoAWmB/FLkAczD26GDfyYBfAZcBmoIJOFMWL1heAlUQM1SFcCuApB1nnPKsGG5ECxBIoIvjNGA2xIBmXPKckJgQwenJKRADPHG6oBHiAgMCXjiA04mJYHk3FDHMhCW9AKc8M4FAZOAHK0jArYAHJC/DgBswJxWCbMALkkoOMBAABgwDC2yfv8Urz9kIKnzwABPMwgdLQXMBjwJwcnmARwE4rgrwKHCE5Hvc4CCB2GawhhVv+LKVAt6Q4lK6zDAcAQAdIEKHGzsRJwAAAABJRU5ErkJggg==',

	init: function() {
		var that = this;

		this.removeExistingUI();
		this.addStyles();
		this.addNewUI();
		this.fixMissingItems();

		NoName.Storage.listen('transitions', function(oldValue, newValue, url) {
			if(newValue) {
				$(document.body).addClass('transitions');
			} else {
				$(document.body).removeClass('transitions');
			}
		});
	},

	exportOverrides: function() {
		var that = this;

		//Override raffle list getItems() function to handle items with no schema information
		unsafeWindow.getItem = exportFunction(function(item) {
			return that.getItemOverride(item);
		}, unsafeWindow);

		//Remove slDown, message transitions are done in css now
		unsafeWindow.slDown = exportFunction(function() {}, unsafeWindow);
	},

	removeExistingUI: function() {
		unsafeWindow.$('.item').unbind('hover');
		//$('#content > .indent').children().unwrap();
	},

	getItemOverride: function(item) {
		var element = document.createElement('div');
		element.setAttribute('ilevel', item.level);
		element.setAttribute('iname', item.name || 'Unknown item');
		element.setAttribute('iu1', item.iu1);
		element.className = 'item ' + item.q;
		element.style.backgroundImage  = 'url(' + (item.image || this.missingImage)  + ')';

		return element.outerHTML;
	},

	//Fix other missing items that arent added via getItems()
	fixMissingItems: function() {
		var that = this;

		$('.item').each(function() {
			var $img = $(this).children('img');

			if($img.attr('src') && $img.attr('src') !== 'null') {
				this.style.backgroundImage = 'url(' + $img.attr('src') + ')';
			} else {
				$(this).attr('iname', 'Unknown item');
				this.style.backgroundImage = 'url(' + that.missingImage + ')';
			}

			$img.remove();
			this.style.width = '';
			this.style.height = '';
		});
	},

	addStyles: function() {
		$(document.head).append(
			$('<style></style>').text(
				'@keyframes fadeInDown {\
					0% {\
						transform: translateY(-50px);\
						opacity: 0;\
					}\
					100% {\
						transform: translateY(0);\
						opacity: 1;\
					}\
				}\
				@keyframes RestoreColour {\
					100% {\
						background-color: #2A2725;\
					}\
				}\
				html, body {\
					height: auto;\
				}\
				body {\
					font-size: 14px;\
					overflow: hidden;\
				}\
				h1 {\
					margin-top: 0;\
					font-family: tf2build;\
					color: #EBE2CA;\
				}\
				table {\
					margin: 0 -2px;\
				}\
				td {\
					border-top: 1px solid transparent;\
					border-bottom: 1px solid transparent;\
				}\
				.indent {\
					left: auto;\
					margin-left: 0;\
					padding: 0 12px;\
				}\
				/* Remove 2px gap around tables caused by border-spacing */\
				table[width="100%"] {\
					width: auto !important;\
					min-width: 100%;\
				}\
				.item { /*Scale background image to tile size*/\
					background-size: 100% 100%;\
					border-width: 0;\
					width: 68px;\
					height: 68px;\
					/*transition: all 0.1s;*/\
					position: relative;\
					overflow: hidden;\
				}\
				.item.q15 { /*Add decorated quality colour*/\
					background-color: rgba(250, 250, 250, 0.6);\
					border-color: #fafafa;\
				}\
				.item.gelite { /*Add grade colours*/\
					color: #eb4b4b;\
				}\
				.item.gassassin {\
					color: #d32ce6;\
				}\
				.item.gcommando {\
					color: #8847ff;\
				}\
				.item.gmercenary {\
					color: #4b69ff;\
				}\
				.item.gfreelance {\
					color: #5e98d9;\
				}\
				.item.gcivilian {\
					color: #b0c3d9;\
				}\
				.item.hasgrade:before {/*Show grade colours in top right corner */\
					content: \'\';\
					position: absolute;\
					top: -20px;\
					right: 0;\
					border: 20px solid transparent;\
					border-right-color: initial;\
					color: inherit;\
				}\
				.item:hover {\
					transform: scale(1.1);\
					box-shadow: none;\
				}\
				/* Not risking messing with the enter button */\
				input:not(#enbut), textarea, select, button:not(#enbut) {\
	    			background-color: #4d4d4d;\
	    			border: 1px solid #4d4d4d;\
	    			box-sizing: border-box;\
	    			border-radius: 2px;\
	    			margin: 3px 0;\
	    			height: 32px;\
	    			font-size: 14px;\
	    			box-shadow: none;\
	    			color: #dddddd;\
    			}\
    			input.full-width, select.full-width, textarea.full-width, button.full-width {\
    				width: 100%;\
    			}\
    			/*Specificity issues*/\
    			input[type=checkbox]:not(#enbut), input[type=radio]:not(#enbut) {\
    				height: auto;\
    				width: auto;\
    			}\
    			input:invalid, textarea:invalid, select:invalid {\
    				border-color: red;\
    			}\
    			select{\
	    			line-height: 32px;\
	    			padding-top: 3px;\
				}\
    			input[type=submit], input[type=button]:not(#enbut), button {\
	    			/*width: auto*/\
	    			min-width: 128px;\
	    			cursor: pointer;\
    			}\
    			textarea {\
    				resize: vertical;\
    				min-height: 75px !important;\
    				font-family: inherit;\
    			}\
    			option {\
	    			background-color: inherit;\
	    			border: none;\
	    			border-radius: 0;\
    			}\
    			.text_holder { /*Make container elements wide enough to fit 10 columns of items*/\
    				width: 746px;\
    				padding: 10px;\
    				color: #dddddd;\
    			}\
    			.ncbutton {\
    				float: none;\
    				display: inline-block;\
    				text-align: right;\
    				margin-bottom: 5px;\
    			}\
				.switch-field { /*Fancy toggles*/\
					padding: 10px 0;\
					overflow: hidden;\
    			}\
    			.switch-title {\
					margin-bottom: 6px;\
    			}\
    			.switch-field input {\
					display: none;\
    			}\
    			.switch-field label {\
					display: inline-block;\
					min-width: 100px;\
					background-color: #4d4d4d;\
					color: rgba(255, 255, 255, 0.6);\
					font-size: 14px;\
					font-weight: normal;\
					text-align: center;\
					text-shadow: none;\
					padding: 6px 14px;\
    			    cursor: pointer;\
    			}\
    			.switch-field input:checked + label {\
					background-color: #CF6A32;\
					color: rgba(0, 0, 0, 0.6);\
    			}\
    			.switch-field input[disabled] + label {\
    				opacity: 0.5;\
    			}\
    			.switch-field label:first-of-type {\
					border-radius: 4px 0 0 4px;\
    			}\
    			.switch-field label:last-of-type {\
					border-radius: 0 4px 4px 0;\
    			}\
    			td:first-child > .raffle_infomation {\
    				display: block;\
    			}\
    			.infitem {\
    				max-width: 250px;\
    				position: absolute;\
    			}\
    			.infitem > .infname {\
    				background-color: transparent;\
    				white-space: normal;\
    			}\
    			.infitem > .infdesc {\
    				padding: 0;\
    				list-style: none;\
    				white-space: normal;\
    			}\
    			.userfeedpost { /* Replaces old js based height calculation */ \
    				max-height: 400px;\
    			}\
    			/* Replaces old jquery colour animations that broke a lot */\
    			.transitions .userfeedpost, .transitions .pentry, .transitions .pubrhead, .transitions .pubrcont {\
    				animation: fadeInDown 0.3s ease-out, RestoreColour 3.0s ease-out;\
    				animation-fill-mode: forwards;\
    			}\
    			.transitions .switch-field label {\
					transition: all 0.1s ease-in-out;\
    			}\
    			'
			)
		);
	},

	addNewUI: function() {
		if(NoName.Storage.get('transitions', true)) {
			$(document.body).addClass('transitions');
		}

		try {
			//ScrapTF mode cooldown
			unsafeWindow.$('#sendfeed').bind('click', function(e) {
				if(!NoName.ScrapTF.canComment()) {
					e.stopImmediatePropagation();

					return false;
				}
			});
		} catch (e) {
			console.warn('[Raffle:addNewUI] Unable to add unsafeWindow event handler');
		}

		$('table table.raffle_infomation').each(function() {
			$(this).removeClass('raffle_infomation');
			$(this).parent().addClass('raffle_infomation');
		});
	},

	//Creates a toggle switch that can replace radio buttons
	createSwitch: function(label, options) {
		var $container = $('<div></div>').addClass('switch-field'),
		children = [];

		children.push($('<div></div>').addClass('switch-title').text(label));

		options.forEach(function(option) {
			children.push(
				$('<input />')
				.prop({
					type: 'radio',
					name: option.name,
					id: option.id,
					checked: !!option.checked
				}).val(option.value)
			);
			children.push(
				$('<label></label>')
				.prop('for', option.id)
				.text(option.label)
			);
		});

		$container.append(children);

		return $container;
	},
};

//Profile pages
window.NoName.Profile = {
	$feedbackType: null,
	$progress: null,

	init: function() {
		this.$progress = $('table tr:nth-child(2) > td > table tr:nth-child(2) > td:nth-child(2)');
		this.$rep = $('.upvb');
		this.$name = $('td:nth-child(2) > div > a');

		this.removeExistingUI();
		this.addStyles();
		this.addNewUI();
	},

	removeExistingUI: function() {
		this.$feedbackType = $('#type1').parent();
		this.$feedbackType.empty();
	},
 
	addStyles: function() {
		$(document.head).append(
			$('<style></style>').text(
				'.nfbutton {\
					margin: 10px 0;\
				}\
				.userfeed, .newfeed {\
					clear: both;\
				}\
				.newfeed .switch-field {\
					padding: 0;\
					margin: 3px 0;\
				}\
				#progress > div {\
					padding: 0 !important;\
					overflow: hidden;\
					height: 23px !important;\
					position: relative;\
					border-radius: 3px;\
					padding: 1px;\
				}\
				#progress div div {\
					position: absolute;\
					top: 0 !important;\
					left: 1px;\
					bottom: 0;\
					margin: auto;\
					line-height: 21px;\
					height: 21px;\
					border-radius: 3px 0 0 3px;\
					max-width: calc(100% - 2px);\
					text-align: center;\
				}\
				#progress div div + div {\
					right: 1px;\
					color: #ffffff;\
				}\
				.transitions .userfeedpost:hover {/* Fix feedback hover background colours */\
    			    animation-fill-mode: none;\
    			    animation-duration: 0s;\
    			}\
    			/* Prevent the rep <td> going full-width*/\
    			table tr:nth-child(3) td:nth-child(2) {\
    				display: inline-block;\
    			}\
			')
		);
	},

	addNewUI: function() {
		var progress = this.calculateProgression();

		//Replace current type radio buttons with switch
		$feedbackSwitch = NoName.UI.createSwitch('', [
			{
				name: 'type',
				id: 'type1',
				label: 'Positive',
				value: '1',
			},
			{
				name: 'type',
				id: 'type2',
				label: 'Negative',
				value: '2',
			},
			{
				name: 'type',
				id: 'type0',
				label: 'Neutral',
				value: '0',
				checked: true,
			},
		]);

		this.$feedbackType.append($feedbackSwitch);
		this.$feedbackType.prev().text('Type:'); //Add : for consistency

		//Fix progress bar value and update colour to match username colour
		this.$progress.prop('id', 'progress');
		this.$progress.find('div > div').first().css({
			'background-color': this.$name.css('color'),
			'font-size': 0,
			width: Math.min(progress, 100) + '%',
			'border-radius': (progress >= 100) ? '3px' : '',
		}).next().text(progress.toFixed() + '%');
	},

	calculateProgression: function() {
		var rep = parseInt(this.$rep.text()),
		progress = 0;

		if(rep < 1000) {
		    progress = rep / 1000;
		} else if(rep < 2500) {
	    	progress = (rep - 1000) / 1500;
		} else if(rep < 5000) {
	    	progress = (rep - 2500) / 2500;
		} else {
		   	progress = rep / 5000;
		}

		progress *= 100;
		
		return progress;
	},
};

//Settings page
window.NoName.Settings = {
	$settings: null,

	init: function() {
		this.removeExistingUI();
		this.addStyles();
		this.addNewUI();

		this.addSettingsContainer();
		this.addScriptSettings();
	},

	removeExistingUI: function() {
		//Remove old radio buttons for raffle icon position
		$('#fselec').next().nextAll().remove();
		$('#fselec').prev().remove();
	},

	addStyles: function() {
		$(document.head).append(
			$('<style></style>').text(
				'table {\
					border-collapse: collapse;\
				}\
				.raffle_infomation form {\
					position: relative;\
				}\
				.raffle_infomation table {\
					width: 100%;\
				}\
				.raffle_infomation td { /* Make column widths consistent */\
					width: 49%; /* Whitespace pls */\
					display: inline-block;\
				}\
				.raffle_infomation h1 + strong {\
					display: block;\
					margin-bottom: 20px;\
					margin-top: -10px;\
				}\
				.raffle_infomation td > img { /* Tweak positioning and size of sidepic preview */\
					display: block;\
					max-width: 100%;\
					height: auto;\
					margin: auto;\
				}\
				#fselec { /* Hide shitty unstylable file input and position it over the placeholder */\
					position: absolute;\
					left: 0;\
					right: 0;\
					width: 100%;\
					height: 40px;\
					top: 0;\
					cursor: pointer;\
					opacity: 0;\
					z-index: 10;\
				}\
				#fname { /* Nicer looking placeholder for file input */\
					font-family: TF2Build;\
					color: #EBE2CA;\
					font-size: 14px;\
					line-height: 40px;\
					max-width: 300px;\
					overflow: hidden;\
					white-space: no-wrap;\
					text-overflow: ellipsis;\
					display: block;\
				}\
				#tl + label {\
					border-radius: 4px 0 0;\
				}\
				#tr + label {\
					border-radius: 0 4px 0 0;\
				}\
				#bl + label {\
					border-radius: 0 0 0 4px;\
				}\
				#br + label {\
					border-radius: 0 0 4px;\
				}\
				#noname-settings .switch-field {\
					display: inline-block;\
					width: 49%;\
				}\
				'
			)
		);
	},

	addNewUI: function() {
		var $iconWarning = $('#fselec').closest('.raffle_infomation').find('h3'),
		$position = NoName.UI.createSwitch('Icon position:', [
			{
				name: 'position',
				id: 'tl',
				label: 'Top-left',
				value: 'tl',
				checked: true,
			},
			{
				name: 'position',
				id: 'tr',
				label: 'Top-right',
				value: 'tr',
			},
			{
				name: 'position',
				id: 'bl',
				label: 'Bottom-left',
				value: 'bl',
			},
			{
				name: 'position',
				id: 'br',
				label: 'Bottom-right',
				value: 'br',
			}
		]),
		$fileName = $('<span></span>').prop('id', 'fname').text('Click to choose file');

		//Add filename placeholder and position switch
		$('#fselec').after($position.hide()).after($fileName);

		//Update placeholder text when hidden input changes
		$('#fselec').on('change', function() {
			var file = this.files[0];

			if(file) {
				$position.show();
				$fileName.text(file.name);
			} else {
				$position.hide();
				$fileName.text('Click to choose file');
			}
		});

		//Reformat sidepic warning in a nicer looking way
		$iconWarning.replaceWith(
			$('<strong></strong>').html($iconWarning.text().replace('	Use', '<br />Use'))
		);
	},

	//Add a new section for our own settings
	addSettingsContainer: function() {
		this.$settings = $('<div></div>').addClass('raffle_infomation').prop('id', 'noname-settings')
			.append(
				$('<h1></h1>').text('Userscript with no name')
			)
			.append(
				$('<strong></strong>').text('Settings for Userscript with no name')
			)
			.append(
				$('<table></table>')
			);

		$('#content > .indent').append('<br />').append(this.$settings);
	},

	//Add script settings to new section
	addScriptSettings: function() {
		var storage = NoName.Storage,
		transitionsEnabled = storage.get('transitions', true),
		showAllItemsEnabled = storage.get('showallitems', false),
		scrapEnabled = storage.get('scrap:enabled', false),

		//Transitions
		$transitions = NoName.UI.createSwitch('Animations: ', [
			{
				name: 'transitions',
				id: 'transitions-disable',
				label: 'Disabled',
				value: '',
				checked: !transitionsEnabled,
			},
			{
				name: 'transitions',
				id: 'transitions-enable',
				label: 'Enabled',
				value: true,
				checked: transitionsEnabled,
			}
		]);

		//Show all items in raffle list
		$showAllItems = NoName.UI.createSwitch('Raffle list - Show all items: ', [
			{
				name: 'show-all-items',
				id: 'show-all-items-disable',
				label: 'Disabled',
				value: '',
				checked: !showAllItemsEnabled,
			},
			{
				name: 'show-all-items',
				id: 'show-all-items-enable',
				label: 'Enabled',
				value: true,
				checked: showAllItemsEnabled,
			}
		]);

		//ScrapTF mode
		$scrap = NoName.UI.createSwitch('ScrapTF Mode (Chrome only)', [
			{
				name: 'scraptfmode',
				id: 'scraptf-disable',
				label: 'Disabled',
				value: '',
				checked: !scrapEnabled,
			},
			{
				name: 'scraptfmode',
				id: 'scraptf-enable',
				label: 'Enabled',
				value: true,
				checked: scrapEnabled,
			}
		]),

		//Update body class to toggle transitions
		$transitions.on('change', function(e) {
			//TODO: maybe change storage.listen to fire in the same tab
			//Would save duplication like this
			if(e.target.value) {
				$(document.body).addClass('transitions');
			} else {
				$(document.body).removeClass('transitions');
			}

			storage.set('transitions', e.target.value);
		});

		$showAllItems.on('change', function(e) {
			storage.set('showallitems', e.target.value);
		});

		//Enable scrapTF mode or check if it can be disabled
		$scrap.on('change', function(e) {
			if(e.target.value) {
				NoName.ScrapTF.enable();
			} else if(!NoName.ScrapTF.disable()) {
				$('#scraptf-enable').prop('checked', true);
			}
		});

		this.$settings.append($transitions).append($showAllItems).append($scrap);
	}
};

//Emulates the... interesting choice to add sizeable cooldowns to everything in scrapTF
//Would be a good idea to not take this seriously, just saying, let me have my fun.
//TODO: Auctions?
window.NoName.ScrapTF = {
	enabled: 0,
	lastEntry: 0,
	lastComment: 0,

	COOLDOWN_RAFFLE: 10,
	COOLDOWN_COMMENT: 30,
	COOLDOWN_DISABLE: 30,

	init: function() {
		var that = this;

		this.enabled = NoName.Storage.get('scrap:enabled', 0);
		this.lastEntry = NoName.Storage.get('scrap:lastentry', 0);
		this.lastComment = NoName.Storage.get('scrap:lastcomment', 0);

		//Opening multiple tabs will not save you
		NoName.Storage.listen('scrap:enabled', function(oldValue, newValue, url) {
			that.enabled = newValue;
		});

		NoName.Storage.listen('scrap:lastentry', function(oldValue, newValue, url) {
			that.lastEntry = newValue;
		});

		NoName.Storage.listen('scrap:lastcomment', function(oldValue, newValue, url) {
			that.lastComment = newValue;
		});
	},

	isEnabled: function() {
		return !!this.enabled;
	},

	enable: function() {
		var now = new Date().getTime();

		if(this.enabled) {
			return false;
		}

		NoName.Storage.set('scrap:enabled', now);
		this.enabled = now;

		alert('ScrapTF mode enabled');
	},

	//Dunno why you would want to turn it off but here you go
	disable: function() {
		var now = new Date().getTime(),
		difference = (now - this.enabled) / 1000,
		remaining = this.COOLDOWN_DISABLE - difference;

		//No quick escape for you
		if(remaining > 0) {
			alert('Please wait ' + remaining.toFixed(0) + ' seconds to disable ScrapTF mode.');

			return false;
		} else {
			this.enabled = 0;
			NoName.Storage.set('scrap:enabled', '');

			alert('ScrapTF mode disabled');

			return true;
		}
	},

	//Recent development
	//Luckily we aren't a bot or we would be so screwed here!
	canEnterRaffle: function() {
		var now = new Date().getTime(),
		difference = (now - this.lastEntry) / 1000,
		remaining = this.COOLDOWN_RAFFLE - difference;

		if(!this.enabled) {
			return true;
		}

		if(remaining > 0) {
			console.warn('[ScrapTF::canEnterRaffle] Blocking raffle entry');
			alert('Please wait ' + remaining.toFixed(0) + ' seconds to enter this raffle.');
			return false;
		}

		this.lastEntry = now;
		NoName.Storage.set('scrap:lastentry', now);

		return true;
	},

	//Stop spamming pls
	canComment: function() {
		var now = new Date().getTime(),
		difference = (now - this.lastComment) / 1000,
		remaining = this.COOLDOWN_DISABLE - difference;

		if(!this.enabled) {
			return true;
		}

		if(remaining > 0) {
			console.warn('[ScrapTF::canComment] Blocking comment');
			alert('Please don\'t spam');
			return false;
		}

		NoName.Storage.set('scrap:lastcomment', now);
		this.lastComment = now;

		return true;
	}
};

//New raffle page
window.NoName.NewRaffle = {
	$itemList: null,
	$selectedItemList: null,
	$oldItems: null,
	$banWarning: null,

	$visibility: null,
	$entry: null,
	$type: null,
	$start: null,

	$submit: null,

	backpack: null,
	levelData: {},

	init: function() {
		var that = this;

		this.$itemList = $('#allitems');
		this.$selectedItemList = $('#selitems');
		this.$banWarning = $('.ban_warning');

		this.$visibility = $('#ptype1').parent();
		this.$entry = $('#ptype2').parent();
		this.$type = $('#af1').parent();
		this.$start = $('#af2').parent();

		this.$submit = $('#rafBut').parent();

		this.removeExistingUI();
		this.addStyles();
		this.addSwitches();
		this.addNewUI();

		this.backpack = new NoName.Backpack({
			container: this.$itemList,
			selectedContainer: this.$selectedItemList,
			autoRender: true,
			autoLoad: true,
			selectableItems: true,
		});
	},

	addStyles: function() {
		$(document.head).append(
			$('<style></style>').text(
				'@keyframes popin {\
					0% {\
						transform: scale(0);\
					}\
					100% {\
						transform: scale(1);\
					}\
				}\
				.indent {/*Make container elements wide enough to fit 10 columns of items*/\
    				width: 786px;\
    				padding: 0;\
    			}\
    			#content {\
	    			width: 786px;\
	    			padding: 5px 0;\
    			}\
    			#nav_holder {\
    				width: 786px;\
    			}\
    			.text_holder table { /*Make table full width in firefox*/\
    				width: 100%;\
    			}\
				.itemtable { /*Fix weird position:relative behaviour*/\
					position: static;\
					text-align: center;\
					padding-bottom: 15px;\
				}\
				.itemtable ol:empty, .itemtable.loading ol, .itemtable.error ol {/*Add minimum height when itemtables dont contain items*/\
					height: 75px;\
				}\
				.itemtable.minimised ol {\
					height: 100px;\
					overflow: hidden;\
				}\
				.itemtable.minimised ol:after {\
					content: "Show all items";\
					display: block;\
					width: 100%;\
					bottom: 0;\
					height: 40px;\
					line-height: 40px;\
					background-color: #2A2725;\
					clear: both;\
				}\
				.itemtable.minimised ol:hover:after {\
					text-decoration: underline;\
					cursor: pointer;\
				}\
				.itemtable a {\
					cursor: pointer;\
				}\
				.itemtable ol { /*Remove default list style*/\
					padding: 10px 0 15px;\
					list-style: none;\
					margin: 0;\
					overflow: auto;\
					/*position: relative;*/\
				}\
				.itemtable ol:before {\
					display: inline-block;\
					width: 100%;\
					text-align: center;\
				}\
				#allitems.loading ol:empty:before {\
					content: "Loading backpack...";\
				}\
				#allitems ol:empty:before {\
    				content: "No selectable items in backpack";\
    			}\
    			#selitems ol:empty:before {\
    				content: "No items selected";\
    			}\
    			#mess {\
	    			width: 100% !important; /*Override styles added by "New raffle page enhanced" script*/\
	    			max-width: none !important;\
    			}\
    			#raffle-button {\
    				margin-top: 25px;\
    			}\
    			.text_holder > table tr:nth-child(9) > td {/*Add a bit of space above the toggles*/\
    				padding-top: 15px;\
    			}\
    			.text_holder > table tr:nth-child(7) > td:nth-child(3),\
    			.text_holder > table tr:nth-child(8) > td:nth-child(2),\
    			.text_holder > table tr:nth-child(9) > td:nth-child(2),\
    			.text_holder > table tr:nth-child(10) > td:nth-child(2) {\
    				padding-left: 10px;\
    			}\
    			.transitions .itemtable .item {\
					animation: popin 0.15s cubic-bezier(.17,.67,.57,1.42);\
    			}\
    			'
			)
		);
	},

	addSwitches: function() {
		var $visibility = NoName.UI.createSwitch('Raffle visiblity:', [
			{
				name: 'rafflepub',
				id: 'ptype1',
				label: 'Public',
				value: 'public',
				checked: true,
			},
			{
				name: 'rafflepub',
				id: 'ptype2',
				label: 'Private',
				value: 'private',
			}
		]);

		var $entry = NoName.UI.createSwitch('Entry type:', [
			{
				name: 'invo',
				label: 'Open',
				id: 'af1',
				value: 'false',
				checked: true,
			},
			{
				name: 'invo',
				id: 'af2',
				label: 'Invite only',
				value: 'true',
			}
		]);

		var $type = NoName.UI.createSwitch('Prize distribution:', [
			{
				name: 'split',
				id: 'isplit1',
				label: 'A21',
				value: 'alltoone',
			},
			{
				name: 'split',
				id: 'isplit2',
				label: '121',
				value: 'onerperson',
				checked: true
			}
		]);

		var $start = NoName.UI.createSwitch('Start timer:', [
			{
				name: 'stype',
				id: 'stype1',
				label: 'Instantly',
				value: 'instantly',
			},
			{
				name: 'stype',
				id: 'stype2',
				label: 'After first entry',
				value: 'afterjoin',
				checked: true
			}
		]);

		//Add radio button replacement toggles
		this.$visibility.append($visibility);
		this.$entry.append($entry);
		this.$type.append($type);
		this.$start.append($start);
	},

	addNewUI: function() {
		//Detach entries and referer, also add :s for consistency
		var that = this,
		$entries = [
			$('#maxentry').parent().prev().text('Maximum entries:').detach(),
			$('#maxentry').parent().detach()
		],

		$referer = [
			$('<td></td>').prop('colspan', 2),
			$('#reffil').parent().prev().text('Referal filter:').detach(),
			$('#reffil').parent().detach()
		];

		//Move entries after duration
		$('#durr').parent().after($entries);

		//Move referer to a new tr after duration/entries
		$('#durr').closest('tr').after(
			$('<tr></tr>').append($referer)
		);

		//Change defaults and other attributes to more sensible values
		$('#rtitle').addClass('full-width').prop({
			placeholder: 'Raffle title',
			maxlength: 32,
			onclick: null,
		}).val('');

		$('#mess').parent().prop('colspan', 3);
		$('#mess').prop({
			maxlength: 2048,
		});

		$('#durr').addClass('full-width').val(3600);

		$('#maxentry').addClass('full-width').prop({
			type: 'number',
		}).val(1000);

		$('#reffil').addClass('full-width').prop('placeholder', '*').val('');

		//Make selected items style consistent with backpack items
		this.$selectedItemList.addClass('itemtable');

		//Add <colgroup> to form table to make column widths consistent
		$('.text_holder table').prepend(
			$('<colgroup></colgroup>').append([
				$('<col />').css('width', '19%'), //Account for padding on 3rd column. Not nice but calc() doesn't work properly here.
				$('<col />').css('width', '30%'),
				$('<col />').css('width', '20%'),
				$('<col />').css('width', '30%'),
			])
		);

		//TODO: perhaps just overwrite the existing create function via unsafeWindow?
		this.$submit.append(
			$('<button></button>').prop({
				type: 'button',
				id: 'raffle-button' //Different id to prevent old event handler triggering
			}).addClass('full-width')
			.text('Raffle it!')
			.on('click', function() {
				that.createRaffle();
			})
		);

		//Lock visibility to private when invite only is selected
		$('#af1, #af2').on('change', function() {
			if(this.value === 'true') {
				$('#ptype2').prop({
					checked: true,
				});
			}

			$('#ptype1, #ptype2').prop('disabled', this.value === 'true');
		});
	},

	removeExistingUI: function() {
		//Remove games selection
		$('#allgames').parent().prev().remove();
		$('#allgames').parent().remove();

		//Remove remaining unneeded radio button <tr>s
		$('#isplit1').closest('tr').remove();
		$('#stype1').closest('tr').remove();

		//I'm sure anyone using this already knows the rules
		this.$banWarning.remove();

		//Empty things we are going to replace
		this.$visibility.empty();
		this.$entry.empty();
		this.$type.empty();
		this.$start.empty();

		//Remove existing button so I can readd it again without existing event handlers
		$('#rafBut').remove();
		$('.infitem').remove();
		$('#rtitle').removeAttr('size');
	},

	createRaffle: function() {
		var items = [];

		$('#raffle-button').prop('disabled', true).val('Please wait...');

		this.backpack.getSelected().forEach(function(item) {
			data = [
				item.getDefIndex(),
				item.getQuality(),
				item.getLevel(),
				''//item.getSeries() //This is always empty in the original inventory apparently
			];

			items.push(data.join(':'));
		});

		$.ajax({
			type: 'POST',
			url: 'http://tf2r.com/job.php',
			dataType: 'json',
			data: {
				postraffle: 'true',
				title: $('#rtitle').val(),
				message: $('#mess').val(),
				maxentry: $('#maxentry').val(),
				duration: $('#durr').val(),
				filter: $('#reffil').val() || '*',
				split: $('input[name=split]:checked').val(),
				pub: $('input[name=rafflepub]:checked').val(),
				stype: $('input[name=stype]:checked').val(),
				invo: $('input[name=invo]:checked').val(),
				items: items,
				games: [],
			}
		}).done(function(data){
			if(data.status == 'fail') {
				alert(data.message);

				$('#raffle-button').prop('disabled', false).val('Raffle it!');
			} else if(data.status == 'ok') {
				//TODO: track raffled items here

				window.location.href = 'http://tf2r.com/k' + data.key;
			}
		});
	},
};

window.NoName.RaffleList = {
	init: function() {
		var that = this;

		this.removeExistingUI();
		this.addStyles();
		this.addNewUI();

		//Update item lists if show all setting changes
		NoName.Storage.listen('showallitems', function(oldValue, newValue, url) {
			unsafeWindow.getItems();
		});
	},

	exportOverrides: function() {
		var that = this;

		//Override getItems function to add +x overflow and optional showing of all items
		unsafeWindow.getItems = exportFunction(function() {
			return that.getItemsOverride();
		}, unsafeWindow);

		//Override check raffles function to remove display: none from raffle header
		unsafeWindow.checkraffles = exportFunction(function() {
			return that.checkrafflesOverride();
		}, unsafeWindow);
	},

	//TODO: This repeats a lot of what getitems() does
	//Can they be merged?
	checkrafflesOverride: function() {
		var that = this;

		if(!unsafeWindow.focused) {
			setTimeout(unsafeWindow.checkraffles, 5000);

			return;
		}


		$.ajax({
			type: 'post',
			url: 'http://tf2r.com/job.php',
			dataType: 'json',
			data: {
				'checkpublicraffles': 'true',
				'lastpraffle': unsafeWindow.lpr,
			},
			success: function(data) {
				if(data.status != 'ok') {
					alert(data.message);

					return;
				}

				if(data.message.newraf.length) {
					that.populateRaffles(data.message.newraf);
				}

				unsafeWindow.ih();
			}
		});

		setTimeout(unsafeWindow.checkraffles, 5000);
	},

	getItemsOverride: function() {
		var list = [],
		that = this;

		$('.jqueryitemsgather').each(function(index, object) {
			list[index] = $(object).attr('rqitems');
		});

		$.ajax({
			type: 'post',
			url: 'http://tf2r.com/job.php',
			dataType: 'json',
			data: {
				'getitems': 'true',
				'list': list.join(';'),
			},
			success: function(data){
				if(data.status != 'ok') {
					alert(data.message);

					return;
				}


				if(data.message.items) {
					that.populateRaffleItems(data.message.items);
				}
			}
		});
	},

	populateRaffles: function(raffles) {
		var showAll = NoName.Storage.get('showallitems', false);

		for(id in raffles) {
			var ent = raffles[id],
			itemlist = '',
			remaining = 0,
			wid = 644,
			$header,
			$content;

			for(var iid in ent.items) {
				var eent = ent.items[iid];

				//Leave space for "+x" if show all items is disabled
				if(!showAll && (wid -= eent.wid) <= 74) {
					remaining++;
					continue;
				}

				itemlist += unsafeWindow.getItem(eent);
			}

			if(unsafeWindow.lpr < ent.id) {
				unsafeWindow.lpr = ent.id;
			}

			//Not proud of this but theres a lot of html to build

			//Raffle header
			$header = $('<div></div>').addClass('pubrhead').append(
				$('<div></div>').addClass('pubrhead-text-left').append(
					$('<a></a>') //Username
					.prop('href', ent.link)
					.css('color', '#' + ent.color)
					.text(ent.name)
				).append(
					$('<div></div>').addClass('pubrhead-text-right').append(
						$('<a></a>').prop('href', ent.rlink).text(ent.rname) //Raffle name
					)
				).append(
					$('<div></div>').addClass('pubrhead-arrow-border')
				).append(
					$('<div></div>').addClass('pubrhead-arrow')
				)
			);

			//Raffle content
			$content = $('<div></div>').addClass('pubrcont').append(
				$('<div></div>').addClass('pubrleft').append(
					$('<div></div>').addClass('pubrav').append(
						$('<a></a>').prop('href', ent.link).append(
							$('<img />').prop('src', ent.avatar).css({ //User avatar
								width: '64px',
								height: '64px',
							})
						)
					)
				).append(
					$('<div></div>').addClass('pubrarro')
				)
			).append(
				$('<div></div>').addClass('pubrright').html(itemlist) //Items
				.attr('data-overflow', (remaining && !showAll) ? ('+' + remaining) : undefined)
			);

			$('.participants').prepend('<div class="clear"></div>').prepend($content).prepend($header);
		}
	},

	populateRaffleItems: function(items) {
		var showAll = NoName.Storage.get('showallitems', false);

		$('.jqueryitemsgather').empty().each(function() {
			var width = $(this).width() - 74,
			raffle = $(this).attr('rqitems'),
			remaining = 0;

			$(this).removeAttr('data-overflow');

			for(var id in items) {
				var ent = items[id];

				if(ent.rkey != raffle) {
					continue;
				}

				//Leave space for "+x" if show all items is disabled
				if(!showAll && (width -= ent.wid) <= 74) {
					remaining++;
					continue;
				}

				$(this).append(unsafeWindow.getItem(ent));
			}

			//Add +x if there are any undisplayed items
			if(remaining && !showAll) {
				$(this).attr('data-overflow', '+' + remaining);
			}
		});
	},

	removeExistingUI: function() {

	},

	addStyles: function() {
		$(document.head).append(
			$('<style></style>').text(
				'.pubrright{\
    				max-width: 86%;\
    				width: 86%;\
    				position: relative;\
    				background-color: inherit;\
    				margin-bottom: 7px;\
    			}\
    			.pubrright[data-overflow]:after {\
    				content: attr(data-overflow);\
    				position: absolute;\
    				top: 0;\
    				right: 0;\
    				display: block;\
    				width: 74px;\
    				background-color: inherit;\
    				margin: 3px;\
    				height: 68px;\
    				line-height: 68px;\
    				font-family: TF2Build;\
    				text-align: center;\
    				font-size: 32px;\
    				color: #837768;\
    				cursor: pointer;\
    			}\
    			.pubrright[data-overflow]:hover:after {\
    				text-shadow: 1px 1px 1px #FF6407;\
    			}\
    			.pubrarro {\
    				left: 80px;\
    			}\
    		')
    	);
	},

	addNewUI: function() {

	},
};

window.NoName.Raffle = {
	raffleID: '',

	init: function() {
		var that = this;

		this.raffleID = window.location.pathname.substring(2, 8);

		this.removeExistingUI();
		this.addStyles();
		this.addNewUI();
	},

	exportOverrides: function() {
		var that = this;

		//Override checkraffle to fix some things
		unsafeWindow.checkraffle = exportFunction(function() {
			return that.checkRaffleOverride();
		}, unsafeWindow);
	},

	checkRaffleOverride: function() {
		$.ajax({
			type: 'post',
			url: 'http://tf2r.com/job.php',
			dataType: 'json',
			data: {
				'checkraffle': 'true',
				'rid': this.raffleID,
				'lastentrys': unsafeWindow.entryc,
				'lastchat': unsafeWindow.lastchat
			}
		}).done(function(data) {
            var ent;

			if(data.status != 'ok') {
				alert(data.message);

				return;
			}

			if(data.message.ended && !unsafeWindow.ended) {
				window.location.reload();
			}

			$('#entry').html(data.message.cur_entry + '/' + data.message.max_entry);

			unsafeWindow.entryc = data.message.entry;
			unsafeWindow.lastchat = data.message.chatmax;

			unsafeWindow.tleft = data.message.timeleft;
			unsafeWindow.nwc = data.message.wc;

			if(!unsafeWindow.started && data.message.started) {
				unsafeWindow.started = true;
				unsafeWindow.updateTimer();
			}

			//Replaced comment container height calculation with css max-height

			//Removed display: none here, it isnt needed now we have css transitions
			//TODO: Make this less terrible
			for(var id in data.message.chaten) {
				ent = data.message.chaten[id];
				$('.userfeed').prepend('<div class="userfeedpost" style="background-color:#' + ent.color + ';"><div class="ufinf"><div class="ufname"><a href="' + ent.url + '" style="color:#' + ent.color + ';">' + ent.name + '</a></div><div class="ufavatar"><a href="' + ent.url + '"><img src="' + ent.avatar + '"></a></div></div><div class="ufmes">' + ent.message + '</div></div>');
			}

			for(id in data.message.newentry) {
				ent = data.message.newentry[id];

				if(unsafeWindow.lastname != ent.name) {
					$('#pholder').prepend('<div class="pentry"><div class="pavatar"><a href="' + ent.link + '"><img src="' + ent.avatar + '" width="64px" height="64px" /></a></div><div class="pname"><a href="' + ent.link + '" style="color:#' + ent.color + ';">' + ent.name + '</a></div></div>');
				}

				unsafeWindow.lastname = ent.name;
			}
		});

		setTimeout(unsafeWindow.checkraffle, (unsafeWindow.ended) ? 5000 : 3500);
	},

	removeExistingUI: function() {

	},

	addStyles: function() {
		$(document.head).append(
			$('<style></style>').text(
				'.newfeed {\
					padding: 10px;\
				}\
				div[style="height:30px;"] {\
    				height: auto !important;\
    				margin-bottom: 5px;\
    				text-align: right;\
    			}\
    			.newfeed td[width="70px"] {\
    				width: 100px;\
    				padding-right: 10px;\
    			}\
    			.newfeed td[width="660px"] {\
    				width: 650px;\
    			}\
    			.raffle_infomation td[colspan="3"] { /* Show newlines in descriptions */\
    				white-space: pre-line;\
    			}\
    			tr:nth-child(7) > .raffle_infomation { /* Ensure raffle prizes container fits 10 items per row */\
    				min-width: 740px;\
    			}\
    			#load-comments {\
    				width: 100%;\
    			}\
    			/* Prevent rep <td> going full-width */\
    			table tr:nth-child(5) > .raffle_infomation + .raffle_infomation {\
    				display: inline-block;\
    			}\
    		')
    	);
	},

	addNewUI: function() {
		var that = this;

		//ScrapTF mode cooldown
		try {
			unsafeWindow.$('#enbut').bind('click', function(e) {
				if(!NoName.ScrapTF.canEnterRaffle()) {
					e.stopImmediatePropagation();

					return false;
				}
			});
		} catch (e) {
			console.warn('[Raffle:addNewUI] Unable to add unsafeWindow event handler');
		}

		//Add load more comments button if there are 50 comments
		if($('.userfeedpost').length == 50) {
			$('.userfeed').append(
				$('<button></button>').prop('id', 'load-comments').text('Load more comments...').on('click', function() {
					that.loadComments();
				})
			)
		}
	},

	loadComments: function() {
		var that = this;

		$('#load-comments').prop('disabled', true).text('Loading...');

		$.ajax({
			type: 'post',
			url: 'http://tf2r.com/job.php',
			dataType: 'json',
			data: {
				'checkraffle': 'true',
				'rid': this.raffleID,
				'lastentrys': unsafeWindow.entryc,
				'lastchat': 0 //All comments
			},
			success: function(data) {
				if(data.status != 'ok') {
					alert(data.message);
				}									
				
				//Remove first 50 comments that are already on the page
				var comments = data.message.chaten.slice(0, -50).reverse();
				that.addComments(comments);

				$('#load-comments').remove();
			}
		});
	},

	addComments: function(comments) {
		for(id in comments) {
			var ent = comments[id],

			//Comment container
			$container = $('<div></div>').addClass('userfeedpost').css({
				'background-color': '#' + ent.color,
			}),

			//Username
			$username = $('<div></div>').addClass('ufinf').append(
				$('<div></div>').addClass('ufname').append(
					$('<a></a>').prop('href', ent.url).css({
						color: '#' + ent.color,
					}).text(ent.name) 
				)
			),

			//Avatar
			$avatar = $('<div></div>').addClass('ufavatar').append(
				$('<a></a>').prop('href', ent.url).append(
					$('<img />').prop('src', ent.avatar)
				)
			),

			//Message
			$message = $('<div></div>').addClass('ufmes').html(ent.message); //Already sanitised

			//Append them all together
			$container.append($username.append($avatar)).append($message);
			$('.userfeed').append($container);
		}
	}
};

window.NoName.Steam = {
	steamID: '',
	JSON_URL: '',

	init: function() {
		this.getSteamID();
		this.JSON_URL = 'http://steamcommunity.com/profiles/' + this.steamID + '/inventory/json/440/2/';
	},

	getSteamID: function() {
		try {
			var result = $('#avatar > a').first().prop('href').match(/https?:\/\/tf2r.com\/user\/(\d+)\.html/);

			this.steamID = result[1];
		} catch(e) {
			console.warn('[Steam::getSteamID] Unable to determine steamID');
		}
	},

	fetchInventoryJSON: function() {
		var defer = jQuery.Deferred();

		if(!this.steamID) {
			defer.reject();

			return defer;
		}

		GM_xmlhttpRequest({
            method: 'GET',
            url: this.JSON_URL,
            onload: function(response) {
            	defer.resolve(response.responseText);
            },
            onerror: function(response) {
				console.error('[Steam::fetchBackpack] Failed to retrieve inventory JSON: ' + response.textStatus);
				defer.reject();
            }
        });

		return defer;
	}
};

//Object that handles parsing and displaying of user's backpack
window.NoName.Backpack = function(options) {
	var that = this;

	this.selectableItems = !!options.selectableItems;
	this.autoRender = !!options.autoRender;
	this.autoLoad = !!options.autoLoad;

	this.$container = options.container;
	this.$selectedContainer = options.selectedContainer;
	this.$info = null;

	this.items = [];
	this.selectedItems = [];
	this.levelData = {};

	if(!this.$selectedContainer || !this.$selectedContainer.length) {
		console.error('[Backpack] $container does not exist');

		return false;
	}

	function _initElements() {
		that.$container.empty().append($('<ol></ol>'));

		if(that.selectableItems) {
			if(!that.$selectedContainer || !that.$selectedContainer.length) {
				console.error('[Backpack] $selectedContainer must exist for items to be selectable');

				return false;
			}

			that.$selectedContainer.empty().append($('<ol></ol>'));

			that.$container.on('click', 'li', function() {
				that.select(this);
			});

			that.$selectedContainer.on('click', 'li', function() {
				that.deselect(this);
			});
		}

		that.$info = $('<div></div>').addClass('infitem').append([
			$('<strong></strong>').addClass('infname'),
			$('<ul></ul>').addClass('infdesc'),
		]);

		that.$container.append(that.$info);
	}

	function _initEvents() {
		that.$container.on('ei:backpackfailed', function() {
			that.$container.addClass('error');
			that.$container.append(
				$('<a></a>').text('Failed to load backpack. Click to retry.')
				.click(function() {
					that.backpack.load();
				})
			);
		});

		that.$container.on('click', 'ol', function(event) {
			if(event.target == this) {
				$(event.delegateTarget).removeClass('minimised');
				that.render(false, 10);
			}
		});

		that.$container.on('mouseover', '.item', function(event) {
			_handleHover(event);
		}).on('mouseout', '.item', function() {
			that.$info.hide();
		});

		that.$selectedContainer.on('mouseover', '.item', function(event) {
			_handleHover(event);
		}).on('mouseout', '.item', function() {
			that.$info.hide();
		});
	}

	function _handleHover(event) {
		var item = event.target.item,
		descriptions = item.getDescriptions(),
		$list = [],

		pos = $(event.target).offset(),
		height = $(event.target).height(),
		width = $(event.target).width();

		//Item name
		that.$info.children('.infname')
			.removeClass()
			.addClass('infname q' + item.getQuality())
			.text(item.getName());

		//Item level and type
		that.$info.children('.infdesc').empty().append(
			$('<li></li>').text(item.type)
		);

		//Other description strings
		descriptions.forEach(function(description) {
			$list.push(
				$('<li></li>').text(description.value).css({
					color:  (description.color) ? '#' + description.color : '#ffffff',
				})
			);
		});

		that.$info.children('.infdesc').append($list);
		that.$info.show();

		//Position popup properly
		that.$info.css({
			'left': pos.left + (width / 2) - (that.$info.width() / 2) + 'px',
			'top': pos.top + height + 'px'
		});
	}

	//Parse default item list to get levels of items that do not have a level in the steam inventory json
	function _getLevelData() {
		that.$container.find('.item').each(function() {
			var defindex = $(this).attr('iid'),
			level =  $(this).attr('ilevel'),
			quality = $(this).attr('iqual');

			//Uniques always have levels and skins never do, so no need to include them
			//Items of other qualities may be strangified, so need to include them just in case
			//Removed for now, seen some cases of unique items having no level
			// if(quality == 6 || quality == 15) {
			// 	return;
			// }

			if(!level) {
				return;
			}

			that.levelData[defindex] = that.levelData[defindex] || {};
			that.levelData[defindex][quality] = that.levelData[defindex][quality] || [];
			that.levelData[defindex][quality].push(level);
		});
	}

	//Populate items array with item objects created from parsed data
	function _populateItems(items) {
		for(var item in items) {
			item = items[item];

			that.items.push(new NoName.Item(item, that.levelData));
		}
	}

	//Use a webworker to parse the json and clean up data
	//Doing this on the main thread causes noticable lag
	this.parseJSON = function(json) {
		var that = this,
		defer = jQuery.Deferred(),

		//Create a blob from the below workerParse function to allow its use in the worker
		work = URL.createObjectURL(
			new Blob([
				'(',
		   		this.workerParse.toString(),
		   		')()'
			], {
				type: 'application/javascript'
			})
		),

		//Create worker
	    worker = new Worker(work);

		//Listen for worker response
	    worker.addEventListener('message', function(e) {
	    	if(e.data.success) {
	    		console.info('[Backpack::parseJSON] Backpack parsed');
	    		_populateItems(e.data.items);

	    		defer.resolve();
	    	} else {
    			console.error('[Backpack::parseJSON] Failed to parse backpack: ' + e.data.error);

    			defer.reject();
	    	}
	    }, false);

	    //Send the worker the json to parse
	    worker.postMessage(json);
	    URL.revokeObjectURL(work);

	    return defer;
	};

	//Used by web worker to parse the json and then restructure the parsed data
	this.workerParse = function() {
		self.addEventListener('message', function(event) {
			try {
				var data = JSON.parse(event.data),
				items = parseItems(data);

				if(items) {
					self.postMessage({success: true, items: items});
				} else {
					throw new Error('Item parsing failed');
				}

			} catch (e) {
				console.error('[Steam::workerParse] Error while parsing JSON : ' + e);
				self.postMessage({success: false, error: e.toString()});
			}
		});

		//Checks the parsed json is a valid response
		//Merges item and description arrays into a single item array
		//Removes unneeded item data
		function parseItems(data) {
			var items = data.rgInventory,
			descriptions = data.rgDescriptions,
			parsedItems = [];

			if(!data.success) {
				console.error('[Backpack::workerParse] Success property is false');

				return false;
			}

			if(!items) {
				console.error('[Backpack::workerParse] Inventory array missing');

				return false;
			}

			if(!descriptions) {
				console.error('[Backpack::workerParse] Descriptions array missing');

				return false;
			}

			for(var item in items) {
				item = items[item];

				var classInstanceId = item.classid + '_' + item.instanceid,
				description = descriptions[classInstanceId];

				parsedItems.push(parseItem(item, description));
			}

			parsedItems.sort(function(item1, item2) {
				return item1.position - item2.position;
			});

			return parsedItems;
		}

		//Parses a single item
		//Extracts level and series data where possible
		//Loops over descriptions to determine which ones to item.descriptions
		//Moved to worker as it is very slow
		function parseItem(item, description) {
			var level = description.type.match(/.*Level (\d+).*/),
			series = description.name.match(/.*Series #(\d+).*/);

			level = (level) ? level[1] : 0;
			series = (series) ? series[1] : 0;

			var parsedItem = {
				id: item.id,
				position: item.pos,
				defindex: description.app_data.def_index,
				quality: description.app_data.quality,
				name: description.name,
				type: description.type,
				level: level,
				series: series,
				tradable: description.tradable,
				descriptions: [],
				tags: [],
				thumbnail: description.icon_url + '/128fx128f', //Reduce image size
				image: description.icon_url_large,
				classes: [],
			};

			parseDescriptions(parsedItem, description.descriptions || []);
			parseTags(parsedItem, description.tags || []);

			return parsedItem;
		}

		//Loops over the description strings for an item and returns the ones we care about
		//Moved to the worker as it is slow enough to cause noticeable ui lag
		function parseDescriptions(item, descriptions) {
			descriptions.forEach(function(description) {

				//Basic killstreak
				if(description.value === 'Killstreaks Active') {
					item.descriptions.push(description);
					return;
				}

				//Specialized killstreak, Professional killstreak,
				//Gifts, Paint, Crafted, Unusual effects, Stat-clock, Festivised
				if(!description.value.indexOf('Sheen: ') ||
                   !description.value.indexOf('Killstreaker: ') ||
                   !description.value.indexOf('\nGift from: ') ||
                   !description.value.indexOf('Paint Color: ') ||
                   !description.value.indexOf('Crafted by ') ||
                   !description.value.indexOf('Festivized') ||
                   !description.value.indexOf('Strange Stat Clock Attached') ||
                   !description.value.indexOf('★ Unusual Effect')) {
					item.descriptions.push(description);
					return;
				}

				//Spells
				if(!description.value.indexOf('Halloween: ')) {
					description.value = description.value.replace('(spell only active during event)', '');

					item.descriptions.push(description);
					return;
				}

				//Custom descriptions, strange parts
				if(description.value.match(/^\'\'.*\'\'$/) ||
                   description.value.match(/^\(.*:.*\)$/)) {
					item.descriptions.push(description);
					return;
				}

				//Collection grades
				if((grade = description.value.match(/^(\w+) Grade (.*)$/)) && description.color) {
					item.descriptions.push(description);
					return;
				}
			});

			return item;
		}

		//Parse item tags that we care about
		//Not yet implemented
		function parseTags(item, tags) {
			tags.forEach(function(tag) {
				switch(tag.category) {
					case 'Rarity':
						item.grade = tag.name.toLowerCase();
						return;

					case 'Class':
						item.classes.push(tag.name.toLowerCase());
						return;

					case 'Exterior':
						item.wear = tag.name;
						return;

					case 'Type' :
						item.slot = tag.name;
						return;
				}
			});

			return item;
		}
	};

	_getLevelData();
	_initElements();
	_initEvents();

	if(this.autoLoad) {
		this.load();
	}
};

window.NoName.Backpack.prototype = {
	render: function(empty, fromPos, toPos) {
		var that = this,
		items = document.createDocumentFragment();

		if(toPos) {
			this.$container.addClass('minimised');
		}

		if(empty) {
			this.$container.children('ol').empty();
		}

		this.items.slice(fromPos, toPos).forEach(function(item, index) {
			if(item.isTradable()) {
				items.appendChild(that.renderItem(item));
			}
		});

		this.$container.children('ol').append(items);
	},

	//Using standard javascript, need all the performance I can get here
	renderItem: function(item) {
		var element = document.createElement('li');
		element.className = 'item q' + item.getQuality();

		if(item.getGrade()) {
			element.className += ' hasgrade g' + item.getGrade();
		}

		element.item = item;
		element.style.backgroundImage  = 'url(' + item.getThumbnail() + ')';

		return element;
	},

	load: function(force) {
		var that = this;
		jsonLoad = NoName.Steam.fetchInventoryJSON();

		this.$container.trigger('ei:backpackloading');
		this.$container.addClass('loading');

		jsonLoad.done(function(json) {
			that.parseJSON(json).done(function() {
				that.$container.trigger('ei:backpackloaded');

	    		if(that.autoRender) {
	    			that.render(true, 0, 10);
	    		}
			}).fail(function() {
				that.$container.trigger('ei:backpackfailed');
			});
		}).fail(function() {
			console.error('[Backpack::load] Failed to load backpack');
			this.$container.trigger('ei:backpackfailed');
		}).always(function() {
			that.$container.removeClass('loading');
		});
	},

	select: function(element) {
		var item = element.item;

		if($(element).parents().index(this.$container) === -1) {
			console.error('[Backpack::select] item is not a descendant of item container');

			return false;
		}

		if(this.items.indexOf(item) < 0) {
			console.error('[Backpack::select] Item does not exist in backpack');

			return false;
		}

		this.selectedItems.push(item);
		this.$selectedContainer.children('ol').append(element);

		return true;
	},

	deselect: function(element) {
		var item = element.item,
		index = this.selectedItems.indexOf(item);

		if(index < 0) {
			console.error('[Backpack::deselect] Item does not exist in backpack');

			return false;
		}

		if($(element).parents().index(this.$selectedContainer) === -1) {
			console.error('[Backpack::deselect] item is not a descendant of selected item container');

			return false;
		}

		this.selectedItems.splice(index, 1);
		this.$container.children('ol').append(element);

		return true;
	},

	isSelected: function(item) {
		return !!this.selectedItems.indexOf(item) > -1;
	},

	getSelected: function() {
		return this.selectedItems;
	}
};

//Object that represents a single item
window.NoName.Item = function(data, levelData) {
	var that = this;

	//General stuff
	this.id = data.id;
	this.defindex = parseInt(data.defindex);

	this.quality = parseInt(data.quality);
	this.tradable = !!data.tradable;

	this.name = data.name || '';
	this.type = data.type || '';
	this.level = data.level;
	this.grade = data.grade || null;
	this.classes = data.classes || [];

	//Fallback to level data found in the default item list if the json api didn't give us one
	//This should only be needed for stranges, as they don't show levels in the steam inventory
	//Using this data is a guess, but it will usually be correct unless the user has multiple copies of the same strange
	//which differ in a noticeable way such as parts
	if(data.level) {
		this.level = data.level;
	} else if(levelData && levelData[this.defindex] && levelData[this.defindex][this.quality]) {
		this.level = levelData[this.defindex][this.quality];
	} else {
		this.level = 1; //The site uses 1 for items that don't have a level
	}

	this.series = data.series || 0;

	this.thumbnail = this.IMAGE_URL + data.thumbnail;
	this.image = this.IMAGE_URL + data.image;

	this.position = data.position;
	this.descriptions = data.descriptions;
	this.tags = data.tags;
};

window.NoName.Item.prototype = {
	IMAGE_URL: 'https://steamcommunity-a.akamaihd.net/economy/image/',

	getDefIndex: function() {
		return this.defindex;
	},

	getName: function() {
		return this.name;
	},

	getThumbnail: function() {
		return this.thumbnail;
	},

	getQuality: function() {
		return this.quality;
	},

	getGrade: function() {
		return this.grade;
	},

	getLevel: function() {
		return this.level;
	},

	getSeries: function() {
		return this.series;
	},

	getPosition: function() {
		return this.position;
	},

	isTradable: function() {
		return this.tradable;
	},

	getDescriptions: function() {
		return this.descriptions;
	},

	//Determines if item matches search filters
	//Not currently used
	matchesFilters: function(filters) {
		if(filters.quality) {
			if(filters.quality.isArray() && filters.quality.indexOf(this.quality) == -1) {
				return false;
			} else if(filters.quality != this.quality) {
				return false;
			}
		}

		if(filters.classes) {
			if(filters.classes.isArray()) {
				var match = false;

				for(var classes in filters.classes) {
					if(this.classes.indexOf(filters.classes[classes])) {
						match = true;
						break;
					}
				}

				if(!match) {
					return false;
				}
			} else if(this.classes.indexOf(filters.classes) == -1) {
				return false;
			}
		}

		if(filters.text) {
			if(this.name.indexOf(text) !== 0) {
				return false;
			}
		}

		return true;
	}
};

//Export override functions
window.NoName.exportOverrides();

//Temporarily hide page content until we've made our layout changes
$(document.head).append(
	$('<style></style>').text(
		'#content .indent {\
			opacity: 0;\
			/*transition: opacity 0.5s ease-out;*/\
		}'
	)
);

$(document).ready(function() {
	//Nu iframes pls
	if(window.top == window.self) {
		//Export override functions again to make sure
		window.NoName.exportOverrides();

		//Lets get this party started
		window.NoName.init();

		//Unhide page content
		$('#content .indent').css('opacity', '1');
	}
});