Turkmaster (Mturk)

A page-monitoring web app for Mturk (Mechanical Turk) designed to make turking more efficient. Easily monitor mturk search pages and requesters and Auto-Accept the HITs you missed.

// ==UserScript==
// @name        Turkmaster (Mturk)
// @namespace   https://greasyfork.org/users/3408
// @author      DonovanM
// @description A page-monitoring web app for Mturk (Mechanical Turk) designed to make turking more efficient. Easily monitor mturk search pages and requesters and Auto-Accept the HITs you missed.
// @include     https://www.mturk.com/mturk/*
// @include     https://worker.mturk.com/*
// @version     1.5.11
// @require     https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js
// @require     https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// ==/UserScript==

var settings = (function() {
	var	LOCAL_STORAGE = "turkmaster_settings";
	var pub = {
		sound         : true,
		animation     : true,
		preloadHits   : false,
		volume        : 50,
		notifications : true,
		alertOnly     : false,
		fontSize      : 10,
		typeface      : "Oxygen",
		desktopNotifications : false,
		canHide       : false,
		stopOnCaptcha : true
	}

	_load();

	function _setFontSize(val) {
		if (val >= 5 && val <= 20) {
			pub.fontSize = val;
			$("#dispatcher div").css("font-size", val + "pt");
			$(".notification_panel p").css("font-size", val + "pt");
			$("#settingsDialog, #settingsDialog div, #settingsDialog li, #settingsDialog input, #settingsDialog button").css("font-size", val + "pt");

			_save();
		}
	}

	function _setDesktopNotifications(val, callback) {
		if (val) {
			requestDesktopNotifications(function(isPermitted) {
				callback(isPermitted);
				pub.desktopNotifications = isPermitted;
				_save();
			});

			pub.desktopNotifications = false;
		} else {
			pub.desktopNotifications = false;
		}

		_save();
	}

	function _setVolume(val) {
		if (val >= 0 && val <= 100) {
			Sound.setVolume(val);
			pub.volume = val;
			_save();
		}
	}

	function _save() {
		// localStorage.setItem(LOCAL_STORAGE, JSON.stringify(pub));
		GM_setValue(LOCAL_STORAGE, JSON.stringify(pub));
	}

	function _load() {
		var values = GM_getValue(LOCAL_STORAGE);

		if (typeof values === 'undefined')
			values = localStorage.getItem(LOCAL_STORAGE);

		if (values) {
			values = JSON.parse(values);

			for (i in values)
				pub[i] = values[i];
		}
	}

	pub.setFontSize = _setFontSize;
	pub.setVolume   = _setVolume;
	pub.setDesktopNotifications = _setDesktopNotifications;
	pub.save = _save;

	return pub;
}());

var pageType = {
	MAIN      : true,	// This is so remote watcher requests don't add new watchers to multiple pages and cause mturk errors.
	DASHBOARD : false,
	HIT       : false,
	REQUESTER : false,
	SEARCH    : false,
	NEW_SITE  : false
};

var loadError = false;
var wasViewed = false;
var dispatch;
var notificationPanel;
var URL_POSTFIX = '&doNotRedirect=true';

if(!('contains' in String.prototype)) {
	String.prototype.contains = function(str, startIndex) {
		return -1 !== String.prototype.indexOf.call(this, str, startIndex);
	};
}

$(document).ready(function(){
	checkPageType();
	loadFonts();

	if (pageType.DASHBOARD) {
		dispatch = new Dispatch();
		DispatchUI.create(dispatch);
		createDetailsPanel();
		IgnoreList.init();

		if (settings.preloadHits)
			loadDefaultWatchers();
		else
			dispatch.load();

		requestMain();
		preloadImages();
		addFormStyle();
	}

	if (pageType.HIT || pageType.REQUESTER || pageType.SEARCH)
		addWatchButton();

	notificationPanel = new NotificationPanel();

	// Listen to messages
	window.addEventListener('storage', onStorageEvent, false);
});

$(window).on('beforeunload', function() {
	if (pageType.DASHBOARD && pageType.MAIN) {
		dispatch.save();
	}
});

function loadFonts() {
	WebFont.load({
		google: { families: [ 'Oxygen:400,700:latin', 'Droid Sans Mono' ] }
	});
}

function onStorageEvent(event) {
	if (event.key.substring(0,13) === "notifier_msg_")
		onMessageReceived(event.key.substring(13), JSON.parse(event.newValue).content);
}

function checkPageType() {
	if (document.URL.match("worker.mturk.com") !== null) {
		pageType.NEW_SITE = true;
	}

	// Dashboard, hit, requester, search
	if (document.URL === "https://www.mturk.com/mturk/dashboard" || document.URL.startsWith("https://worker.mturk.com/dashboard")) {
		pageType.DASHBOARD = true;
		return;
	}

	if (document.URL.match(/https:\/\/www.mturk.com\/mturk\/(preview|accept).+groupId=.*/) !== null || document.URL.startsWith("https://worker.mturk.com/projects/")) {
		pageType.HIT = true;
		return;
	}

	if (document.URL.match(/requesterId=([A-Z0-9]+)/) !== null || document.URL.startsWith("https://worker.mturk.com/requesters/")) {
		pageType.REQUESTER = true;
		return;
	}

	pageType.SEARCH = true;
}

function requestMain() {
	sendMessage({ header: "request_main" });
}

function preloadImages() {
	var images = [
		'https://i.imgur.com/guRzYEL.png',
		'https://i.imgur.com/5snaSxU.png',
		'https://i.imgur.com/VTHXHI4.png',
		'https://i.imgur.com/peEhuHZ.png'
	];

	$(images).each(function(){
		$('<img>')[0].src = this;
	});
}

var SettingsDialog = function() {
	var DOMElement,
		widgetGroups = [
			{
				id      : 'soundSettings',
				text    : 'Sound',
				type    : 'toggle',
				setting : 'sound',
				items   : [
					{
						id   : 'volume',
						text : 'Volume (0 - 100)',
						type : 'text',
						setting : 'volume',
						handler : function (target, value) {
							settings.setVolume(value);
						}
					}
				]
			},
			{
				id      : 'notificationSettings',
				text    : 'Notifications',
				type    : 'toggle',
				setting : 'notifications',
				items   : [
					{
						id   : 'desktopNotifications',
						text : 'Desktop Notifications',
						type : 'toggle',
						setting : 'desktopNotifications',
						handler : _handleDesktopNotificationToggle
					}
				]
			},
			{
				id    : 'more',
				text  : 'Captchas',
				items : [
					{
						id   : 'stopOnCaptcha',
						text : 'Stop on Captcha',
						type : 'toggle',
						setting : 'stopOnCaptcha'
					}
				]
			},
			{
				id    : 'fontSettings',
				text  : 'Font',
				items : [
					{
						id   : 'fontSize',
						text : 'Size (pt)',
						type : 'text',
						setting : 'fontSize',
						handler : function (target, value) {
							settings.setFontSize(value);
						}
					}
				]
			},
			{
				id    : 'uiSettings',
				text  : 'User Interface',
				items : [
					{
						id   : 'hideable',
						text : 'Hideable',
						type : 'toggle',
						setting : 'canHide',
						handler : function (target, value) {
							settings.canHide = value;
							setTimeout(function () { DispatchUI.setHide() }, 50);
						}
					}
				]
			},
			{
				id    : 'backup',
				text  : 'Backup',
				items : [
					{
						type : 'more',
						id   : 'export',
						text : 'Export',
						handler: _showExport
					},
					{
						type : 'more',
						id   : 'import',
						text : 'Import',
						handler: _showImport
					}
				]
			}
		];

	function _show() {
		if (!DOMElement)
			_createDOMElement()

		_setCurrentSettings(widgetGroups);
		DOMElement.show();
		$(window).on('click', _handleWindowClick);
	}

	function _isVisible() {
		if (DOMElement)
			return DOMElement.is(":visible");
		else
			return false;
	}

	// Sets the widgets to reflect the current settings
	function _setCurrentSettings(list) {
		list.forEach(function (item) {
			if (item.type)
				_setSetting(item.id, item.type, item.setting);

			if (item.items)
				_setCurrentSettings(item.items);
		})
	}

	function _setSetting(id, type, setting) {
		var widget = DOMElement.find("#" + id),
			value = settings[setting];

		if (type === "toggle" && value) {
			widget.children(".on_off").addClass("on");
		} else if (type === "text") {
			widget.children("input").val(value);
		}
	}

	function _cancel() {
		DOMElement.hide();
	}

	function _createDOMElement() {
		_addStyle();

		DOMElement = $('<div id="settingsDialog"><h2>Settings</h2></div>').append(_createGroups(widgetGroups));

		_addHandlers();

		$("body").append(DOMElement);
	}

	// Creates settings groups for each group item in groups
	function _createGroups(groups) {
		return groups.map(function (group) {
			return _createGroup(group);
		});
	}

	function _addDefaultHandlers(items) {
		items.forEach(function (item) {
			if (item.type && !item.handler)
				item.handler = _setValue;
			if (item.items)
				_addDefaultHandlers(item.items);
		})
	}

	function _setValue(target, value) {
		settings[this.setting] = value;
	}

	function _createGroup(params) {
		var widgetHTML = widgetHTML || "",
			group,
			list;

		if (params.type === 'toggle')
			widgetHTML = getToggleHTML();

		group = $('<div id="' + params.id + '">' + widgetHTML + '<h3>' + params.text + '</h3><ul></ul></div>');
		list = group.find('ul');

		params.items.forEach(function (item) {
			list.append(_createItem(item));
		});

		return group;
	}

	function _createItem(params) {
		return (params.type === 'toggle') ? _createToggleItem(params.id, params.text) :
		       (params.type === 'text')   ? _createTextItem(params.id, params.text) :
		       (params.type === 'more')   ? _createMoreItem(params.id, params.text) :
		       undefined;
	}

	function _createTextItem(id, text) {
		return $('<li id="' + id + '">' + text + '<input type="text" class="text" /></li>');
	}

	function _createToggleItem(id, text) {
		return $('<li id="' + id + '">' + getToggleHTML() + text + '</li>');
	}

	function _createMoreItem(id, text) {
		return $('<li id="' + id + '"><button class="more">...</button>' + text + '</li>');
	}

	function getToggleHTML() {
		return '<button class="on_off"><span>ON</span><span>OFF</span></button>';
	}

	function _addHandlers() {
		DOMElement.on('click', function(e) {
			if (e.target.tagName === "BUTTON" || e.target.parentNode.tagName === "BUTTON")
				_handleWidgetClick(e);
		});

		DOMElement.on('change', _handleInputChange);
		_addDefaultHandlers(widgetGroups);
	}

	function _handleWindowClick(e) {
		var target = e.target;

		// Hides the settings dialog if user clicks outside of the dialog
		if (!DOMElement.is(target) && DOMElement.has(target).length === 0 && $("#settings img").get(0) !== target) {
			_cancel();
			$(window).off('click', _handleWindowClick);
		}
	}

	function _handleInputChange(e) {
		var target = $(e.target),
			value = target.val(),
			id = target.parent().attr('id');

		_setWidget(id, target, value);
	}

	function _handleWidgetClick(e) {
		e.preventDefault();

		// Chrome returns the span as the target while FF returns the button
		var target = (e.target.tagName === "BUTTON") ? $(e.target) : $(e.target).parent(),
			value = target.hasClass("on"),
			id = target.parent().attr('id');

		// Toggle widget unless it's for desktop notifications (because it's asynchronous)
		if (id !== "desktopNotifications")
			value = _toggle(target);

		_setWidget(id, target, value);
	}

	function _setWidget(id, target, value) {
		_findWidget(widgetGroups, id).handler(target, value);
		settings.save();
	}

	function _toggle(element) {
		if (element.hasClass("on")) {
			element.removeClass("on");
			return false;
		} else {
			element.addClass("on");
			return true;
		}
	}

	// Recursively looks for a widget with specified id
	function _findWidget(widgets, id, i) {
		return widgets.reduce(function (prev, curr) {
			return (prev !== null) ? prev :
			       (curr.id === id) ? curr :
			       (curr.items) ? _findWidget(curr.items, id) :
			       null;
		}, null);
	}

	function _handleDesktopNotificationToggle(target, value) {
		if (value)
			target.removeClass("on");

		// Desktop notification requests require user action so we need a callback
		// for when the user responds.
		settings.setDesktopNotifications(!value, function(isPermitted) {
			if (isPermitted) {
				target.addClass("on");
			} else {
				target.removeClass("on");
				console.error("Desktop notifications are blocked.");
			}
		});
	}

	function _showExport() {
		var div = $('<div id="export-box" class="dialog-big"><h2>Export Watchers</h2><h3>Copy the text below. (Triple-click to highlight all)</h3><p>' + dispatch.exportWatchers() + '</p></h2></div>');
		$('<button>Close</button>')
			.click(function() { div.remove() })
			.appendTo(div);

		div.appendTo($("body"));
	}

	function _showImport() {
		var div = $('<div id="import-box" class="dialog-big"><h2>Import Watchers</h2><h3>Paste the backup text to load watchers</h3><textarea></textarea></h2></div>');
		$('<button>Save</button>')
			.click(function() { dispatch.importWatchers($("#import-box textarea").val()); div.remove() })
			.appendTo(div);
		$('<button>Close</button>')
			.click(function() { div.remove() })
			.appendTo(div);

		div.appendTo($("body"));

		div.find("textarea").focus();
	}

	function _addStyle() {
		addStyle("\
			#settingsDialog {\
				position: fixed;\
				top: 16px;\
				left: 249px;\
				background-color: #fafafa;\
				padding: 10px;\
				width: 300px;\
				font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif;\
				border-bottom: 1px solid #DDD;\
				border-right: 1px solid #DDD;\
				border-radius: 0.3em;\
				color: #333;\
			}\
			#settingsDialog div, #settingsDialog li, #settingsDialog input, #settingsDialog button {\
				font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif;\
			}\
			#settingsDialog > div {\
				margin: 0px 0px 0.5em;\
				border: 1px solid #eee;\
				padding: 0.35em 0.60em 0.05em;\
				background-color: #fff;\
			}\
			#settingsDialog h2, #settingsDialog h3 {\
				margin: 0 0 0.5em;\
				font-size: 95%;\
				letter-spacing: 0.5px;\
			}\
			#settingsDialog h2 {\
				font-weight: 400;\
				text-align: center;\
				font-size: 110%;\
				color: #333;\
			}\
			#settingsDialog button.on_off {\
				background: none;\
				border: none;\
				padding: 0;\
				outline: none;\
				height: 1.3em;\
				margin-top: 0em;\
			}\
			#settingsDialog .on_off span { color: #333; margin: 1px; font-size: 56%; font-weight: bold; border-radius: 1.6em;  }\
			#settingsDialog .on_off span:nth-child(2) { background-color: #aeaeae; color: #fff; padding: 0.4em 0.8em; }\
			#settingsDialog .on_off.on span:nth-child(1) { background-color: #55b8ea; color: #fff; padding: 0.4em 0.8em; }\
			#settingsDialog .on_off.on span:nth-child(2) { background-color: inherit; color: #333; padding: 0 0.8em 0 0; }\
			#settingsDialog .on_off { margin-top: 6px; }\
			#settingsDialog ul { margin: 0 0 0.2em; padding: 0 0 0 1.9em }\
			#settingsDialog ul li { list-style: none; margin-bottom: 0.5em; }\
			#settingsDialog li input, #settingsDialog li button { float: right; }\
			#settingsDialog li input[type='text'] { width: 3em; font-size: 80%; margin-right: 0.8em; text-align: right; padding-right: 0.5em }\
			#settingsDialog li .more { width: 24px; border: none; color: #808080; font: bold 160% inital; line-height: 0%; transform: rotate(90deg); background-color: transparent; position: relative; top: 2px; cursor: pointer; height: 0.7em; padding: 0 0 0.65em; }\
			#settingsDialog li#typeface input { width: 8em }\
			.dialog-big { position: fixed; top: 2em; left: 50%; width: 860px; margin-left: -430px; background-color: white; padding: 2%; border: 1px solid #ddd; font-family: 'Oxygen'; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.4); border-radius: 10px; }\
			.dialog-big p { background-color: #f7f7f7; padding: 1.5em; height: 500px; overflow: scroll; }\
			.dialog-big h2, #export-box h3 { font-weight: 400 }\
			.dialog-big h2 { font-size: 170%; margin-top: 0 }\
			.dialog-big button { background-color: #cecece; color: white; padding: 3px 10px; font-family: 'Oxygen'; border: none; border-radius: 3px; font-weight: bold; transition: background-color 0.3s; margin-right: 0.5em }\
			.dialog-big button:hover { background-color: #55B8EA }\
			.dialog-big textarea { display: block; width: 100%; height: 500px; margin: 1em 1em 1em 0; }\
		");
	}

	return {
		show: _show,
		hide: _cancel,
		isVisible: _isVisible
	}
}();


