Userscript with no name

Overhauls the new raffle page and enhances a few others

当前为 2016-04-11 提交的版本,查看 最新版本

// ==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     0.995
// @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);
		}
	},

	addStyles: function() {
		this.UI.addStyles();
		this.NewRaffle.addStyles();
		this.Settings.addStyles();
		this.Profile.addStyles();
		this.RaffleList.addStyles();
		this.Raffle.addStyles();
	},
};

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 {\
					/*border-collapse: collapse;*/\
					/*border-spacing: 0;*/\
				}\
				td {\
					border-top: 1px solid transparent;\
					border-bottom: 1px solid transparent;\
				}\
				.indent {\
					left: auto;\
					margin-left: 0;\
					padding: 0 12px;\
				}\
				.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 .userfeedpost:hover {/* Fix feedback hover background colours */\
    			    animation-fill-mode: none;\
    			    animation-duration: 0s;\
    			}\
    			.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');
		}
	},

	//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.removeExistingUI();
		this.addStyles();
		this.addNewUI();
	},

	removeExistingUI: function() {
		this.$feedbackType = $('#type1').parent();
		this.$feedbackType.empty();
	},

	addStyles: function() {
		$(document.head).append(
			$('<style></style>').text(
				'.nfbutton {\
					margin-top: 0;\
					margin-bottom: 10px;\
				}\
				.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;\
				}\
				#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;\
				}\
				#progress div div + div {\
					right: 1px;\
				}\
			')
		);
	},

	addNewUI: function() {
		//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

		this.$progress.prop('id', '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').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();
	},

	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;

		console.log('here');

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

				// data.message.newraf.push({
				// 	rname: 'Test raffle',
				// 	name: 'Jim',
				// 	color: '#8650ac',
				// 	items: [
				// 		{q:'q6'},{q:'q6'},{q:'q6'},{q:'q6'},{q:'q6'},{q:'q6'},{q:'q6'},{q:'q6'},{q:'q6'},{q:'q6'},
				// 	],
				// 	avatar: 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/3c/3ca4b5a14f243e6236463e6921db739ce6ae1e7f_medium.jpg',
				// 	rlink: 'http://tf2r.com/kiq2b8d.html',
				// 	link: 'http://tf2r.com/user/76561197994939255.html',
				// });

				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;\
    			}\
    		')
    	);
	},

	addNewUI: function() {
		//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');
		}
	},
};

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

//Add styles early
//window.NoName.addStyles();

$(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();
	}
});
长期地址
遇到问题?请前往 GitHub 提 Issues。