Userscript with no name

Overhauls the new raffle page and enhances a few others

As of 23.04.2016. See ბოლო ვერსია.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==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/*
// @version     1.1.1
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @resource    css https://gist.githubusercontent.com/JLyne/c02c409932a14c1734c5/raw/176cb0da7b7e402de011e563ebfb9124ccc0527a/noname-style.css
// @require     http://code.jquery.com/jquery-1.12.0.min.js
// @require     https://greasyfork.org/scripts/18834-userscript-with-no-name-skin-dictionary/code/Userscript%20with%20no%20name%20-%20Skin%20dictionary.js?version=120348
// @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('/news.html')) {
			document.body.id = 'home';
		}

		if(!window.location.pathname.indexOf('/donate.html')) {
			document.body.id = 'donate';
		}

		if(!window.location.pathname.indexOf('/info.html')) {
			document.body.id = 'info';
		}

		if(!window.location.pathname.indexOf('/chat.html')) {
			document.body.id = 'chat-page';
		}

		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) {
			that._fire(e);
		});
	},

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

		var old = localStorage[key];
		localStorage[key] = value;
		
		this._fire({
			key: key,
			oldValue: old,
			newValue: value,
			url: window.location.href,
		});

		return true;
	},

	_fire: function(e) {
		if(this.callbacks[e.key]) {
			for(var i = 0; i < this.callbacks[e.key].length; i++) {
				this.callbacks[e.key][i](e.oldValue, e.newValue, e.url);
			}
		}
	},

	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.updateItems();

		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 that don't display correctly
		unsafeWindow.getItem = exportFunction(function(item) {
			return that.getItem(item);
		}, unsafeWindow);

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

	//TODO: These 2 functions do similar things
	//Can parts be merged?
	getItem: function(item) {
		var element = document.createElement('div'),
		information = NoName.Item.prototype.getExtendedInformation(item.name);

		//If item has extended information available then use it
		//Show fallback image if the item doesnt have one
		if(information) {
			item.name = information.name;

			element.className = 'item ' + item.q + ' hasgrade g' + information.grade.toLowerCase();
			element.style.backgroundImage  = 'url(' + (information.image || item.image || this.missingImage)  + ')';
			element.setAttribute('iextended', 'true');
		} else {
			element.className = 'item ' + item.q;
			element.style.backgroundImage  = 'url(' + (item.image || this.missingImage)  + ')';
		}

		element.setAttribute('ilevel', item.level);
		element.setAttribute('iname', item.name || 'Unknown item');
		element.setAttribute('iu1', item.iu1);

		return element.outerHTML;
	},

	//Add extra information to items that don't display correctly if available
	//Otherwise display generic unknown item details
	//Used to update items not added by getItems()
	updateItems: function() {
		var that = this;

		$('.item').each(function() {
			var $img = $(this).children('img'),
			level = $(this).attr('ilevel'),
			name = $(this).attr('iname'),
			image = $img.attr('src'),
			information = null;

			//If an item has no image then show as unknown item
			if(!image || image === 'null') {
				$(this).attr('iname', 'Unknown item');
				this.style.backgroundImage = 'url(' + that.missingImage + ')';
			} else {
				information = NoName.Item.prototype.getExtendedInformation(name);

				//If item has extended information available then show it
				if(information) {
					this.style.backgroundImage = 'url(' + information.image || image + ')';
					$(this).attr('iname', information.name).attr('iextended', 'true');
					$(this).addClass('hasgrade g' + information.grade.toLowerCase());
				} else { //Otherwise just use the existing image
					this.style.backgroundImage = 'url(' + image + ')';
				}
			}

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

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

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

		options.forEach(function(option, index) {
			option.id = option.id || name.replace(' ', '-').toLowerCase() + '-' + index;
			option.value = (typeof option.value !== 'undefined') ? option.value : '';

			children.push(
				$('<input />')
				.prop({
					type: 'radio',
					name: 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;
	},

	//Item details tooltip
	//Used everywhere items are shown
	//Except the new raffle page which uses its own implementation due to different data
	showItemInfo: function(item) {
		var $item = $(item),
		name = $item.attr('iname'),
		level = $item.attr('ilevel'),
		series = $item.attr('iu1'),

		pos = $item.offset(),
		height = $item.height(),
		width = $item.width();

		//Need all item classes other than .item so this'll do
		$('.infname').addClass(item.className).removeClass('item').html(name);
		$('.infdesc').html('<li>Level: ' + level + ((series) ? '<br />#' + series : '') + '</li>');

		if($item.attr('iextended')) {
			//TODO: style this properly
			$('.infdesc').append('<li style="color:red;">Factory new variant shown, actual item will differ</li>');
		}

		$('.infitem').show().css({
			left: pos.left + (width / 2) - ($('.infdesc').outerWidth() / 2) + 'px',
			top: (pos.top + height + 4) + 'px'
		});
	},

	hideItemInfo: function() {
		$('.infitem').hide();
		$('.infname').removeClass().addClass('infname');
	},

	removeExistingUI: function() {
		//Unbind hover event handler added by the existing js
		unsafeWindow.$('.item').unbind('mouseenter mouseleave');
	},

	addStyles: function() {
		GM_addStyle(GM_getResourceText('css'));	
	},

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

		//Might throw sandbox related exceptions although this should be fixed
		try {
			//ScrapTF mode cooldown
			//Export handler function to prevent sandbox issues
			unsafeWindow.$('#sendfeed').bind('click', exportFunction(function(e) {
				if(!NoName.ScrapTF.canComment()) {
					e.stopImmediatePropagation();

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

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

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

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

	init: function() {
		document.body.id = 'profile';

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

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

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

		$('#profile table tr:nth-child(3) > td:nth-child(2)').prop('id', 'rep');

		//Replace current type radio buttons with switch
		$feedbackSwitch = NoName.UI.createSwitch('', 'type', [
			{
				id: 'type1',
				label: 'Positive',
				value: '1',
			},
			{
				id: 'type2',
				label: 'Negative',
				value: '2',
			},
			{
				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() {
		document.body.id = 'settings';

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

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

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

	addNewUI: function() {
		var $iconWarning = $('#fselec').closest('.raffle_infomation').find('h3'),
		$position = NoName.UI.createSwitch('Icon position:', 'position', [
			{
				id: 'tl',
				label: 'Top-left',
				value: 'tl',
				checked: true,
			},
			{
				id: 'tr',
				label: 'Top-right',
				value: 'tr',
			},
			{
				id: 'bl',
				label: 'Bottom-left',
				value: 'bl',
			},
			{
				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'),
				$('<strong></strong>').text('Settings for Userscript with no name'),
				$('<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),
		raffleLayout = storage.get('rafflelayout', false),

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

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

		//Alternate raffle layout
		$raffleLayout = NoName.UI.createSwitch('Raffle - Alternate Layout:', 'raffle-layout', [
			{
				label: 'Disabled',
				checked: !raffleLayout,
			},
			{
				label: 'Enabled',
				value: true,
				checked: raffleLayout,
			}
		]);

		//ScrapTF mode
		$scrap = NoName.UI.createSwitch('ScrapTF Mode', 'scraptf-mode', [
			{
				label: 'Disabled',
				checked: !scrapEnabled,
			},
			{
				label: 'Enabled',
				value: true,
				checked: scrapEnabled,
			}
		]);

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

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

		$raffleLayout.on('change', function(e) {
			storage.set('rafflelayout', 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-mode-1').prop('checked', true);
			}
		});

		this.$settings.append([
			$transitions,
			$showAllItems,
			$raffleLayout,
			$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;

		document.body.id = 'new-raffle';

		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.addSwitches();
		this.addNewUI();

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

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

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

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

		var $start = NoName.UI.createSwitch('Start timer:', 'stype', [
			{
				id: 'stype1',
				label: 'Instantly',
				value: 'instantly',
			},
			{
				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;

		document.body.id = 'raffle-list'

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

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

		//Hack to fix timing issues for now
		this.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.getItems();
		}, unsafeWindow);

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

		//Remove ih() function as we're replacing it with our own implementation
		unsafeWindow.ih = exportFunction(function() {}, unsafeWindow);
	},

	//TODO: This repeats a lot of what getitems() does
	//Can they be merged?
	checkraffles: 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);
	},

	getItems: 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(var 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() {
	},

	addNewUI: function() {
		$('.participants').on('mouseover', '.item', function() {
			NoName.UI.showItemInfo(this);
		}).on('mouseout', '.item', NoName.UI.hideItemInfo);
	},
};

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

	$statsContainer: null,

	$message: null,
	$winChance: null,
	$timeLeft: null,
	$entries: null,

	init: function() {
		var that = this;

		document.body.id = 'raffle';

		//TODO: Are raffle ids always 6 characters?
		this.raffleID = window.location.pathname.substring(2, 8);

		this.$timeLeft = $('#tlefttd');
		this.$message = $('td[colspan="3"]').first();
		this.$entries = $('#entry');
		this.$winChance = $('#winc');

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

		unsafeWindow.updateWC();

		NoName.Storage.listen('rafflelayout', function() {
			unsafeWindow.location.reload();
		});
	},

	exportOverrides: function() {
		var that = this;

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

		unsafeWindow.updateTimer = exportFunction(function() {
			that.updateTimer();
		}, unsafeWindow);
	},

	checkRaffle: function() {
		var that = this;

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

			that.addComments(data.message.chaten, true);

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

	updateTimer: function() {
		if(!unsafeWindow.started) {
			return;
		}
		
		unsafeWindow.tleft--;
		
		if(unsafeWindow.tleft >= 0) {
			var hours = Math.floor(tleft / 3600),
			minutes = Math.floor(tleft / 60 - hours * 60),
			seconds = Math.floor(tleft - hours * 3600 - minutes * 60),
			time = [];
			
			hours && time.push(('00' + hours).slice(-2) + 'h');
			minutes && time.push(('00' + minutes).slice(-2) + 'm');
			seconds && time.push(('00' + seconds).slice(-2) + 's');
			
			$("#tlefttd").text(time.join(' '));
		} else {
			$("#tlefttd").text('Ended');
		}
		
		if(unsafeWindow.tleft > 0 && unsafeWindow.started) {
			setTimeout(unsafeWindow.updateTimer, 1000);	
		}
	},

	removeExistingUI: function() {
	},

	addNewUI: function() {
		var that = this;

		//Add ids to things I'll need to target later
		$('.indent table:nth-child(2) > tbody > tr:nth-child(5) > td:nth-child(2)').prop('id', 'rep');
		$('.indent table:nth-child(2) tr:nth-child(7) td').prop('id', 'prizes');

		//ScrapTF mode cooldown
		//Might throw sandbox related exceptions although this should be fixed
		try {
			//Export handler function to prevent sandbox issues
			unsafeWindow.$('#enbut').bind('click', exportFunction(function(e) {
				if(!NoName.ScrapTF.canEnterRaffle()) {
					e.stopImmediatePropagation();

					return false;
				}
			}, unsafeWindow));
		} 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();
				})
			);
		}

		//New hover handler
		$('.indent').on('mouseover', '.item', function() {
			//Timing issues mean we can't be sure the original hover events are gone
			unsafeWindow.$('.item').unbind('mouseenter mouseleave');

			NoName.UI.showItemInfo(this);
		}).on('mouseout', '.item', function() {
			NoName.UI.hideItemInfo();
		});

		//Replace raffle delivery links with buttons
		//TODO: Event handlers
		// $('.spectik').each(function() {
		// 	var type = $(this).attr('id'),
		// 	wid = $(this).attr('id'),
		// 	text = $(this).text();

		// 	$(this).replaceWith(
		// 		$('<button></button>').addClass('spectik').data({
		// 			wid: wid,
		// 			type: type,
		// 		}).prop('type', 'button').attr('data-type', type).text(text)
		// 	);
		// });

		//unsafeWindow.$('.spectik').unbind();

		//Load new raffle layout if enabled
		if(NoName.Storage.get('rafflelayout', false)) {
			this.addRaffleStats();
		}
	},

	//Replace old entries/time/chance stats with some new fancy looking ones
	addRaffleStats: function() {
		var message = this.$message.html();

		//Remove nested table as it only contains one cell we need, the raffle message
		this.$message = this.$message.closest('.raffle_infomation');
		this.$message.prop('id', 'raffle-message').html(message);

		//Remove things we dont need anymore
		this.$entries.closest('tr').remove();

		//TODO: These should be moved somewhere instead of removed
		$('td[data-rstart-unix]').closest('tr').remove();
		$('td[data-rsend-unix]').closest('tr').remove();

		//Create new <tr> and <td> for raffle stats
		this.$statsContainer = $('<td></td>').addClass('raffle_infomation').prop('id', 'raffle-stats');
		this.$message.parent().after(
			$('<tr></tr>').append(this.$statsContainer)
		);

		//Account for extra row on avatar <td>
		$('td[rowspan="2"]').attr('rowspan', '3');

		this.$entries = $('<strong></strong>').prop('id', 'entry').text('...');
		this.$winChance = $('<strong></strong>').prop('id', 'winc').text('...');
		this.$timeLeft = $('<strong></strong>').prop('id', 'tlefttd').text('...');

		this.$statsContainer.append([
			this.$timeLeft,
			this.$entries,
			this.$winChance,
		]);
	},

	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, prepend) {
		if(prepend) {
			comments = comments.reverse();
		}

		for(var 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),
				$message
			]);

			if(prepend) {
				$('.userfeed').prepend($container);
			} else {
				$('.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();
            },
            onprogress: function(response) {
            }
        });

		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.$displayWarning = null;

	this.items = [];
	this.selectedItems = [];
	this.badItems = []; //Selected items that will not be displayed correctly in raffles

	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.$displayWarning = $('<div></div>').addClass('notif lev1').html(
				'Some selected items will not display correctly in your raffle<br />\
				Consider listing them in the description.'
			).hide();

			that.$selectedContainer.prepend(that.$displayWarning);
		}

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

		if(item.getGrade()) {
			that.$info.children('.infname').addClass('hasgrade g' + item.getGrade());
		}

		//Grade colours
		if(item.getGrade()) {
			that.$info.children('.infname');
		}

		//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.outerWidth() / 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);

		//Show warning if this item won't be displayed correctly in the raffle
		if(this.selectableItems && !item.willDisplayCorrectly()) {
			console.warn('[Backpack::select] Selected item will not be displayed correctly');

			this.badItems.push(item);
			this.$displayWarning.show();
		}

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

		//Remove from bad items list if it exists
		if(this.selectableItems && this.badItems.indexOf(item) > -1) {
			this.badItems.splice(this.badItems.indexOf(item), 1);
		}

		//If bad item list is empty remove warning
		if(this.selectableItems && !this.badItems.length) {
			this.$displayWarning.hide();
		}

		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/',

	//Newer taunts
	//Festivizers
	//Smissmass gifts
	badIndexes: [30671, 30618, 5838, 5839, 1162],

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

	//Checks whether the current item is likely to be displayed correctly
	//Many newer items don't appear correctly due to missing schema information:
	//Appearing as stock weapons, having unlocalised names, or having no image and name at all
	//Returning true is not a guarantee of correct display, but should be a good enough guess for most items
	willDisplayCorrectly: function() {
		//Skins show as stock or nothing at all
		if(this.defindex >= 15000 && this.defindex < 16000) {
			return false;
		}

		//Tough break cosmetics and anything valve adds in the future in this range
		if(this.defindex > 30742) {
			return false;
		}

		//Other known bad defindexes
		if(this.badIndexes.indexOf(this.defindex) > -1) {
			return false;
		}

		return true;
	},

	//Returns extended information from the dictionary, using the items unlocalised name
	//Used for skins that don't display correctly but still include a name
	getExtendedInformation: function(unlocalisedName) {
		var prefix = '',
		information = null;

		if(!unlocalisedName.indexOf('Strange')) {
			prefix = 'Strange ';
			unlocalisedName = unlocalisedName.replace('Strange ', '');
		}

		if(!window.itemDictionary) {
			return false;
		}

		if(!window.itemDictionary[unlocalisedName]) {
			return false;
		}

		information = window.itemDictionary[unlocalisedName];
		information.name = prefix + information.name;

		return information;
	},

	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 css early
window.NoName.UI.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();

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