function addWatchButton() {
	var type = (pageType.HIT) ? 'hit' : (pageType.REQUESTER) ? 'requester' : (pageType.SEARCH) ? 'page' : '';
	var button = $("<div>").addClass("watcher_button")
		.append($("<a>")
			.text("Watch this " + type + "?")
			.attr('href', "javascript:void(0)")
			.click(addWatcher)
		);

	function addWatcher() {
		// Get current and default values
		var time        = 60,
			auto        = true,
			alert       = false,
			name        = "",
			stopOnCatch = true;

		// Find the name if available
		if (!pageType.NEW_SITE) {
			if (pageType.REQUESTER) {
				if ($(".title_orange_text_bold").length > 0) {
					name = $(".title_orange_text_bold").text().match(/Created by '(.+)'/);
					name = (typeof name !== 'undefined') ? name[1] : "";
				} else if (document.URL.match(/prevRequester=/)) {
					name = document.URL.match(/prevRequester=([^&]*)/)[1];
				}
			} else if (pageType.SEARCH) {
				name = document.URL.match(/searchWords=([^&]*)/);

				if (name !== null) {
					name = name[1].replace('+', ' ');
					name = name.charAt(0).toUpperCase() + name.slice(1);	// Capitalize first letter
				} else {
					name = "";
				}
			} else if (pageType.HIT) {
				name = $(".capsulelink_bold > div:nth-child(1)").text().trim();
			}
		} else {
			if (pageType.REQUESTER) {
				name = $(".back-to-search-link span").text();
			}
			if (pageType.HIT) {
				let data = $('.project-detail-bar [data-react-class]').data('react-props');

				if (data) {
					name = data.modalOptions.projectTitle;
				}
			}
		}

		// Pull up a Watcher Dialog with default values set
		watcherDialog(
			{
				name: name,
				time: time * 1000,
				type: type,
				option: {
					auto        : auto,
					alert       : alert,
					stopOnCatch : stopOnCatch
				}
			},
			function(values) {
				var id;

				if (!pageType.NEW_SITE) {
					id = (document.URL.match(/groupId=([A-Z0-9]+)/) || document.URL.match(/requesterId=([A-Z0-9]+)/) || [,document.URL])[1];
				} else {
					id = (document.URL.match(/requesters\/([A-Z0-9]+)/) || document.URL.match(/projects\/([A-Z0-9]+)/) || [,document.URL])[1];
				}
				var watcher = {
						id          : id,
						duration    : values.time,
						type        : (type === "page") ? "url" : type,
						name        : values.name,
						auto        : values.auto,
						alert       : values.alert,
						stopOnCatch : values.stopOnCatch,
						muteBatch   : values.muteBatch
					};

				sendMessage({
					header    : 'add_watcher',
					content   : watcher,
					timestamp : true
				});
			}
		);
	}

	var location;	// Location to add the watch button
	if (!pageType.NEW_SITE) {
		if (pageType.HIT) {
			if ($(".message.success h6").length)
				location = $(".message.success h6");
			else if ($("#javascriptDependentFunctionality").length)
				location = $("#javascriptDependentFunctionality");
			else if ($("body > form:nth-child(7) > table:nth-child(9) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(1)").length)
				location = $("body > form:nth-child(7) > table:nth-child(9) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(1)");
			else if (!$("span#alertboxHeader:visible").length) {
				button.addClass("block");
				location = $("img[src='/media/submit_hit_disabled.gif']").eq(0).parent().parent();
			}
			else
				location = $('span#alertboxHeader');
		} else if (pageType.REQUESTER || pageType.SEARCH) {
			if ($(".title_orange_text_bold").length)
				location = $(".title_orange_text_bold");
			else
				location = $(".error_title");
		}
	} else {
		if (pageType.REQUESTER || pageType.SEARCH)
			location = $(".projects-info-header h1");
		else {
			location = $("#MainContent h2").first();

			if (!location.length) {
				location = $(".project-detail-bar > .row .row [data-react-class]").first();
			}
		}
	}
	location.append(button);
	addFormStyle();
}


function addFormStyle() {
	addStyle("\
		#add_watcher_form {\
			position: fixed;\
			width: 600px;\
			top: 50px;\
			left: 50%;\
			margin: 50px -300px;\
			background-color: #fcfcfe;\
			border: 1px solid #aaa;\
			border-radius: 1px;\
			text-align: center;\
			}\
		#add_watcher_form h3 {\
			font: 12pt Verdana;\
			margin: 0 0 15px;\
			background-color: #def;\
			background-color: rgba(230, 230, 230, 1);\
			padding: 3px;\
			color: #111;\
			}\
		#add_watcher_form input[type='text'] {\
			font: 10pt Verdana;\
			margin: 10px 20px 0 0;\
			}\
		#add_watcher_form input[type='button'] {\
			margin-top: 20px;\
			font: 9pt Verdana;\
			color: #444;\
			background-color: #eee;\
			border: 1px solid #999;\
			}\
		#add_watcher_form input[type='button']:hover {\
			background-color: #9df;\
			}\
		#add_watcher_form p {\
			margin: 10px;\
			font: 11pt Verdana;\
			}\
		#add_watcher_form .form_buttons input {\
			margin: 5px;\
		}\
		.watcher_button { display: inline; }\
		.watcher_button.block { display: block; }\
		.watcher_button a {\
			text-decoration: none;\
			font-weight: normal;\
			background-color: #CECECE;\
			color: white;\
			padding: 3px 10px;\
			border-radius: 10px;\
			font-family: 'Oxygen', verdana, sans-serif;\
			transition: background-color 0.4s;\
			font-size: 13px;\
		}\
		.watcher_button a:hover { background-color: #55B8EA }\
		.error_title .watcher_button { display: block; margin: 15px }\
	");
}

function addStyle(styleText) {
	var style = '<style type="text/css">' + styleText + '</style>';
	$("head").append(style);
}

function loadDefaultWatchers() {
	// Add a few watchers. Won't be done like this in the future
	dispatch.isLoading = true;
	dispatch.add(new Watcher({
		id: "https://worker.mturk.com/?filters%5Bsearch_term%5D=survey&page_size=20&page_number=1&sort=updated_desc&filters%5Bmin_reward%5D=0.25",
		time: 60000,
		type: 'url',
		name: "Surveys $0.25 and up"})); //$.25 surveys
	dispatch.add(new Watcher({
		id: "https://worker.mturk.com/?filters%5Bsearch_term%5D=survey&page_size=20&page_number=1&sort=updated_desc&filters%5Bmin_reward%5D=0.75",
		time: 60000,
		type: 'url',
		name: "Surveys $0.75 and up"})); //$.75 surveys
	dispatch.add(new Watcher({
		id: "A11L036EBWKONR",
		time: 120000,
		type: 'requester',
		name: "Project Endor",
		option: {alert:true}}));	// Endor
	dispatch.add(new Watcher({
		id: "A6YG5FKV2TAVC",
		time: 300000,
		type: 'requester',
		name: "Agent Agent",
		option: {alert:true}}));	// Agent Agent
	dispatch.add(new Watcher({
		id: "AKEBQYX32KM19",
		time: 120000,
		type: 'requester',
		name: 'Crowdsurf Support'}));
	dispatch.add(new Watcher({
		id: "https://worker.mturk.com/?filters%5Bsearch_term%5D=transcri&page_size=20&page_number=1&sort=updated_desc&filters%5Bmin_reward%5D=0",
		time: 60000,
		type: 'url',
		name: "Transcription HITs"})); // Transcription HITs
	dispatch.isLoading = false;
	dispatch.save();
}

function onMessageReceived(header, message) {
	console.log('message', header, message);
	if (pageType.DASHBOARD && pageType.MAIN) {
		switch(header) {
			case 'notification_viewed' :
				wasViewed = true;
				break;
			case 'add_watcher' :
				var msg = message;
				dispatch.add(new Watcher({
					id     : msg.id,
					time   : msg.duration,
					type   : msg.type,
					name   : msg.name,
					option : {
					 	auto: msg.auto,
					 	stopOnCatch: msg.stopOnCatch,
					 	alert: msg.alert
					}
				})).start();
				break;
			case 'ignore_requester' :
				IgnoreList.add(IgnoreList.REQUESTER, message.id);
				break;
			case 'mute_hit' :
				var id = message.id;
				IgnoreList.add(IgnoreList.HIT, id);
				break;
			case 'unmute_hit' :
				var id = message.id;
				IgnoreList.remove(IgnoreList.HIT, id);
				break;
			case 'request_main' :
				sendMessage({ header: "request_denied" });
				break;
			case 'request_denied' :
				dispatch.onRequestMainDenied();
				break;
			case 'show_main' :
				alert("Showing the main dashboard. (Close this Mturk page to establish a notifier in a different tab or window)");
				break;
		}
	} else if (!pageType.DASHBOARD || (pageType.DASHBOARD && !pageType.MAIN)) {

		switch(header) {
			case 'new_hits' :
				var hits = message.hits;

				// Re-create the hits so their methods can be used
				for(var i = hits.length; i--;) hits[i] = new Hit(hits[i]);

				// Show the hits and let the dashboard know it was seen
				if (document.hasFocus())
					sendMessage({ header: "notification_viewed" });
				notificationPanel.add(new NotificationGroup({ title: message.title, hits: hits, url: message.url }));
				break;

			case 'captcha' :
				if (document.hasFocus())
					alert("Captcha Alert!");
				break;

			case 'turkopticon' :
				// This needs a more elegant solution. If the servers start lagging we might be
				// using addTO() for the wrong group. It won't show the TO for the wrong requester,
				// though, so it's safe to use for now. It's just that some ratings could be missing.
				notificationPanel.notifications[notificationPanel.notifications.length - 1].addTO(message);
				break;
		}
	}
}
function sendMessage(message) {
	var header    = message.header;
	var content   = message.content || new Date().getTime();	// Make the content a timestamp when there's no actual content
	var timestamp = message.timestamp && new Date().getTime();	// If wanted, adds a timestamp to the content so messages with the same content will still trigger the event consecutively
	localStorage.setItem('notifier_msg_' + header, JSON.stringify({ content: content, timestamp: timestamp}));
}

function sendDesktopNotification(hits, watcher) {
	// Let's check if the user is okay to get some notification
	if (Notification.permission === "granted" && settings.desktopNotifications) {
		// If the user isn't on a mturk page to receive a rich notification, then send a web notification
		if (!wasViewed) {
			var bodyText = "";

			for (var i = 0, len = hits.length; i < len; i++)
				bodyText += "\n" + hits[i].title.substring(0, 40) + ((hits[i].title.length > 40) ? "..." : "") + "\n" + hits[i].reward + "\n";

			var notification = new Notification(
				watcher.name,
				{
					body: bodyText,
					icon: "http://halfelf.org/wp-content/uploads/sites/2/2012/06/amazon_icon.png"
				}
			);
			notification.onclick = function() {
				window.focus();					// Focus this window (dashboard)
				this.close();					// Closes the notification
				showDetailsPanel(watcher);		// Opens the details panel for whatever watcher the notification was for
			};
			notification.onshow = function() { setTimeout(function() { notification.close() }, 5000) }; // Need to set a close time for Chrome
		}
	}
}

function requestDesktopNotifications(callback) {
	// Let's check if the browser supports notifications
    if (!("Notification" in window)) {
		alert("This browser does not support desktop notification");
    } else {
		window.Notification.requestPermission(function (permission) {
			// Whatever the user answers, we make sure Chrome stores the information
			if(!('permission' in Notification))
				window.Notification.permission = permission;

			// If the user is okay, let's create a notification
			if (permission === "granted") {
				var notification = new window.Notification("Desktop notifications enabled.");
				notification.onshow = function() { setTimeout(function() { notification.close() }, 5000) };
				callback(true);
			} else {
				callback(false);
			}
		});
	}
}



function Hit(attrs) {
	attrs = attrs || {};
	this.id           = attrs.id;
	this.uid          = attrs.uid;
	this.isAutoAccept = attrs.isAutoAccept || false;
	this.requester    = attrs.requester;
	this.requesterID  = attrs.requesterID;
	this.url          = attrs.url;
	this.title        = attrs.title;
	this.reward       = attrs.reward;
	this.description  = attrs.description;
	this.available    = attrs.available;
	this.time         = attrs.time;
	this.isQualified  = (typeof attrs.isQualified !== 'undefined') ? attrs.isQualified : true;
	this.canPreview   = (typeof attrs.canPreview !== 'undefined') ? attrs.canPreview : true;
}
Hit.prototype.getURL = function(type) {
	var url;

	if (pageType.NEW_SITE) {
		switch(type) {
			case 'preview':
				return `https://worker.mturk.com/projects/${this.id}/tasks`;
			case 'accept':
				return `https://worker.mturk.com/projects/${this.id}/tasks/accept_random?ref=w_pl_prvw`;
			case 'view':
				return `https://worker.mturk.com/projects/${this.id}/tasks/${this.taskID}?assignment_id=${this.assignmentID}&from_queue=true`
			default:
				return "";
		}
	}

	switch(type) {
		case 'preview':
			url = "https://www.mturk.com/mturk/preview?groupId=" + this.id;
			break;
		case 'accept' :
			url = (this.isQualified) ? "https://www.mturk.com/mturk/previewandaccept?groupId=" + this.id : null;
			break;
		case 'auto'   :
			url = "https://www.mturk.com/mturk/previewandaccept?groupId=" + this.id + "&autoAcceptEnabled=true";
			break;
		case 'view'   :
			url = "https://www.mturk.com/mturk/continue?hitId=" + this.uid;
			break;
		case 'return' :
			// This will need to be changed. It's the same as 'view' until more testing is done on AMT's return functionality
			url = "https://www.mturk.com/mturk/preview?hitId=" + this.uid;
			break;
		default:
			url = "";
	}

	return url + URL_POSTFIX;
};
// Returns the position of a hit in a hit array by its ID
Hit.indexOf = function(hitId, hits) {
    for (var i = 0, len = hits.length; i < len; i++) {
        if (hitId === hits[i].id)
            return i;
    }
    return -1;
};
// Returns true if there are multiple hits in the array and all of the hits are from the same requester
Hit.isSameRequester = function(hits) {
	if (hits.length > 1) {
		var compareRequester = hits[0].requester;
		for (var i = 1, len = hits.length; i < len; i++) {
			if (compareRequester !== hits[i].requester)
				return false;
		}
		return true;
	} else {
		return false;
	}
};
// Returns a list of unique requester IDs from an array of hits
Hit.getUniqueReqeusters = function(hits) {
	var ids = [];

	for (var i = 0, len = hits.length; i < len; ++i) {
		var id = hits[i].requesterID;

		if (ids.indexOf(id) === -1)
			ids.push(id)
	}

	return ids;
}

// Message object (Not used)
function Message() {
	/*  Status (changed): Unchanged, Added, Removed, Count
		We should mark each Hit in the message with what has changed. The count change should be sent with this.
		The message will also tell the client whether or not to pop-up the notification.	*/
}

// The details panel for each watcher
function createDetailsPanel() {
	var div = $('<div>').attr('id', 'details_panel').addClass('notification_panel');
	addStyle("#details_panel {\
		background-color: #fff;\
		position: absolute;\
		top: 0px;\
		margin-left: 1px;\
		left: 270px;\
		width: 500px;\
		border: 1px solid #e3e3e3;\
		border-radius: 0 0 3px 0;\
		border-width: 0 1px 1px 0;\
		transition: left 0.5s ease;\
		display: none;\
		z-index: 1050;\
	}\
	#details_panel h4 { display: none }\
	#details_panel.left { left: 30px }");

	$(div).mouseleave(function() { $(this).hide() });

	$("body").append(div);
}

var lastWatcher = "";
function showDetailsPanel(watcher) {
	var panel = $("#details_panel");
	var group;

	// Only change the panel contents if it's a different watcher or the same one, but updated
	if (watcher !== lastWatcher || (watcher === lastWatcher && watcher.isUpdated)) {
		$("*", panel).remove();
		if (watcher.lastHits.length > 0) {
			group = new NotificationGroup({ hits: watcher.lastHits, isSticky: false, watcher: watcher });
			$(panel).append((group).getDOMElement());

			// This doesn't need a callback since the data will already be cached at this point
			group.addTO(TO.get(Hit.getUniqueReqeusters(watcher.lastHits), _handleTOReceived));
		} else {
			$(panel).append($('<div>').append('<h2>').css('text-align', 'center').html("<br />There are no HITs available.<br /><br />"));
		}
	}
	$(panel).show();

	function _handleTOReceived(data) {
		group.addTO(data);
	}

	lastWatcher = watcher;
}


var IgnoreList = (function() {
	var _time = 60000,
		_hits = [];
		_requesters = [],
		_HIT = 0,
		_REQUESTER = 1;

	function _init() {
		// _clear();
		_load();
		_addListeners();
	}

	function _addListeners() {
		$(window).on('unload', function() { _save(); })
	}

	function _save() {
		localStorage.setItem('notifier_ignore', JSON.stringify(_hits));
		localStorage.setItem('notifier_ignore_requesters', JSON.stringify(_requesters));
	}

	function _load() {
		var storedHits       = localStorage.getItem('notifier_ignore');
		var storedRequesters = localStorage.getItem('notifier_ignore_requesters');

		if (storedHits !== null) {
			try {
				_hits = JSON.parse(storedHits);
			} catch (e) {
				_clear();
				_save();
				console.error("Ignored hits couldn't be loaded correctly.");
			}
		} else {
			console.log("No ignored hits found");
		}

		if (storedRequesters !== null) {
			try {
				_requesters = JSON.parse(storedRequesters);
			} catch (e) {
				_clear();
				_save();
				console.error("Ignored requesters couldn't be loaded correctly.");
			}
		} else {
			console.log("No ignored requesters found");
		}
	}

	function _clear() {
		_hits = [];
		_requesters = [];
		localStorage.removeItem('notifier_ignore');
		localStorage.removeItem('notifier_ignore_requesters');
	}

	function _contains(type, item) {
		if (type === _HIT)
			return (_hits.indexOf(item) !== -1);
		else
			return (_requesters.indexOf(item) !== -1);
	}

	function _isIgnored(requester) {
		return (_requesters.indexOf(requester) !== -1);
	}

	function _isMuted(item) {
		return (_hits.indexOf(item) !== -1);
	}

	function _filter(hits) {
		var filteredHits = [];

		for (var i = 0, len = hits.length; i < len; i++) {
			var hit = hits[i];

			if ((_hits.indexOf(hit.id) === -1) && (_requesters.indexOf(hit.requester) === -1))
				filteredHits.push(hit);
		}

		return filteredHits;
	}

	function _add(type, id) {
		if (type === _HIT) {
			if (_hits.indexOf(id) === -1)
				_hits.push(id);
		} else if (type === _REQUESTER) {
			if (_requesters.indexOf(id) === -1)
				_requesters.push(id);
		}

		_save();
	}

	function _remove(type, id) {
		if (type === _HIT) {
			var index = _hits.indexOf(id);

			if (index !== -1)
				_hits.splice(index, 1);

		} else if (type === _REQUESTER) {
			var index = _requesters.indexOf(id);

			if (index !== -1)
				_requesters.splice(index, 1);
		}

		_save();
	}

	return {
		init: _init,
		add: _add,
		remove: _remove,
		filter: _filter,
		isMuted: _isMuted,
		isIgnored: _isIgnored,
		HIT: _HIT,
		REQUESTER: _REQUESTER
	}
})();

function formatDuration(seconds) {
	const minute = 60; // seconds
	const hour = minute * 60;
	const day = hour * 24;

	let remainingSeconds = seconds;

	function getUnitCount(unitDuration) {
		const count = Math.floor(remainingSeconds / unitDuration);
		remainingSeconds -= count * unitDuration;
		return count;
	}

	const days = getUnitCount(day);
	const hours = getUnitCount(hour);
	const minutes = getUnitCount(minute);

	const unit = (number, abreviation) => number ? `${number}${abreviation}` : '';
	return `${unit(days, 'd')}${unit(hours, 'h')}${unit(minutes, 'm')}${unit(remainingSeconds, 's')}`;
}

function formatCurrency(amount) {
	let formattedAmount = amount;

	if (!!((amount % 1) && !((amount * 10) % 1))) { // Add a 0 if there's one decimal place i.e. 2.5
		formattedAmount = String(amount) + '0';
	} else if (!(amount % 1)) { // Add .00 if there are no decimals
		formattedAmount = String(amount) + '.00';
	}

	return '$' + formattedAmount;
}
function Evt() { /* Nothing */ };
Evt.ADD          = 1;
Evt.REMOVE       = 2;
Evt.START        = 3;
Evt.STOP         = 4;
Evt.CHANGE       = 5;
Evt.UPDATE       = 6;
Evt.HITS_CHANGE  = 7;
Evt.DELETE       = 8;
Evt.VIEW_DETAILS = 9;

Evt.prototype.addListener = function(type, callback) {
	switch(type) {
		case Evt.ADD:
			this.listener.onadd.push(callback);
			break;
		case Evt.REMOVE:
			this.listener.onremove.push(callback);
			break;
		case Evt.START:
			this.listener.onstart.push(callback);
			break;
		case Evt.STOP:
			this.listener.onstop.push(callback);
			break;
		case Evt.CHANGE:
			this.listener.onchange.push(callback);
			break;
		case Evt.UPDATE:
			this.listener.onupdate.push(callback);
			break;
		case Evt.HITS_CHANGE:
			this.listener.onhitschange.push(callback);
			break;
		case Evt.DELETE:
			this.listener.ondelete.push(callback);
			break;
		case Evt.VIEW_DETAILS:
			this.listener.onviewdetails.push(callback);
			break;
		default:
			console.error("Invalid Event type in addListener()");
	}
}

Evt.prototype.notify = function(type, data) {
	switch(type) {
		case Evt.ADD:
			this.callFunctionArray(this.listener.onadd, data);
			break;
		case Evt.REMOVE:
			this.callFunctionArray(this.listener.onremove, data);
			break;
		case Evt.START:
			this.callFunctionArray(this.listener.onstart, data);
			break;
		case Evt.STOP:
			this.callFunctionArray(this.listener.onstop, data);
			break;
		case Evt.CHANGE:
			this.callFunctionArray(this.listener.onchange, data);
			break;
		case Evt.UPDATE:
			this.callFunctionArray(this.listener.onupdate, data);
			break;
		case Evt.HITS_CHANGE:
			this.callFunctionArray(this.listener.onhitschange, data);
			break;
		case Evt.DELETE:
			this.callFunctionArray(this.listener.ondelete, data);
			break;
		case Evt.VIEW_DETAILS:
			this.callFunctionArray(this.listener.onviewdetails, data);
			break;
		default:
			console.error("Unknown event type:", type);
	}
}

Evt.prototype.callFunctionArray = function(functions, data) {
	if (functions.length > 0)
		for (var i = 0, len = functions.length; i < len; i++)
			functions[i](data);
}



var DispatchUI = {
	create: function(dispatch) {
		DispatchUI.dispatch = dispatch;
		DispatchUI.init();
		DispatchUI.addStyle();
		DispatchUI.addActions();
		DispatchUI.addListeners();
		DispatchUI.addDragAndDrop();
		return DispatchUI.div;
	},

	init: function() {
		var div = DispatchUI.div = $('<div id="dispatcher"><div id="controller"></div><div id="watcher_container"></div></div>');

		DispatchUI.watchers = [];

		// Move dashboard contents to the right and put the dispatch panel on the left
		var pageElements = $("body > *");
		$("body").html("");
		$("body").append(
			$("<div>")
				.attr('id', "content_container")
				.append($(pageElements))
		);

		$("body").css('margin', "0").prepend(div);

		var ctrl = DispatchUI.ctrl = $("#controller", div);
		var settingsBtn = $('<a id="settings" href="javascript:void(0)" title="Setting"><img /></a')
				.click(function() {
					if (!SettingsDialog.isVisible())
						SettingsDialog.show();
					else
						SettingsDialog.hide();
				});

		ctrl.append(
			settingsBtn,
			'<div class="play_container">\
				<div class="play_all" title="Start All"></div>\
				<div class="play selected" title="Start Selected"></div>\
				<div class="pause" title="Pause All"></div>\
			</div>',
			"Turkmaster"
		);

		// Adding the data URL inline wouldn't work for some reason, so I'm doing it this way.
		// Image from http://latierrasenosestrecha.org/wp-content/themes/purity/img/icons/settings.png
		$("img", settingsBtn)[0].src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpFRUQ3N0Q2NkUyQjJFMDExOTM4OUZBRkY5RUM4NjkxMiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFQjNFQjA2OEIyRTYxMUUwOUZDRUUxRERBNzIzQkY1NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFQjNFQjA2N0IyRTYxMUUwOUZDRUUxRERBNzIzQkY1NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFRkQ3N0Q2NkUyQjJFMDExOTM4OUZBRkY5RUM4NjkxMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFRUQ3N0Q2NkUyQjJFMDExOTM4OUZBRkY5RUM4NjkxMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PiP+YRgAAALDSURBVHja7FdPZB1BGJ9tawmt8HinrUQpjdBqqFaq5BChJBI5xCvVQ1mPRq89hGqkeusxpLpKDtFTqpEorUZKKdFcWo2WR7REwyOE8niUeP19/Jbxmd3NbjXv0Aw/szO733z/ft/MrNdqtUw72zHT5tZ2A04UEYqiaBTdPFAC9oDb1Wp15TAjMEflhv3cP4kAPC2j6wK24OEv61WgPg2U3El054BtyO2m6fCSqgCLDKBbBjqBHWAQ+A3cAmYcItPAAp/XgDOAGD0GI97nMgDKRel3K8yGuRbP/BSHxMCGkhMjemBEPQ8HetUica79jJT6Djlx5nxeEn4GagnvNiSswCl45UnP8UbC99vAehEOSA4/AmUrvPeBF8A9YJgEFQVvgMdABXhgRUoIeBWGbhUtw07rOWRkvgB3qNywrwKfGIVJJb9fdB+oWJ6Ih8LkRRLR1WT+JfAO+GBxonLgMkTYx9E9cxBpBLgO3OW4xt1vHTJDlIkj8oQGPFdrSBWFkFlKi8CsQ7khicat8YQolwf0qyrso4yWcVTRbFYKAofgPpTsWWRsYrzJiL2yKiNuZZLP1YIsA1ybxXEoKlmLdmB8kd6PcK7f+n7XMtZkra/PgpAHS5eaFwUrZL+0RRgRMtfD6jASwg4k7AeTefaBGda0NMnzTeBrincx0XpYDdc49xCRmi5ShpHkm89D9PRGSn5lfoJkjZU3uU6hfcDnwRK3p0zFBS5at/Iacb5fMb2Rsm8kG4Dwd6B7q8ItBj3iES0p6eNZ0Mfxa773VUWs8XTNdSERlp9NeHeZO6IYepBLT0CZ1Twp+EZCaYI1M5Q1HXJyH9jMlQJev8YoLO0HcAXoBqYS1pri+0v83lg3onru45ghLvFqVcMiDWu+5TDaUxzq/as7YcZl9afaVneg6PRhXstDVYZh0Wu5d/Rv+N8b8EeAAQBg+uBx8hdn9gAAAABJRU5ErkJggg==";
	},

	addActions: function() {
		var dispatch = DispatchUI.dispatch,
			ctrl = DispatchUI.div.find("#controller");
		$("div.play_all", ctrl).mousedown(function() {
			dispatch.start(true);
		});
		$("div.play", ctrl).mousedown(function() {
			dispatch.start();
		});
		$("div.pause", ctrl).mousedown(function() {
			dispatch.stop();
		});
	},

	addListeners: function() {
		var dispatch = DispatchUI.dispatch;
		var div = DispatchUI.div;

		dispatch.addListener(Evt.ADD, function(watcher) {
			// This could be done on one line, but then we would lose access to the WatcherUI's internal Watcher object and functionality
			var watcher = WatcherUI.create(watcher);
			$("#watcher_container", DispatchUI.div).append(watcher.element);
			DispatchUI.watchers.push(watcher);
			// watchers.push(WatcherUI.create(watcher).appendTo($("#watcher_container", div)));
		});

		dispatch.addListener(Evt.REMOVE, function(watcher) {
			// Remove watcher from array
			var index = -1,
				watchers = DispatchUI.watchers;

			for (var i = 0, len = watchers.length; i < len; i++) {
				if (watchers[i].watcher === watcher) {
					index = i;
					break;
				}
			}

			if (index !== -1)
				DispatchUI.watchers.splice(index, 1);
		});

		DispatchUI.setHide = function() {
			if (settings.canHide) {
				$(window).on('click', handleWindowClick);
			} else {
				$(window).off('click', handleWindowClick);
			}
		}

		DispatchUI.setHide();

		function handleWindowClick(e) {
			if (!div.is(e.target) && div.has(e.target).length === 0 && $(".notification_panel").has(e.target).length === 0 && !$("#settingsDialog").is(e.target) && $("#settingsDialog").has(e.target).length === 0) {
				hide();
				$(window).off('click', handleWindowClick);
				$(div).on('click', handleDivClick);
			}
		}

		function handleDivClick(e) {
			show();
			$(div).off('click', handleDivClick);

			if (settings.canHide)
				$(window).on('click', handleWindowClick);
		}

		function hide() {
			div.addClass("tm-hidden");
			$("#content_container").addClass("full");
			$("#details_panel").addClass("left");
		}

		function show() {
			div.removeClass("tm-hidden");
			$("#content_container").removeClass("full");
			$("#details_panel").removeClass("left");
		}
	},

	addStyle: function() {
		addStyle("#dispatcher { background-color: #f5f5f5; position: fixed; top: 0px; float: left; left: 0; height: 100%;  width: 270px; font-size: 8pt;  margin-left: 0px; transition: left 0.5s ease; }\
			#dispatcher.tm-hidden { left: -240px }\
			#content_container { position: absolute; left: 270px; top: 0; right: 0; border-left: 2px solid #dadada; transition: left 0.5s ease; }\
			#content_container.full { left: 30px }\
			#dispatcher #controller { text-align: center; font: 160% Candara, sans-serif; color: #585858; position: relative; padding: 3px 5px; }\
			#dispatcher #controller .on_off { margin: 6px 5px 0 0 }\
			#dispatcher #controller .on_off a { font-size: 80% }\
			#dispatcher #controller #settings { top: 2px; position: absolute; right: 5px; }\
			#dispatcher #controller #settings img { width: 1.5em }\
			#dispatcher #controller .play_container { position: absolute; left: 5px }\
			#dispatcher #watcher_container { position: absolute; top: 30px; bottom: 0; overflow-y:auto; width: 100%;}\
			#dispatcher #watcher_container p { margin: 30px 0px }\
			#dispatcher #watcher_container .error_button a { text-decoration: none; color: #555; background-color: #fff; padding: 3px 10px; margin: 5px; border: 1px solid #aaa; border-radius: 2px }\
			#dispatcher #watcher_container .error_button a:hover { background-color: #def; border-color: #aaa }\
			#dispatcher div { font-size: 7pt }\
			#dispatcher .watcher {\
				box-sizing: border-box;\
				margin: 3px 3px 0;\
				background-color: #fff;\
				position: relative;\
				border-bottom: 1px solid #ddd;\
				border-right: 1px solid #ddd;\
				top: 0;\
				transition: background-color 0.5s, top 0.1s;\
				-moz-user-select: none;\
				-webkit-touch-callout: none;\
				-webkit-user-select: none;\
				-khtml-user-select: none;\
			}\
			#dispatcher .watcher.dragging { cursor: grabbing; z-index: 100; opacity: 0.8; transition: background-color 0.5s, top 0s }\
			#dispatcher .watcher div { font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif }\
			#dispatcher .watcher.running .details { background-color: #BADFF2 }\
			#dispatcher .watcher.updated { background-color: rgba(218, 240, 251, 1); }\
			#dispatcher .watcher .details { width: 25px; text-align: center; float: right; background-color: rgba(234, 234, 234, 1); position: absolute; top: 0; bottom: 0; right: 0; font-size: 90%; color: #fff; transition: background-color 0.5s }\
			#dispatcher .watcher .details.updated { background-color: rgba(218, 240, 251, 1); background-color: #F0FF60; }\
			#dispatcher .watcher .name { font-size 130%; color: black; text-decoration: none; display: inline-block; margin-top: -3px}\
			#dispatcher .watcher .name:hover { text-decoration: underline }\
			#dispatcher .watcher.dragging .name:hover { text-decoration: none }\
			#dispatcher .watcher .time { display: block; float: left; font-size: 80% }\
			.on_off { float: right; cursor: pointer }\
			.on_off a { color: #333; margin: 1px; font-size: 56%; font-weight: bold }\
			.on_off a:nth-child(2) { background-color: #aeaeae; color: #fff; border-radius: 12px; padding: 3px 6px; }\
			.on_off.on a:nth-child(1) { background-color: #55b8ea; color: #fff; border-radius: 12px; padding: 3px 6px; }\
			.on_off.on a:nth-child(2) { background-color: inherit; color: #333; border-radius: inherit; padding: inherit; }\
			.watcher .on_off {  }\
			#dispatcher .watcher > .content { margin-right: 25px; padding: 5px 5px 5px 33px;}\
			#dispatcher .watcher .bottom { margin: 0 0 -5px; color: #aaa }\
			#dispatcher .watcher .bottom a:link { color: black; }\
			#dispatcher .watcher .bottom a:hover { color: #cef; }\
			#dispatcher .watcher .details { font-size: 150%; font-weight: bold }\
			#dispatcher .watcher .last_updated { position: absolute; right: 30px; bottom: 4px; font-size: 80% }\
			#dispatcher .watcher .icons { visibility: hidden; margin-left: 10px; bottom: 5px }\
			#dispatcher .watcher:hover .icons { visibility: visible }\
			#dispatcher .watcher .icons img { opacity: 0.2; height: 0.9em }\
			#dispatcher .watcher .icons img:hover { opacity: 1 }\
			#dispatcher .watcher .color_code { position: absolute; left: 0; top: 0; bottom: 0; width: 9px; cursor: grab;}\
			#dispatcher .watcher .color_code div { position: absolute; left: 0; top: 0; bottom: 0; width: 5px; transition: width 0.15s; }\
			#dispatcher .watcher.dragging .color_code { cursor: grabbing; }\
			#dispatcher .watcher .color_code:hover div { width: 9px }\
			#dispatcher .watcher .color_code.hit div       { background-color: rgba(234, 111, 111, .7); }\
			#dispatcher .watcher .color_code.requester div { background-color: rgba(51, 147, 255, .7); }\
			#dispatcher .watcher .color_code.url div       { background-color: rgba(57, 221, 122, .7); }\
			.watcher .play_container {\
				padding: 0px 0px 0px 12px;\
				float: left;\
				cursor: default;\
			}\
			.play, .pause, .play_all {\
				width:20px;\
				height: 20px;\
				position: relative;\
				display: block;\
			}\
			#controller .play, #controller .pause, #controller .play_all { float: left; }\
			.play:before, .play_all:before {\
				width: 0;\
				height: 0;\
				border-width: 8px 11px;\
				border-style: solid;\
				border-color: transparent transparent transparent #747474;\
				position: absolute;\
				content: '';\
				top: 3px;\
				left: 0px;\
			}\
			.play.selected:after {\
				width: 6px;\
				height: 6px;\
				border-radius: 1px;\
				position: absolute;\
				content: '';\
				background-color: #999;\
				top: 13px;\
				right: 9px;\
			}\
			.play_all:after {\
				width: 0;\
				height: 0;\
				border-width: 8px 11px;\
				border-style: solid;\
				border-color: transparent transparent transparent #999;\
				position: absolute;\
				content: '';\
				top: 3px;\
				left: 5px;\
			}\
			.watcher.running .play:before, .watcher.running .play:after, .pause:before, .pause:after {\
				width: 4px;\
				height: 15px;\
				background: #747474;\
				position: absolute;\
				content: '';\
				top: 3px;\
			}\
			.watcher.running .play:before, .pause:before {\
				left: 0px;\
				border: none;\
			}\
			.watcher.running .play:after, .pause:after {\
				left: 6px;\
			}\
			.play_select {\
				width: 6px;\
				height: 6px;\
				border: 2px solid #cecece;\
				border-radius: 2px;\
				margin-top: 2px;\
			}\
			.watcher.selected .play_select { background-color: #55b8ea; border-color: #b4e6ff; }\
			.watcher.incompatible { background-color: #e9e9e9 !important }\
			");
	},

	addDragAndDrop: function() {
		// Drag watchers
		var startY, currentBaseY, max, min, height,
			dragDiv, nextDiv, prevDiv, startPos, endPos, isDragging,
			slop = 7, currentWatcher, watchers = DispatchUI.watchers;

		DispatchUI.div.on("mousedown", ".watcher", function(e) {
			isDragging = false;

			// Get the position of the watcher in the listing
			startPos = endPos = $("#watcher_container .watcher").index(e.currentTarget);

			// Get reference to the selected watcher
			currentWatcher = watchers[startPos];
			dragDiv = currentWatcher.element;
			nextDiv = dragDiv.next();
			prevDiv = dragDiv.prev();

			// TODO Check target to prevent dragging from a component inside the watcher (i.e. buttons, links, etc.)
			height = dragDiv.outerHeight(true);

			startY = e.clientY;
			currentBaseY = dragDiv.offset().top;

			// max = Math.min($("#watcher_container").outerHeight(true), height * (DispatchUI.dispatch.watchers.length + .75)) - height;
			min = watchers[0].element.offset().top;
			max = Math.min($("#watcher_container").outerHeight(true), height * (watchers.length - 1) + watchers[0].element.offset().top);

			$(window).on("mousemove", move);
			$(window).on("mouseup", up);
		});

		function move(e) {
			var offsetY = e.clientY - startY;

			if (!isDragging && (Math.abs(offsetY) > slop)) {
				// Start dragging
				isDragging = true;

				dragDiv.addClass("dragging");
			}

			if (isDragging) {
				dragDiv.css('top', offsetY);

				var diffY = dragDiv.offset().top - currentBaseY;

				if (dragDiv.offset().top > max) {
					dragDiv.offset({ top: max });
					diffY = 0;
				} else if (dragDiv.offset().top < min) {
					dragDiv.offset({ top: min });
					diffY = 0;
				}

				if (diffY > height / 2) {
					// Move down one spot
					nextDiv.offset({ 'top': nextDiv.offset().top - height });
					nextDiv = nextDiv.nextAll(":not(.dragging)").first();
					prevDiv = (prevDiv.length) ? prevDiv.nextAll(":not(.dragging)").first() : (dragDiv !== watchers[0].element) ? watchers[0].element : watchers[1].element;

					currentBaseY += height;
					endPos++;
				} else if (-diffY > height / 2) {
					// Move up one spot
					prevDiv.offset({ 'top': prevDiv.offset().top + height });
					prevDiv = prevDiv.prevAll(":not(.dragging)").first();
					nextDiv = (nextDiv.length) ? nextDiv.prevAll(":not(.dragging)").first() : (dragDiv !== watchers[watchers.length - 1].element) ? watchers[watchers.length - 1].element : watchers[watchers.length - 2].element;

					currentBaseY -= height;
					endPos--;
				}
			}
		}

		function up(e) {
			$(window).off("mousemove", move);
			$(window).off("mouseup", up);

			if (isDragging) {
				e.preventDefault();
				dragDiv.removeClass("dragging");
				isDragging = false;

				// $("div", colorCode).css('width', '');
				dragDiv.css('cursor', '');
				dragDiv.css('z-index', '');
				dragDiv.css('opacity', '');
				$(".name", dragDiv).removeClass("no_hover");

				// Reset all watcher offsets
				$("#watcher_container .watcher").css('transition', "background-color 0.5s, top 0s");
				$("#watcher_container .watcher").css('top', '');

				setTimeout(function() { $("#watcher_container .watcher").css('transition', ''); }, 600);

				if (startPos !== endPos) {
					if (endPos > startPos)
						dragDiv.insertAfter($("#watcher_container .watcher")[endPos]);
					else
						dragDiv.insertBefore($("#watcher_container .watcher")[endPos]);

					DispatchUI.dispatch.moveWatcher(startPos, endPos);

					// Re-arrange our watchers array
					watchers.splice(startPos, 1);
					watchers.splice(endPos, 0, currentWatcher);
				}
			}
		}
	}
}

/** Dispatch object. Controls all of the watchers.

**/
function Dispatch() {
	this.watchers = new Array();
	this.isLoading = false;

	// Listeners
	this.listener = {
		onadd:		[],
		onremove:	[],
		onstart:	[],
		onstop:		[],
		onchange:	[]
	};
}
Dispatch.prototype = new Evt();
Dispatch.prototype.start = function(startAll) {
	if (this.watchers.length > 0) {
		var count = 0;
		for (var i = 0, len = this.watchers.length; i < len; i++) {
			// Don't start them all at the same time. There is a 2 second delay
			// between each start. It had to be done in a self-executing function
			// in order for the setTimeout to work properly.
			if (this.watchers[i].state.isSelected || startAll) {
				(function (watcher, x){
						watcher.timer = setTimeout(function() { watcher.start(); }, x * 0000); // Let's try 0ms
				})(this.watchers[i], count++);
			}
		}
	}
	this.notify(Evt.START, null);
}
Dispatch.prototype.stop = function() {
	// Stop all Watchers
	if (this.watchers.length > 0) {
		for (var i = 0, len = this.watchers.length; i < len; i++)
			this.watchers[i].stop();
	}
	this.interruptStart = true;
	this.notify(Evt.STOP, null)
}
Dispatch.prototype.add = function(watcher) {
	var self = this;

	watcher.addListener(Evt.CHANGE, function() {
		self.save();
	})

	this.watchers.push(watcher);

	if (!this.isLoading) {
		this.save();
	}

	this.notify(Evt.ADD, watcher);

	// TODO Add a listener to save the watcher list after a watcher has been changed
	return watcher;
}
Dispatch.prototype.save = function() {
    if (!loadError) {
        // localStorage.setItem('notifier_watchers', JSON.stringify(dispatch.watchers, Watcher.replacerArray));
        GM_setValue('notifier_watchers', JSON.stringify(dispatch.watchers, Watcher.replacerArray));

        var lastChecked = getLastChecked(dispatch.watchers);

        if (lastChecked > 0)
	        localStorage.setItem('notifier_watchers_lastChecked', JSON.stringify(lastChecked));
    }

    function getLastChecked(watchers) {
    	var lastChecked = (watchers[0].date) ? watchers[0].date.getTime() : 0;

    	for (var i = 1, len = watchers.length; i < len; i++) {
    		if (watchers[i].isRunning)
    			return new Date.getTime();

    		if ((watchers[i].date) && (watchers[i].date.getTime() > lastChecked))
    			lastChecked = watchers[i].date.getTime();
    	}

    	return lastChecked;
    }
}
Dispatch.prototype.load = function() {
	this.isLoading = true;

	var data;
	var watchers,
		lastChecked = localStorage.getItem('notifier_watchers_lastChecked');

	data = GM_getValue('notifier_watchers');

	if (typeof data === 'undefined')
		data = localStorage.getItem('notifier_watchers');

	if (data !== null) {
		try {
			watchers = JSON.parse(data);

			try {
				lastChecked = JSON.parse(lastChecked);
			} catch(e) {
				lastChecked = null;
			}

			var now = new Date().getTime(),
				expTime = 180000,	// 3 minutes
				expired = (lastChecked !== null) ? now - lastChecked > expTime : false; // Expired if most recent watcher update happened more than x minutes before page was loaded

			// Add the watchers. Clear last hits if past the expiration time
			for(var i = 0; i < watchers.length; i++) {
				if (expired)
					watchers[i].lastHits = [];

				this.add(new Watcher(watchers[i]));
			}

		} catch(e) {
			loadError = true;
			console.error("Error loading saved list", e);
        }
	} else {
		loadDefaultWatchers();
	}

	this.isLoading = false;
}
Dispatch.prototype.remove = function(watcher) {
	var index = this.watchers.indexOf(watcher);

	if (index !== -1)
		this.watchers.splice(index, 1);

	watcher.delete();
	this.save();
	this.notify(Evt.REMOVE, watcher);
}
Dispatch.prototype.moveWatcher = function(from, to) {
	if ((to >= 0 && to < this.watchers.length) && (from >= 0 && from < this.watchers.length)) {
		var watcher = this.watchers.splice(from, 1);
		this.watchers.splice(to, 0, watcher[0]);
		this.save();
	}
}
Dispatch.prototype.getWatcherByProperty = function(name, value) {
	if (this.watchers.length > 0) {
		for (var i = 0, len = this.watchers.length; i < len; i++) {
			if (this.watchers[i][name] === value)
				return this.watchers[i];
		}
	}
	return null;
}
Dispatch.prototype.getWatcherIndex = function(watcher) {
	return this.watchers.indexOf(watcher);
}
Dispatch.prototype.getWatcher = function(index) {
	return this.watchers[index];
}
Dispatch.prototype.getWatcherCount = function() {
	return this.watchers.length;
}
Dispatch.prototype.hideWatchers = function() {
	$("#controller a").css('display', "none");
	$("#watcher_container").html("");
	$("#watcher_container")
		.css('background-color', "#f9f9f9")
		.css('color', "#ff6b6b")
		.css('text-align', "center").append(
			$("<p>").text("There is already a notifier running on a different page."),
			$("<p>").addClass("error_button").append(
				$("<a>")
					.html("Close")
					.attr('href', "javascript:void(0)")
					.click(function() {
						$("#dispatcher").css('display', "none");
						$("#content_container").css('left', "0px");
					}),
				$("<a>")
					.html("Show")
					.attr('href', "javascript:void(0)")
					.click(function() {
						sendMessage({ header: 'show_main' });
					})
			));
}
Dispatch.prototype.onRequestMainDenied = function() {
	pageType.MAIN = false;
	this.hideWatchers();
}
Dispatch.prototype.exportWatchers = function() {
	var watcherAttrs = ["id", "time", "type", "name", "option", "auto", "alert", "stopOnCatch", "state", "isSelected", "url"];

	return JSON.stringify(this.watchers, watcherAttrs);
}
Dispatch.prototype.importWatchers = function(data) {
	try {
		data = JSON.parse(data);
		dispatch.isLoading = true;

		for (var i = 0, len = data.length; i < len; i++) {
			var watcher = new Watcher(data[i]);

			if (!this.getWatcherByProperty('id', watcher.id) && !this.getWatcherByProperty('name', watcher.name))
				this.add(watcher);
		}

		dispatch.isLoading = false;
		dispatch.save();

		console.log("Watchers imported", dispatch.watchers);
	} catch(e) {
		console.error("Error importing watchers", e, data);
		alert("Invalid input. Try disabling word wrap on your text editor and re-copy.")
	}
}


function watcherDialog(watcher, callback) {
	var dialog = $("<div>").attr('id', 'add_watcher_form').append(
	$("<h3>").text("Add a watcher"),
	$("<p>").append(
		$("<label>").text("Name ").append(
			$("<input>").attr('id', "watcherName").attr('type', "text").val(watcher.name)),
		$("<label>").text(" Time ").append(
			$("<input>").attr('id', "watcherDuration").attr('type', "text").val(watcher.time / 1000))
		),
		(watcher.type === "hit") ?
			$("<p>").append(
				$("<input>").attr('type', "checkbox").attr('id', "autoaccept").prop('checked', watcher.option.auto),
				$("<label>").attr('for', "autoaccept").text("Auto-accept")
				)
			: "",
		(watcher.type === "hit") ?
			$("<p>").append(
				$("<input>").attr('type', "checkbox").attr('id', "stopaccept").prop('checked', watcher.option.stopOnCatch),
				$("<label>").attr('for', "stopaccept").text("Stop on accept")
				)
			: "",
		$("<p>").append(
			$("<input>").attr('type', "checkbox").attr('id', "alert").prop('checked', watcher.option.alert),
			$("<label>").attr('for', "alert").text("Alert")
			),
		(watcher.type === "hit") ?
			$("<p>").append(
				$("<input>").attr('type', "checkbox").attr('id', "mutebatch").prop('checked', watcher.option.muteBatch),
				$("<label>").attr('for', "mutebatch").text("Silence batches after first seen")
				)
			: "",
		$("<p>").addClass("form_buttons").append(
			$("<input>").attr('type', "button").attr('value', "Save"),
			$("<input>").attr('type', "button").attr('value', "Cancel")
			)
	);

	function save() {
		callback({
			name		: $("#watcherName", dialog).val(),
			time		: parseInt($("#watcherDuration", dialog).val(), 10) * 1000,
			alert		: $("#alert", dialog).prop('checked'),
			auto		: $("#autoaccept", dialog).prop('checked'),
			stopOnCatch	: $("#stopaccept", dialog).prop('checked'),
			muteBatch	: $("#mutebatch", dialog).prop('checked')
		})

		hide();
	};

	function hide() {
		dialog.hide();
		dialog.remove();
		dialog.empty();
	}

	$("input[value='Save']", dialog).click(save);

	$("input[type='button']", dialog).click(hide);

	$(dialog).keydown(function(e) {
		switch(e.keyCode) {
			case 13: // Enter
				save();
				break;
			case 27: // Esc
				hide();
				break;
		}
	});

	$("body").append(dialog);

	if ($("#watcherName", dialog).val() === "")
		$("#watcherName", dialog).focus().select();
	else
		$("#watcherDuration", dialog).focus().select();
}


function WatcherUI() { /* Nothing */ };
WatcherUI.create = function(watcher) {
	// Create jQuery Element...
	var div = $("<div>").addClass("watcher")
		.html('<div class="details"> > </div>'
		+ (!watcher.isIncompatible() ? '<div class="play_container"><div class="play"></div><div class="play_select"></div></div>' : '')
		+ '<div class="content">\
			<a class="name" href="' + watcher.getURL() + '" target="_blank">' + ((typeof watcher.name !== 'undefined') ? watcher.name : watcher.id) + '</a>\
			<div class="bottom">\
	            <span class="time">' + (watcher.time / 1000) + ' seconds </span>\
	            <span class="icons">\
	                <a class="edit" href="javascript:void(0)"><img src="https://i.imgur.com/peEhuHZ.png" /></a>\
	                <a class="delete" href="javascript:void(0)"><img src="https://i.imgur.com/5snaSxU.png" /></a>\
	            </span>\
				<div class="last_updated" title="Last checked: ' + ((typeof watcher.date !== 'undefined') ? watcher.date.toString() : "n/a") + '">' + ((typeof watcher.date !== 'undefined') ? watcher.getFormattedTime() : "n/a") + '</div>\
			</div>\
			<div class="color_code"><div></div></div>\
		</div>');

	if (watcher.state.isSelected)
		div.addClass("selected");

		if (watcher.isIncompatible()) {
			div.addClass("incompatible");
			div.prop('title', 'This URL watcher is incomaptible with the new worker site');
		}

	// Add listeners
	watcher.addListener(Evt.START, function() {
		div.addClass("running");
	});

	watcher.addListener(Evt.STOP, function() {
		div.removeClass("running");
	});

	watcher.addListener(Evt.UPDATE, function(e) {
		$(".last_updated", div).text(watcher.getFormattedTime()).attr('title', "Last checked: " + watcher.date.toString());
		div.addClass("updated");
		setTimeout(function() { div.removeClass("updated") }, 1000);
	});

	watcher.addListener(Evt.CHANGE, function() {
		$(".name", div).text(watcher.name).attr('href', watcher.url + URL_POSTFIX);
		$(".time", div).text(watcher.time / 1000 + " seconds");

		if (watcher.state.isSelected)
			$(div).addClass("selected");
		else
			$(div).removeClass("selected");
	});

	watcher.addListener(Evt.HITS_CHANGE, function() {
		$(".details", div).addClass("updated");
	});

	watcher.addListener(Evt.DELETE, function() {
		div.remove();
	});

	watcher.addListener(Evt.VIEW_DETAILS, function() {
		$(".details", div).removeClass("updated");
	});


	// Set actions
	$(".edit", div).click(showWatcherDialog);
	div.dblclick(showWatcherDialog);

	function showWatcherDialog() {
		watcherDialog(watcher, function(values) {
			watcher.setValues({
				name        : values.name,
				time        : values.time,
				alert       : values.alert,
				auto        : values.auto,
				stopOnCatch : values.stopOnCatch,
				muteBatch   : values.muteBatch
			})
		});
	}

	$(".delete", div).click(function() {
		dispatch.remove(watcher);
	});

	$(".details", div).mouseover(function () {
		showDetailsPanel(watcher);
		$(this).removeClass("updated");
	});

	$("div.play_select", div).mousedown(function() {
		watcher.toggleSelected();
	});

	$("div.play", div).mousedown(function() {
		if (watcher.state.isRunning)
			watcher.stop();
		else
			watcher.start();
	});


	// Add colors for watcher type
	var colorCode = $(".color_code", div);
	if (watcher.type === 'hit') {
		colorCode.addClass("hit");
		colorCode.attr('title', "HIT Watcher");
	} else if (watcher.type === 'requester') {
		colorCode.addClass("requester");
		colorCode.attr('title', "Requester Watcher");
	} else if (watcher.type === 'url') {
		colorCode.addClass("url");
		colorCode.attr('title', "URL Watcher");
	}
	colorCode.attr('title', colorCode.attr('title') + "\nClick and drag to re-order");


	$(".delete img", div).hover(function() { $(this).attr('src', "https://i.imgur.com/guRzYEL.png")}, function() {$(this).attr('src', "https://i.imgur.com/5snaSxU.png")});
	$(".edit img", div).hover(function() { $(this).attr('src', "https://i.imgur.com/VTHXHI4.png")}, function() {$(this).attr('src', "https://i.imgur.com/peEhuHZ.png")});

	return { element: div, watcher: watcher };
}

/**	The Watcher object. This is what controls the pages that are monitored and how often

	Events:
		onStart      - The watcher has started to check the desired page with a time interval
		onStop       - The time interval has stopped
		onUpdate     - The watcher has just checked the page for hits
		onChange     - Attributes of the watcher changed, like name, interval time, etc.
		onDelete     - When a watcher has been deleted
		onHitsChange - The watcher updated and found a different set of hits from last time
		onCaptcha?   - The watcher encounters a captcha. Not sure if this should be handled by the Watcher or Loader (maybe both)

**/
function Watcher(attrs) {
	var DEFAULT_TIME = 60000;
	this.interval    = null;		// For continuous interval
	this.timer       = null; 			// For initial setTimeout
	this.newHits     = [];

	attrs = attrs || {};

	// Default states
	this.state = {};
	state = attrs.state || {};
	this.state.isRunning  = (typeof state.isRunning !== 'undefined') ? state.isRunning : false;
	this.state.isSelected = (typeof state.isSelected !== 'undefined') ? state.isSelected : false;
	this.state.isUpdated  = (typeof state.isUpdated !== 'undefined') ? state.isUpdated : false;

	// TODO Erase these state overwrites once we implement resuming state after a page load
	// Currently if a watcher is on when dispatch saves the watcher list, it'll still be marked as running even
	// though it wouldn't be running on page load.
	this.state.isRunning = false;
	this.state.isUpdated = false;

	// Required attributes
	this.id   = attrs.id;
	this.time = attrs.time || DEFAULT_TIME;
	this.type = attrs.type;
	this.name = attrs.name || this.id;
	this.lastHits = attrs.lastHits || [];

	// Options
	this.option = {};
	option 	= attrs.option || {};
	this.option.auto        = (typeof option.auto !== 'undefined') ? option.auto : false;
	this.option.alert       = (typeof option.alert !== 'undefined') ? option.alert : false;
	this.option.stopOnCatch = (typeof option.stopOnCatch !== 'undefined') ? option.stopOnCatch : true;
	this.option.muteBatch   = (typeof option.muteBatch !== 'undefined') ? option.muteBatch : false;

	// Figure out the URL
	this.url = attrs.url;

	if (typeof this.url === 'undefined')
		this.setUrl();

	// Listeners
	this.listener = {
		onstart       : [],
		onstop        : [],
		onupdate      : [],
		onchange      : [],
		onhitschange  : [],
		ondelete      : [],
		onviewdetails : []
	};

	return this;
}
Watcher.prototype = new Evt();
Watcher.prototype.toString = function() {
	return this.name;
}
Watcher.prototype.getHTML = function() {
	this.DOMElement = $("<div>");
	return $("<div>");
}
Watcher.prototype.getURL = function() {
	if (!pageType.NEW_SITE)
		return this.url + URL_POSTFIX;

	if (this.type === 'requester') {
		return `https://worker.mturk.com/requesters/${this.id}/projects`;
	}

	if (this.type === 'hit') {
		return `https://worker.mturk.com/projects/${this.id}/tasks${this.option.auto ? '/accept_random?ref=w_pl_prvw' : ''}`;
	}

	return this.url;
}
Watcher.prototype.setUrl = function() {
	switch(this.type) {
		case 'hit':
			this.url = "https://www.mturk.com/mturk/preview" + (this.option.auto ? "andaccept" : "") + "?groupId=" + this.id;

			break;
		case 'requester':
			this.url = "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId=" + this.id;
			break;
		case 'url':
			if (typeof this.url === 'undefined')
				this.url = this.id;

			// URL watchers get a random id because of id requirements for CSS
			this.id = "A" + Math.floor(Math.random() * 100000000);
			break;
	}
}
Watcher.prototype.isIncompatible = function () {
	return pageType.NEW_SITE && this.type === 'url' && this.url.startsWith('https://www.mturk.com/')
}
Watcher.prototype.setAuto = function(isAuto) {
	this.option.auto = isAuto;
	this.setUrl();
}
Watcher.prototype.isNewHit = function (hit) {
	return (this.newHits.indexOf(hit) !== -1);
}
Watcher.prototype.onChanged = function(newHits) {
	Messenger.sendHits(this, newHits);
	this.isUpdated = true;
	this.notify(Evt.HITS_CHANGE, newHits);
}
Watcher.prototype.start = function() {
	if (!this.state.isRunning) {
		var _this = this;

		// Set the interval and start right away
		this.interval = setInterval(function(){ _this.getData() }, this.time);
		this.getData();

		this.state.isRunning = true;

		this.notify(Evt.START, null);
	}

	return this;
}
Watcher.prototype.stop = function() {
	// Stop the interval object and the timer object
	clearInterval(this.interval);
	clearTimeout(this.timer);
	this.state.isRunning = false;

	this.notify(Evt.STOP, null);
}
Watcher.prototype.delete = function() {
	this.notify(Evt.DELETE, this);

	this.stop();
	this.listener = null;
	this.newHits  = null;
	this.lastHits = null;
}
Watcher.prototype.filterMessages = function(newHits) {
	// Determine which hits, if any, the user should be notified of
	// For now just showing new hits
	var filteredHits;

	if (typeof this.lastHits !== 'undefined' && this.lastHits.length > 0) {
		filteredHits = [];

		for (var i = 0, len = newHits.length; i < len; i++) {
			for (var j = 0, len2 = this.lastHits.length; j < len2; j++) {
				if (newHits[i].id === this.lastHits[j].id)
					break;

				// If we reach the end with no matches, add it to the changed hits array
				if (j === len2 - 1 )
					filteredHits.push(newHits[i]);
			}
		}
	} else {
		// If "last hits" doesn't exist, then all of the new hits should be considered new
		filteredHits = newHits;
	}

	this.lastHits = newHits;
	return filteredHits;
}
Watcher.prototype.toggleSelected = function() {
	if (this.state.isSelected)
		this.state.isSelected = false;
	else
		this.state.isSelected = true;

	this.notify(Evt.CHANGE, null);
}
Watcher.prototype.markViewed = function () {
	if (this.isUpdated) {
		isUpdated = false;
		this.notify(Evt.VIEW_DETAILS, null);
	}
}
Watcher.prototype.updateWatcherPanel = function() {
	this.date = new Date();
	this.notify(Evt.UPDATE, null);
}
Watcher.prototype.setValues = function(values) {
	var val = values || {};
	this.name = val.name || this.name;
	this.setAuto(val.auto);
	this.option.stopOnCatch = val.stopOnCatch;
	this.option.alert = val.alert;
	this.option.muteBatch = val.muteBatch;

	if (typeof val.time !== 'undefined' && this.time !== val.time) {
		this.time = val.time;

		if (this.state.isRunning) {
			this.stop();
			this.start();
		}
	}

	this.notify(Evt.CHANGE, null);
}
Watcher.prototype.getFormattedTime = function() {
	if (typeof this.date !== 'undefined') {
		var time = this.date;
		var str = "";
		var hours = time.getHours();
		var ampm = "am";

		if (hours >= 12) {
			if (hours > 12)
				hours -= 12;
			ampm = "pm";
		} else if (hours === 0) {
			hours = 12;
		}

		str += hours + ":"
			+ ((time.getMinutes() < 10) ? "0" : "") + time.getMinutes() + ":"
			+ ((time.getSeconds() < 10) ? "0" : "") + time.getSeconds()
			+ ampm;

		return str;
	} else {
		return "N/A";
	}
}
Watcher.prototype.setHits = function(hits) {
	if (typeof hits !== 'undefined') {
		if (Object.prototype.toString.call(hits) !== '[object Array]')
			hits = new Array(hits);
		this.sendHits(hits);
	}
	this.updateWatcherPanel();
}
Watcher.prototype.sendHits = function(hits) {
	// Only send the hits if there is actually something to send
	// In the near future this will have to be changed to show when HITs go away completely
	if (typeof hits !== 'undefined' && hits.length > 0) {
		var newHits = this.newHits = this.filterMessages(hits);

		if (newHits.length) {
			this.onChanged(newHits);
		} else if (this.option.auto && !this.option.stopOnCatch) {
			this.onChanged(newHits); // Might add a different method for this case, but using onChanged for now
		}
	}
}
Watcher.prototype.getData = function() {
	var _this = this;
	Loader.load(this, this.getURL(), function(data) {
		if (!pageType.NEW_SITE || _this.getURL().startsWith('https://www.mturk.com/'))
			_this.onDataReceived($(data));
		else
			_this.onNewDataReceived(data);
	});
}
Watcher.prototype.onNewDataReceived = function(data) {
	try {
		data = JSON.parse(data);

		if (this.type === 'hit') {
			const hit = this.parseNewHit(data);
			this.setHits(hit);

			if (hit && this.option.auto && this.option.stopOnCatch) {
				this.stop();
			}
		} else {
			const hits = data.results.map(this.parseNewHit.bind(this));
			const sortedHits = hits.sort((a, b) => a.lastUpdated < b.lastUpdated ? -1 : 1);
			this.setHits(sortedHits);
		}
	} catch(e) {
		console.log('Not a JSON response');
	}
}
Watcher.prototype.parseNewHit = function(result) {
	const hit = new Hit();

	// From DjBonadoobie (thanks!) [https://greasyfork.org/en/forum/discussion/32519]:
	// Mturk now returns a message object on errors (ie {message: 'Accepted maximum hits/ PRE's/no more hits etc.'})
	// We need to check for and return.
	if (typeof result !== 'object' || result.results || result.message) {
		return; // No more HITs available
	}

	if (this.option.auto) {
		Object.assign(hit, {
			taskID: result.task_id,
			assignmentID: result.assignment_id,
			isAutoAccept: true,
		});
		result = result.project;

	} else {
		Object.assign(hit, {
			url: result.project_tasks_url,
			canPreview: result.caller_meets_preview_requirements,
			isQualified: result.caller_meets_requirements,
		});
  }

	if (result.project) {
		result = result.project;
	}

	return Object.assign(hit, {
		available: result.assignable_hits_count,
		description: result.description,
		lastUpdated: new Date(result.last_updated_time),
		id: result.hit_set_id,
		requester: result.requester_name,
		requesterID: result.requester_id,
		reward: formatCurrency(result.monetary_reward.amount_in_dollars),
		time: formatDuration(result.assignment_duration_in_seconds),
		title: result.title,
	});
}
Watcher.prototype.onDataReceived = function(data) {
	var error = $(".error_title", data);
	if (error.length > 0) {
		if (error.text().contains("You have exceeded")) {
			console.error("Exceeded the maximum rate!");
			return;
		}
	}

	if (this.type === 'hit')
		this.setHits(this.parseHitPage(data));
	else
		this.setHits(this.parseListing(data));
}
Watcher.prototype.parseListing = function(data) {
	var hitCount = $("table:nth-child(3) > tbody:nth-child(1) > tr", data).length;
	var hits = new Array();
	var	qryUrl       = "td:nth-child(3) > span:nth-child(1) > a",
		qryTitle     = "td:nth-child(1) > a:nth-child(1)",
		qryRequester = "td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > a",
		qryReward    = "td:nth-child(3) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > span:nth-child(1)",
		qryAvailable = "td:nth-child(3) > table > tbody > tr:nth-child(2) > td:nth-child(2)",
		qryTime      = "td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2)";

	for (var i = 0; i < hitCount; i++) {
		// Get nearby ancestors so jQuery won't have to do a full search for each element (faster)
		var base    = $("table:nth-child(3) > tbody:nth-child(1) > tr:nth-child(" + (i+1) + ") > td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1)", data),
			topRow  = $("tr:nth-child(2) > td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1)", base),
			content = $("tr:nth-child(3) > td:nth-child(3) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1)", base);

		var hit = new Hit();
		hit.requester   = $(qryRequester, content).text();
		hit.requesterID = $(qryRequester, content).attr("href").match(/requesterId=([A-Z0-9]+)/)[1];
		hit.title       = $(qryTitle, topRow).text().trim();
		hit.reward      = $(qryReward, content).text().trim();
		hit.available   = $(qryAvailable, content).text().trim();
		hit.time        = $(qryTime, content).text().trim();

		var urlData = $(qryUrl, topRow);
		hit.url = urlData.attr("href");

		var idMatch = hit.url.match(/(group|notqualified\?hit|requestqualification\?qualification)Id=([A-Z0-9]+)/);

		if (idMatch !== null) {
			hit.id = idMatch[2];
		}

		hit.canPreview = false;

		// Check each link to see if user is qualified or can preview the HIT, etc.
		urlData.each(function() {
			if (typeof this.href !== 'undefined') {
				if (this.href.contains("qual"))
					hit.isQualified = false;
				else if (this.href.contains("preview"))
					hit.canPreview = true;
			}
		});
		hits[i] = hit;
	}

	return hits;
}
Watcher.prototype.parseHitPage = function(data) {
	var msgbox = $("#alertboxHeader", data);
	var hasCaptcha = ($(data).length > 0) ? ($(data).text()).contains("In order to accept your next HIT") : false;

	if ($(msgbox).length > 0 && ($(msgbox).text()).contains("There are no")) {
		// If there aren't any more available, keep checking. If they were just previously available
		// then we should alert the user that it's gone.
	} else {
		// If it's newly available, alert the user and start auto-stacking if that's desired.
		//TODO We need to test for "You are not qualified to accept this HIT."

		if (hasCaptcha) {
			console.log("Has captcha");
			sendMessage({header: 'captcha'});
		}

		var uid = $("input[name='hitId']", data).attr("value");
		var hit = new Hit({id: this.id, uid: uid, isAutoAccept: this.option.auto});
		hit.requester = $("form:nth-child(7) > div:nth-child(9) > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(3) > td:nth-child(3) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)", data).text().trim();
		hit.title     = $(".capsulelink_bold > div:nth-child(1)", data).text().trim();
		hit.reward    = $("td.capsule_field_text:nth-child(5) > span:nth-child(1)", data).text().trim();
		hit.available = $("td.capsule_field_text:nth-child(8)", data).text().trim();
		hit.time      = $("td.capsule_field_text:nth-child(11)", data).text().trim();

		if (((hasCaptcha && settings.stopOnCaptcha) || (this.option.auto && this.option.stopOnCatch)) && this.state.isRunning)
			// We should probably toggle off all auto-accept hits when we encounter a captcha. Maybe send a special message to all mturk windows while we're at it.
			// The special message could be some kind of banner that says that no more hits can be accepted in the background until the captcha is entered. (It would
			// be pretty cool if we could pull up the captcha image in the background and just show it and the form to enter it from another page).
			this.stop();

		return new Array(hit);
	}
}
Watcher.replacerArray = ["id", "time", "type", "name", "option", "auto", "alert", "stopOnCatch", "state", "isRunning", "isSelected", "isUpdated", "url", "lastHits"];

var Messenger = function() {
	var SEND_HITS = "new_hits";
	var SEND_TO = "turkopticon";
	var notificationGroup;

	function _sendHits(watcher, hits) {
		// Pass through ignore filters
		hits = IgnoreList.filter(hits);

		if (hits.length) {
			var toData = TO.get(Hit.getUniqueReqeusters(hits), _handleTOReceived);

			if (settings.notifications) {
				// Set wasViewed to false to check if any receiving windows were focused when this was sent.
				wasViewed = false;

				// Send Hits
				sendMessage({ header: SEND_HITS, content: { 'title': watcher.name, 'hits': hits, 'url': watcher.url } });

				// Get TO and send it
				if (toData)
					sendMessage({ header: SEND_TO, content: toData });

				// Attempt to send a browser notification after a brief period of time. If another mturk
				// page was visible when it received the hits, this will cancel out.
				if (!document.hasFocus())
					setTimeout(function() { sendDesktopNotification(hits, watcher); }, 200);
			}

			// Show notification on dashboard, too
			notificationGroup = notificationPanel.add(new NotificationGroup({ title: watcher.name, hits: hits, url: watcher.url }));
			notificationGroup.addTO(toData);

			// Sound alert for auto-accept HIT watchers and watchers that have the alert set on
			if (watcher.option.auto || watcher.option.alert)
				Sound.alert(watcher);

		} else {
			if (!watcher.option.muteBatch && watcher.option.auto && !watcher.option.stopOnCatch)
				Sound.alert(watcher);
		}
	}

	function _handleTOReceived(data) {
		sendMessage({ header: SEND_TO, content: data });

		if (data)
			notificationGroup.addTO(data);
	}

	return {
		sendHits: _sendHits
	}

}();


/** Watcher Stack and Queue
	Stack - Grab as many as possible right away
		Limit - The number of HITs to stack at once
			Stop - Stop after the limit is reached
			Queue - Start queing after the limit is reached

	Queue - Grab one at a time, paced about as fast as they can be done
**/


// Loader. This is what loads pages in the background. Page requests get added to a queue
// so we can load pages in moderation to avoid exceeding the maximum request rate.
//
// Public methods:
// - load(watcher, url, callback) is the only "public" method. The callback receives the data from
//	 the requested page.

var Loader = function() {
	var queue        = [],
		pauseTime    = 2000,	// The amount of time to pause (in milliseconds)
		intervalTime = 200,	// The amount of time between page loads
		count        = 0,
		paused       = true,
		maxLoad      = 6;	// The max number of pages to load without pausing

	function _load(watcher, url, callback) {
		if (!_isQueued(watcher)) {
			queue.push({url: url, callback: callback, watcher: watcher});

			// If queue length is now 1 and was paused, it means we should resume loading
			if (queue.length === 1 && paused) {
				paused = false;
				_next();
			}
		}
	}

	// Checks to see if the watcher is already queued
	function _isQueued(watcher) {
		if (queue.length > 0) {
			for (var i = 0, len = queue.length; i < len; i++)
				if (queue[i].watcher === watcher)
					return true;
		}
		return false;
	}

	// GETs thet next URL in the queue
	function _next() {
		if (queue.length > 0) {
			var info = queue.shift();
			_getData(info.url, info.callback);
		} else {
			paused = true;
		}
	}

	function _getData(url, callback) {
		GM_xmlhttpRequest({
			method: "GET",
			url: url,
			headers: {
				accept: 'application/json'
			},
			onload: function(res) {
				callback(res.responseText);

				if (++count < maxLoad) {
					setTimeout(_next, intervalTime);
				} else {
					paused = true;
					count = 0;
					setTimeout(function() {
						if (paused)
							_next();
					}, pauseTime);
				}
			}
		});
	}

	return {
		load: _load
	}
}();

var TO = function() {
	var URL_PREFIX = "https://turkopticon.ucsd.edu/api/multi-attrs.php?ids=",
		cache = {};

	setInterval(function() { cache = {}; }, 3600000); // Clear cache once per hour

	function _get(ids, callback) {
		var results = _getFromCache(ids);

		// If not all requesters found in storage, fetch from server
		if (results.missing.length > 0)
			_fetchFromServer(URL_PREFIX + results.missing.join(','), callback);

		return JSON.stringify(results.found);
	}

	function _getFromCache(ids) {
		var sorted = { found: {}, missing: [] };

		for (var i = 0, len = ids.length; i < len; i++) {
			if (cache[ids[i]])
				sorted.found[ids[i]] = cache[ids[i]];
			else
				sorted.missing.push(ids[i]);
		}

		return sorted;
	}

	function _fetchFromServer(url, callback) {
		$.get(url, function(data) {
			_cache(data);

			if (typeof callback === 'function')
				callback(data);
		})
	}

	function _getCount(obj) {
		var count = 0;

		for (key in obj)
			count++;
	}

	function _cache(data) {
		var ratings = JSON.parse(data);

		for (id in ratings) {
			cache[id] = ratings[id];
		}
	}

	return {
		get: _get
	}
}();

/** The NotificationPanel object. This holds and manipulates incoming notification groups

**/
function NotificationPanel() {
	this.isHidden      = true;
	this.notifications = new Array();
	this.createPanel();
	this.isHovered     = false;
	this.timeout       = null;
}
NotificationPanel.prototype.add = function(notification) {
	var _this = this;

	// Get rid of the leftover notification if there's one there
	if (this.notifications.length > 0 && this.notifications[0].hasTimedOut && !this.notifications[0].isHovered) {
		var oldNotification = this.notifications[0];
		setTimeout(function() { _this.onTimeoutListener(oldNotification);}, 1500);
	}

	// Cancel delayed timeout from mouseout (so panel won't close right after a new
	// notification comes in)
	clearTimeout(this.timeout);

	notification.onTimeout = function() { _this.onTimeoutListener(notification) };
	this.notifications.push(notification);
	this.addToPanel(notification);

	if (this.isHidden) {
		this.show();
	}

	return notification;
}

NotificationPanel.prototype.remove = function(notification) {
	// Don't remove the notification if the user has their mouse hovering over it.
	// The notification will trigger onTimeout later on mouseout which will call
	// this method again for removal.
	if (!notification.isHovered) {
		this.removeFromPanel(notification);

		var newArray = new Array();
		for (var i = 0, len = this.notifications.length; i < len; i++)
			if (this.notifications[i] !== notification)
				newArray.push(this.notifications[i]);

		this.notifications = newArray;
	}
}
NotificationPanel.prototype.show = function() {
	if (this.isHidden) {
		this.getDOMElement().removeClass("tm-hidden");
		this.isHidden = false;
	}
}
NotificationPanel.prototype.hide = function() {
	if (!this.isHidden && !this.isHovered) {
		this.getDOMElement().addClass("tm-hidden");
		this.isHidden = true;
	}
}
NotificationPanel.prototype.createPanel = function() {
	var _this = this;
	var panel =	$('<div class="notification_panel tm-hidden" id="receiver"></div>')
			.hover(
				function() {
					clearTimeout(this.timeout);
					_this.isHovered = true;
					_this.show()
				},
				function(){
					_this.isHovered = false;
					this.timeout = setTimeout(function() { _this.hide() }, 1500); // Delay hiding the panel
				}
			)

	$("body").append(panel);

	this.DOMElement = panel;

	addStyle("\
		.notification_panel div, .notification_panel p { font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif; }\
		#receiver.notification_panel { \
			position      : fixed;\
			width         : 400px;\
			bottom        : 0px;\
			right         : 0px;\
			background    : rgba(255, 255, 255, 1);\
			padding       : 5px;\
			border        : 1px solid #d5d5d5;\
			border-size   : 1px 0 0 1px;\
			overflow      : auto;\
			border-radius :  5px 0 0 0;\
			border-right  : 0;\
			transition    : right 0.2s;\
			z-index       : 1050;\
		}\
		#receiver.notification_panel.tm-hidden {\
			right: -395px;\
		}\
		#receiver .notification_group {\
			background : #fdfdfd;\
			border     : 1px solid #eaeaea;\
			padding    : 5px;\
			margin     : 10px 0;\
			opacity    : 1;\
			overflow   : hidden;\
			transition : opacity 0.7s, max-height 0.2s ease-in-out 0.7s, margin 0.2s linear 0.7s, padding 0.2s linear 0.7s;\
			border-right-color  : #dedede;\
			border-bottom-color : #dedede;\
		}\
		#receiver .notification_group.removed {\
			opacity    : 0;\
			max-height : 0;\
			padding    : 0;\
			margin     : 0;\
		}\
		#receiver .notification_group h3 { margin: 3px; font-weight: normal }\
		#receiver .notification_group h3 a:link, #receiver .notification_group h3 a:visited { color: #333 }\
		#receiver .notification_group h4 a:link,\
		#receiver .notification_group h4 a:visited { margin: 2px 0 0 4px; color: #222; }\
		.notification_panel h2, #details_panel h2 { font-size: 100%; font-weight: normal; margin: 0px }\
		.notification {\
			padding          : 5px 3px 0 5px;\
			background-color : #fff;\
			border-bottom    : 1px solid #e9e9e9;\
			position         : relative;\
			margin-left      : 5px;\
		}\
		.notification:last-child { border: none; padding-bottom: 3px }\
		.notification .mute {\
			position  : absolute;\
			bottom    : 6px;\
			right     : 5px;\
			color     : #999;\
			cursor    : pointer;\
			font-size : 76%;\
		}\
		.notification a.requester:link, .notification a.requester:visited {\
			margin-top  : 2px;\
			color       : black;\
			font-size   : 80%;\
			font-weight : bold;\
		}\
		#details_panel .notification.ignored {\
			opacity: 0.4;\
		}\
		#receiver .notification.ignored {\
			display: none;\
		}\
		.notification .ignore {\
			font-size: 60%;\
			color: #999;\
			visibility: hidden;\
			cursor: pointer;\
		}\
		.notification:hover .ignore {\
			visibility: visible;\
		}\
		.notification .extra_info {\
			font-style : italic;\
			font-size  : 80%;\
			color      : #505050;\
			cursor     : default;\
		}\
		.notification_panel a:link, .notification_panel a:visited {\
			text-decoration : none;\
			color           : #6bf;\
		}\
		.notification_panel a.title:link, .notification_panel a.title:visited {\
			display       : block;\
			white-space   : nowrap;\
			overflow      : hidden;\
			text-overflow : ellipsis;\
			font-size     : 102%;\
		}\
		.notification_panel .links {\
			position : absolute;\
			bottom   : 6px;\
			right    : 35px;\
		}\
		.notification_panel a.hit_link {\
			font-size     : 70%;\
			color         : #fff;\
			background    : none repeat scroll 0% 0% #55B8EA;\
			border-radius : 12px;\
			display       : inline;\
			margin        : 10px 5px 0px 0px;\
			padding       : 3px 9px;\
			font-weight   : bold;\
			transition    : background-color 0.25s;\
		}\
		.notification_panel a.hit_link:visited { background-color: #9df; }\
		.notification_panel a.hit_link:hover { background: #8df; }\
		.notification_panel p {	margin: 3px 0 6px 0; font-size: 80%; cursor: default }\
		.notification_panel .autoaccept {\
			background-color : rgba(148, 236, 255, .3);\
			background-color : rgba(214, 255, 91, 1);\
			background-color : rgba(252, 255, 143, 1);\
		}\
		.notification.not_qualified { background-color: rgba(245, 244, 229, 1) }\
		.notification_panel .new { background-color: rgba(220, 255, 228, 1); }\
		.notification_panel .ratings-button {\
			float: left;\
			margin-right: 0.3em;\
			height: 0.7em;\
			width: 0.7em;\
			background-color: #93C9FF;\
			border-radius: 3px;\
			font-size: 80%;\
			position: relative;\
			top: 0.6em;\
    }\
		.notification_panel .ratings.no-TO .ratings-button {\
			background-color: #ccc;\
		}\
		.notification_panel .ratings-button > .ratings-chart {\
			position: absolute;\
			bottom: -3em;\
			left: 0.4em;\
			background-color: rgb(255, 255, 255);\
			color: #444;\
			visibility: hidden;\
			padding: 0.3em;\
			border: 1px solid #f0f0f0;\
			z-index: 100;\
    }\
		.notification_panel .notification:last-child .ratings-chart {\
			bottom: -2em;\
		}\
		.notification_panel .ratings-button:hover > .ratings-chart { visibility: visible; }\
		.notification_panel .ratings.no-TO .ratings-button > .ratings-chart { bottom: -1em; }\
		.notification_panel .ratings-chart table { border-collapse: collapse; }\
		.notification_panel .ratings-chart td { font-family: 'Oxygen',verdana,sans-serif; font-size: 70%; color: #444; padding: 0 2em 0 0; cursor: default; vertical-align: center }\
		.notification_panel .ratings-chart td:nth-child(3) { font-family: 'Droid Sans Mono',fixed-width; font-size: 60%; white-space: nowrap }\
		.notification_panel .ratings-chart p { font-size: 80%; padding: 0 2em 0 0; margin: 0.5em 0 0; white-space: nowrap }\
		.notification_panel .ratings-chart .light { opacity: 0.6 }\
		.notification_panel .ratings.no-TO .ratings-chart { padding: 0.5em }\
		.notification_panel .ratings-chart .rating { padding: 0.3em 0 0 }\
		.notification_panel .ratings-chart .rating > div { background-color: #eee; height: 0.5em; width: 13em; margin: 0 0.5em 0 0; border-radius: 4px }\
		.notification_panel .ratings-chart .rating > div > div { background-color: #55B8EA; height: 100%; border-radius: 4px }\
		");
}
NotificationPanel.prototype.getDOMElement = function() {
	return this.DOMElement;
}
NotificationPanel.prototype.addToPanel = function(notification) {
	$(this.getDOMElement()).prepend(notification.getDOMElement());
}
NotificationPanel.prototype.removeFromPanel = function(notification) {
	$(notification.getDOMElement()).remove();
}
NotificationPanel.prototype.onTimeoutListener = function(notification) {
	if (this.notifications.length > 1) {
		var _this = this;
		if (document.hasFocus() && settings.animation) {
			notification.fadeOut();
			setTimeout(function() { _this.remove(notification) }, 905);
		} else {
			setTimeout(function() { _this.remove(notification) }, 705);
		}
	} else {
		this.hide();
	}
}


/** The NotificationGroup object. This holds groups of Notifications and interacts
	directly with the NotificationPanel
**/
function NotificationGroup(obj) { // title, hits, isSticky, watcher, url
	this.title       = obj.title || null;
	this.hits        = obj.hits;
	this.isSticky    = (typeof obj.isSticky !== 'undefined') ? obj.isSticky : this.hasAutoAccept();
	this.url         = obj.url;
	this.timeout     = (this.isSticky) ? 15000 : 6000;
	this.hasTimedOut = false;
	this.isHovered   = false;

	if (typeof obj.watcher !== 'undefined') this.watcher = obj.watcher;

	var _this = this;
	setTimeout(function() {
		if (typeof _this.onTimeout !== 'undefined' && _this.onTimeout !== null) {
			_this.hasTimedOut = true;

			if (!_this.isHovered)
				_this.onTimeout(_this);
		}
	}, this.timeout);

	if (typeof this.hits[0] === 'undefined')
		console.error("Error, no hits for notification", document.URL, obj);

	this.createDOMElement();
}
NotificationGroup.prototype.addTO = function(data) {
	if (data !== "{}") {
		var ratings = JSON.parse(data);
		var group = this.getDOMElement();

		var notifications = group.find(".notification"),
			singleRequester = group.find("h4 .requester");

		if (singleRequester.length > 0) {
			var id
			for (var i in ratings)
				id = i;

			this.appendRatings({ notification: singleRequester.parent(), id: id, ratings: ratings[id] });
		}

		for (var id in ratings) {
			currentNotification = notifications.filter(function() { return $(this).data("requesterID") === id });
			this.appendRatings({ notification: currentNotification, id: id, ratings: ratings[id] });
		}
	}
}
NotificationGroup.prototype.appendRatings = function(obj) {
	var notification = obj.notification,
		requesterID  = obj.id,
		ratings      = obj.ratings,
		attrs        = ratings.attrs,
		requesterEl  = notification.find(".requester");

	// Would be nice to have a chart-looking icon
	var element = $('<div class="ratings"><div class="ratings-button" style="float: left"><div class="ratings-chart"></div></div></div>');

	if (ratings) {
		var html = '\
				<table><tbody>\
					<tr><td>Communicativity</td><td class="rating">' + bar(attrs.comm) + '</td><td class="light">' + attrs.comm + ' / 5</td></tr>\
					<tr><td>Pay</td>            <td class="rating">' + bar(attrs.pay ) + '</td><td class="light">' + attrs.pay  + ' / 5</td></tr>\
					<tr><td>Fairness</td>       <td class="rating">' + bar(attrs.fair) + '</td><td class="light">' + attrs.fair + ' / 5</td></tr>\
					<tr><td>Quickness</td>      <td class="rating">' + bar(attrs.fast) + '</td><td class="light">' + attrs.fast + ' / 5</td></tr>\
				</tbody></table>';

		var count = ratings.reviews;

		html +=	'<p>Scores based on <a href="http://turkopticon.ucsd.edu/' + requesterID + '" target="_blank">' + count + ' review' + ((count !== 1) ? "s" : "") + '</a>';
		html += ' <span class="light">(' + ratings.tos_flags + ' TOS violation' + ((ratings.tos_flags !== 1) ? "s" : "") + ')</light> - <a href="http://turkopticon.ucsd.edu/report?requester[amzn_id]=' + requesterID + '&requester[amzn_name]=' + ratings.name +'" target="_blank">Add review</a></p>';

		element.find(".ratings-chart").append(html);
		element.find(".ratings-button").css('background-color', getHsl(avg(attrs) / 5 * 100));
	} else {
		var html = '<p>No ratings available.</p>';
		html += '<p>Be the first to <a href="http://turkopticon.ucsd.edu/report?requester[amzn_id]=' + requesterID + '&requester[amzn_name]=' + ratings.name +'" target="_blank">review this requester</a>.';

		element.find(".ratings-chart").append(html);
		element.addClass("no-TO");
	}

	function bar(rating) {
		var percent = rating / 5 * 100,
			color = getHsl(percent);

		return '<div><div style="width: ' + percent + '%; background-color: ' + color + '">&nbsp;</div></div>';
	}

	function getHsl(percent) {
		var hue = ((percent / 100 * 5) - 1) / 4  * 100; // Max hue = 100 (green)
		return 'hsl(' + hue + ', 78%, 50%)';
	}

	function avg(attrs) {
		var count = 0,
			sum   = 0,
			comm  = parseFloat(attrs.comm, 10),
			pay   = parseFloat(attrs.pay, 10),
			fast  = parseFloat(attrs.fast, 10),
			fair  = parseFloat(attrs.fair, 10);

		if (comm !== 0) { sum += comm; count ++; }
		if (pay  !== 0) { sum += pay;  count ++; }
		if (fast !== 0) { sum += fast; count ++; }
		if (fair !== 0) { sum += fair; count ++; }

		return sum / count;
	}

	requesterEl.before(element);
}
NotificationGroup.prototype.createDOMElement = function() {
	var _this = this,
		REQUESTER_PREFIX = "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId=",
		hit = this.hits[0],
		isSameReq = Hit.isSameRequester(this.hits),
		self = this;

	var div = $('<div>').addClass("notification_group")
		.append((this.title !== null) ? $('<h3><a href="' + this.url + URL_POSTFIX + '" target="_blank">' + this.title + '</a></h3>') : "")
		.append((isSameReq) ? $('<h4><a href="' + REQUESTER_PREFIX + hit.requesterID + URL_POSTFIX + '" target="_blank" class="requester">' + hit.requester + '</a></h4>') : "")
		.hover(
			function() { _this.isHovered = true },
			function() {
				_this.isHovered = false;

				if (_this.hasTimedOut && typeof _this.onTimeout === 'function')
					_this.onTimeout(_this);
			}
		);

	// Sort the notifications (ignored go to the bottom)
	if (pageType.DASHBOARD && pageType.MAIN)
		this.hits.sort(function(a, b) { return (IgnoreList.isIgnored(a.requester)) ? 1 : 0 });

	// Add the notifications
	for (var i = 0, len = this.hits.length; i < len; i++) {
		var notification = new NotificationHit(this.hits[i], isSameReq, (typeof this.watcher !== 'undefined') ? this.watcher : null);

		notification.onIgnore = function(requesterID) {
			// Remove all notifications within the group that match the requester ID
			var notifications = self.DOMElement.find(".notification");
			notifications.filter(function() { return $(this).data("requesterID") === requesterID }).addClass("ignored");
		};

		notification.onUnIgnore = function(requesterID) {
			// Remove all notifications within the group that match the requester ID
			var notifications = self.DOMElement.find(".notification");
			notifications.filter(function() { return $(this).data("requesterID") === requesterID }).removeClass("ignored");
		};

		$(div).append(notification.getDOMElement());
	}

	if (this.hits[0].isAutoAccept)
		div.addClass("autoaccept");

	this.DOMElement = div;

	// This is required to get the shrinking effect when notifications are removed
	setTimeout(function() { div.css('max-height', div.height()); }, 1000);
}
NotificationGroup.prototype.getDOMElement = function() {
	return this.DOMElement;
}
NotificationGroup.prototype.hasAutoAccept = function() {
	var hasAutoAccept = false;
	for (var i = 0, len = this.hits.length; i < len; i++)
		if (this.hits[i].isAutoAccept) hasAutoAccept = true;
	return hasAutoAccept;
}
NotificationGroup.prototype.fadeOut = function(duration) {
	this.getDOMElement().addClass("removed");
}

/** The Notification object. This holds the notification data for individual hits

**/
function NotificationHit(hit, isSameReq, watcher) {
	this.hit = hit;
	this.isSameReq = isSameReq;
	if (typeof watcher !== 'undefined') this.watcher = watcher;

	this.createDOMElement();
}
NotificationHit.prototype.createDOMElement = function() {
	var URL_PREFIX = "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId=";

	// Create notification
	var hit = this.hit;
	var notification = $('<div>').addClass("notification").append(
		'<a class="title" target="_blank" href="' + hit.getURL('preview') + '" title="' + hit.title + '">' + hit.title + '</a>',
		(!this.isSameReq) ? $('<a class="requester" href="' + URL_PREFIX + hit.requesterID + URL_POSTFIX + '" target="_blank">' + hit.requester + '</a> <a class="ignore">ignore</a>') : "",
		'<p>' + hit.reward + " - " + hit.available + " rem. - " + hit.time + '</p>\
		 <div class="links"></div>\
		 <div><a class="mute"></a></div>'
	).data("requesterID", hit.requesterID);

	// Add links
	if (typeof hit.id !== 'undefined' && hit.id !== "undefined" && hit.isQualified) {
		if (this.hit.isAutoAccept) {
			$(".links", notification).append('<a class="hit_link" href="' + hit.getURL('view') + '" target="_blank">VIEW</a>');
		} else {
			$(".links", notification).append('\
				<a class="hit_link" target="_blank" href="' + hit.getURL('preview') + '">PREVIEW</a>\
				<a class="hit_link" target="_blank" href="' + hit.getURL('accept') + '">ACCEPT</a>');
			$(".links", notification).append($('<a class="hit_link" href="#">WATCH</a>').click(event => {
				event.preventDefault();
				const watcherInfo = {
					id: hit.id,
					duration: 60000,
					type: "hit",
					name: hit.title,
				};

				if (pageType.MAIN) {
					dispatch.add(new Watcher(watcherInfo)).start();
				} else {
					sendMessage({
						header: 'add_watcher',
						content: watcherInfo,
						timestamp: true
					});
				}
			}));
		}
	} else {
		if (this.hit.isAutoAccept && PageType.NEW_SITE) { // This is copy/pasted. Will clean up when the old site goes away
			$(".links", notification).append('<a class="hit_link" href="' + hit.getURL('view') + '" target="_blank">VIEW</a>');
		}

		$(notification).addClass("not_qualified");
		$(".links", notification).append(
			(hit.canPreview) ? '<a class="hit_link" href="' + hit.getURL('preview') + '" target="_blank">PREVIEW</a>' : "",
			'<span class="extra_info">Not Qualified&nbsp;&nbsp;</span>');
	}

	if (IgnoreList.isIgnored(hit.requester))
		notification.addClass("ignored");


	var id = hit.id;
	var muteButton = $('a.mute', notification);

	$(muteButton).text((typeof IgnoreList !== 'undefined' && IgnoreList.isMuted(id)) ? "muted" : "mute");
	$(muteButton).click(function () {
		if (!pageType.DASHBOARD || (pageType.DASHBOARD && !pageType.MAIN)) {
			if ($(this).text() === "mute")
				sendMessage({ header: "mute_hit", content: { id: id }, timestamp: true });
			else
				sendMessage({ header: "unmute_hit", content: { id: id }, timestamp: true });
		} else {
			if (!IgnoreList.isMuted(id))
				IgnoreList.add(IgnoreList.HIT, id);
			else
				IgnoreList.remove(IgnoreList.HIT, id);
		}

		if ($(this).text() === "mute")
			$(this).text("muted");
		else
			$(this).text("mute");
	});

	var ignoreButton = $("a.ignore", notification);

	var self = this;
	ignoreButton.click(function() {
		if (!pageType.DASHBOARD || (pageType.DASHBOARD && !pageType.MAIN)) {
			sendMessage({ header: "ignore_requester", content: { id: hit.requester } });
		} else {
			if (!notification.hasClass("ignored"))
				IgnoreList.add(IgnoreList.REQUESTER, hit.requester);
			else
				IgnoreList.remove(IgnoreList.REQUESTER, hit.requester);
		}

		if (!notification.hasClass("ignored")) {
			if (self.onIgnore && typeof self.onIgnore === 'function')
				self.onIgnore(hit.requesterID);
		} else {
			if (self.onUnIgnore && typeof self.onUnIgnore === 'function')
				self.onUnIgnore(hit.requesterID);
		}
	});


	if (hit.isAutoAccept)
		notification.addClass("autoaccept");

	if  (typeof this.watcher !== 'undefined' && this.watcher !== null && this.watcher.isNewHit(hit))
		$(notification).addClass("new");

	$(notification).append(muteButton);

	this.DOMElement = notification;
}
NotificationHit.prototype.getDOMElement = function() {
	return this.DOMElement;
}

var Sound = function() {
	var soundSource = "data:audio/ogg;base64," + "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",
		soundSource2 = "http://rpg.hamsterrepublic.com/wiki-images/3/3e/Heal8-Bit.ogg",
		sound = $('<audio><source src="' + soundSource + '"><source src="' + soundSource2 + '"></audio>')[0];
	sound.volume = settings.volume / 100;

	// Sound from http://www.freesfx.co.uk (Multimedia System Alert 003)
	var altSoundSource = "data:audio/mp3;base64," + "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VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuOf/7WGT/j/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABDdVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuOf/7WGT/j/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABDdVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7WGT/j/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ==",
		altSoundSource2 = "http://s1download-universal-soundbank.com/mp3/sounds/13406.mp3",
		altSound = $('<audio><source src="' + altSoundSource + '"><source src="' + altSoundSource2 + '"></audio>')[0];
	altSound.volume = settings.volume / 100;

	function _alert(context) {
		if (settings.sound) {
			if (context instanceof Watcher) {	// It's written this way in case we want sounds for other things in the future
				var option = context.option;
				if (!option.stopOnCatch && option.auto) {
					altSound.currentTime = 0;
					altSound.play();
				} else if (option.auto || option.alert) {
					sound.currentTime = 0;
					sound.play();
				}
			}
		}
	}

	function _setVolume(volume) {
		sound.volume = altSound.volume = volume / 100;
	}

	return {
		alert     : _alert,
		setVolume : _setVolume
	}
}();