Anime Lighter

Filter for anime trackers [planning for many]

// ==UserScript==
// @name         Anime Lighter
// @namespace    horc.net
// @version      3.7.1
// @description  Filter for anime trackers [planning for many]
// @author       RandomClown @ HoRC
// @copyright    © 2015
// @homepage     http://git.horc.net
// @icon         https://bitbucket.org/horc/anime-lighter/raw/master/img/Logo.png
// @grant        GM_xmlhttpRequest
// @grant        GM_log
// @require      http://code.jquery.com/jquery-2.1.3.min.js
// @require      https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
// @match        http://www.nyaa.se/*
// @match        http://horriblesubs.info
// ==/UserScript==

	var greasyfork = true;

// 
// 
//     Source code available: https://bitbucket.org/horc/anime-lighter/
// 
// 

/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />
/// <reference path="Class AnimeFilter.js" />
/// <reference path="Class LighterUI.js" />
/// <reference path="Class Run.js" />
/// <reference path="Class StoreUpdater.js" />
/// <reference path="Standard.js" />

var HORC_NAME = 'Anime Lighter Beta';



//    Wait for ready
new function () {
	var runonce = 0;

	function complete() {
		if (document.readyState === 'complete') {
			document.removeEventListener('DOMContentLoaded', complete, false);
			window.removeEventListener('load', complete, false);
			if (runonce++) return;
			setTimeout(ready);
		} else {
			document.addEventListener('DOMContentLoaded', complete, false);
			window.addEventListener('load', complete, false);
		}
	}
	complete();
}

//    Now ready
function ready() {
	new StoreUpdater('horc-animes', '1.0'); // <-- refrain from updating this one
	new StoreUpdater('horc-position', '1.1');
	new StoreUpdater('horc-style', '1.0');
	if (document.horc_updated) {
		location.reload(true);
		return;
	}















	run_animelighter();
}

//    Run main module
function run_animelighter() {
	//	Load site mod
	var mod = document.mod;

	delete document.readymods;
	console.log('	Host: ' + window.location.host);

	mod.createpositions();

	new MainUI(mod.style);

	new AnimeFilter(mod.filteroptions);

	mod.run();
}
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />


//    Get Anime - Generic
//    
//        Parse an episode text for the anime name
//        Works for most things
function default_getanime(jqe) {

	var str = jqe.text();

	return smartget(str);
}


////////////////////////////////////////
////    Useful Functions

function smartget(str) {


	//	Make it sane
	var str = trimws(str.replace(/_/g, ' ').replace(/\s\s+/g, ' '));

	if (!str.length) throw 'Expected a string';

	//	Get rid of leading subber
	if (-1 < str.charAt(0).search(/[\[\(\{]/)) str = trimws(consumetag(str));

	//	Get rid of next tag & beyond
	str = trimws(str.substr(0, str.search(/[\[\(\{]/)));

	//	Get rid of episode number[s] of format
	str = trimws(consumenumber(str));

	if (!str.length) throw 'Couldn\'t get the name';

	return str;
}

//    Consume Tag
//    
//        Gets rid of 1 tag: [subber], [1080p], [etc]
function consumetag(str) {


	var mode = false, done = false;
	var count = 0;
	var s = 0, e = 0;

	for (var i = 0; i < str.length; ++i) {
		if (done) break;
		switch (str.charAt(i)) {
			case '[':
			case '(':
			case '{':
				mode = true;
				count++;
				break;
			case ']':
			case ')':
			case '}':
				count--;
				if (mode && !count) done = true;
				break;
			default:
				break;
		}
		if (mode) e++;
		else s++, e++;
	}

	return str.substr(0, s) + str.substr(e);
}

//    Consume Number
//    
//        Gets rid of the episode number
function consumenumber(str) {


	//	Consumes numbers with a tack before
	function cnum_tack(str) {
		for (var s = 0; s < str.length ; ++s) {
			var hitnum = false;

			if (str.charAt(s) === '-' || str.charAt(s) === '‒') {
				var n = s + 1;

				//	Consume ws
				for (; n < str.length; ++n) if (!isws(str.charAt(n))) break;

				//	Consume number
				for (; n < str.length; ++n) {
					if (-1 < str.charAt(n).search(/\d/)) hitnum = true;
					break;
				}

				if (hitnum) {
					return str.substr(0, s);
				}
			}
		}
		return str;
	}

	//	Consumes numbers without a tack before
	function cnum_none(str) {
		var digits = 0;
		var i = 0;
		for (i = str.length - 1; 0 <= i; --i) {
			if (-1 < str.charAt(i).search(/\d/)) digits++;
			if (str.charAt(i).search(/\d/) === -1) break;
		}

		if (i === 0) return str; // reached the end

		if (1 < digits && isws(str.charAt(i))) return str.substr(0, i); // lone number & at least 2 digits

		return str; // some mixed number like: S2
	}

	str = cnum_tack(str);
	str = cnum_none(str);

	return str;
}

//    Trim Whitespace
//    
//        Gets rid of beginning & ending whitespace
function trimws(str) {


	var mode = false, done = false;
	var count = 0;
	var s = 0, e = 0;

	for (s = 0; s < str.length; ++s) {
		if (str.charAt(s) === ' ') continue;
		if (str.charAt(s) === '\t') continue;
		if (str.charAt(s) === '\r') continue;
		if (str.charAt(s) === '\n') continue;
		break;
	}
	for (e = str.length - 1; 0 <= e; --e) {
		if (str.charAt(e) === ' ') continue;
		if (str.charAt(e) === '\t') continue;
		if (str.charAt(e) === '\r') continue;
		if (str.charAt(e) === '\n') continue;
		break;
	}

	return str.substr(s, e - s + 1);
}

function isws(str) {
	for (var i = 0; i < str.length; ++i) if (!(str.charAt(i) === ' ' || str.charAt(i) === '\t' || str.charAt(i) === '\n' || str.charAt(i) === '\r' || str.charAt(i) === ' ')) return false;
	return true;
}
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />
/// <reference path="_ Host All.js" />


if ('horriblesubs.info' === window.location.host) {
	var options = {};

	options.style = "/* Configuration for horriblesubs.info */\n\n.horc-sidebar {\n\tfont-size: 0.75em;\n\twidth: 10em;\n}\n\n\t.horc-sidebar:hover {\n\t\twidth: 44em;\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\twidth: 40em;\n\t}\n\n.horc-ep-droppanel {\n\tfont-size: 1.5em;\n}\n\n\n\n/* Original episode list */\n\n.horc-episode-orig.watch {\n\tbackground-color: rgba(0, 255, 0, 0.25);\n}\n\n.horc-episode-orig.drop {\n\tcolor: rgba(0, 0, 0, 0.15);\n}\n\n.horc-episode-orig.hide {\n\tdisplay: none;\n\tvisibility: hidden;\n}\n\n.horc-episode-orig.dragging {\n\tfont-weight: bolder;\n}\n\n\n\n/* Main UI */\n\n#horc-statusbar {\n\tfont-size: 1.2em;\n\ttext-align: center;\n\tbackground-color: rgba(200,255,200, 0.9);\n\twidth: 100%;\n\tposition: fixed;\n\tpadding: 1em 0 1em 0;\n\tmargin: 0;\n\tleft: 0;\n\tbottom: 0;\n\tborder-radius: 1em;\n}\n\n\t#horc-statusbar > div {\n\t\tmargin: 0 1em 0 1em;\n\t}\n\n.horc-slot {\n\ttext-align: center;\n\twidth: 100%;\n}\n\n#horc-mainui {\n\ttext-align: center;\n\tbackground-color: rgba(230, 230, 230, 0.96);\n\tuser-select: none;\n}\n\n\t#horc-mainui > div {\n\t\tmargin: 1em 0;\n\t\tcursor: initial;\n\t\tuser-select: initial;\n\t}\n\n\t#horc-mainui h2 {\n\t\tcolor: #000000;\n\t\tpadding: 0.2em;\n\t\tmargin: 0;\n\t}\n\n.horc-sidebar {\n\tposition: absolute;\n\tright: 0;\n\toverflow: hidden;\n\tz-index: 1;\n\ttransition: all 0.4s;\n\topacity: 0.25;\n\t-webkit-filter: blur(2px);\n\t-moz-filter: blur(2px);\n\tfilter: blur(2px);\n}\n\n\t.horc-sidebar:hover {\n\t\topacity: 1;\n\t\t-webkit-filter: blur(0px);\n\t\t-moz-filter: blur(0px);\n\t\tfilter: blur(0px);\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\tpadding: 2em;\n\t\tpadding-bottom: 0.5em;\n\t\tborder-radius: 2em;\n\t}\n\n\n\n/* Anime Filter */\n\n.horc-listhead {\n\tcursor: default;\n\tuser-select: none;\n}\n\n\t.horc-listhead.horc-listhead-hide {\n\t\tcolor: #888888 !important;\n\t}\n\n.horc-listcounter {\n\tfont-family: Arial;\n\tfont-weight: initial;\n\tmargin-right: -100%;\n\tfloat: left;\n}\n\n#horc-watchlist {\n\tbackground-color: rgba(0, 255, 0, 0.05);\n}\n\n\t#horc-watchlist > .horc-content {\n\t\tbackground-color: rgba(0, 255, 0, 0.1);\n\t}\n\n#horc-droplist {\n\tbackground-color: rgba(0, 0, 0, 0.05);\n}\n\n\t#horc-droplist > .horc-content {\n\t\tbackground-color: rgba(0, 0, 0, 0.1);\n\t}\n\n#horc-hidelist {\n\tbackground-color: rgba(255, 0, 0, 0.05);\n}\n\n\t#horc-hidelist > .horc-content {\n\t\tbackground-color: rgba(255, 0, 0, 0.1);\n\t}\n\n#horc-mainui .horc-episode-filter {\n}\n\n\t#horc-mainui .horc-episode-filter:not(:last-child) {\n\t\tborder-bottom: 0.2em solid rgba(255, 255, 255, 0.8);\n\t}\n\n\n\n/* Position Drag UI */\n\n.horc-posdrag-icon {\n\twidth: 4em;\n\theight: 6em;\n\tmargin-left: -1em;\n\tmargin-top: -1em;\n\tbackground-color: rgb(230, 230, 230);\n\tborder: 0.25em solid #ffffff;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-slot .horc-ui-droppanel {\n}\n\n\t.horc-slot .horc-ui-droppanel .horc-circle {\n\t\tposition: absolute;\n\t\tdisplay: inline-block;\n\t\tz-index: 8;\n\t\twidth: 8em;\n\t\theight: 8em;\n\t\tmargin: -4em;\n\t\tpadding: 0;\n\t\tbackground: radial-gradient( rgba(100,100,255, 1.0), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8) );\n\t\tborder-radius: 50%;\n\t}\n\n\n\n/* Episode Drag UI */\n\n.horc-episode-icon {\n\tfont-family: Arial, Helvetica, sans-serif;\n\tcolor: #000000;\n\tfont-size: 1.2em;\n\tfont-weight: bold;\n\ttext-align: center;\n\tmax-width: 24em;\n\tmargin-left: -1em;\n\tmargin-top: -3em;\n\tpadding: 1em;\n\tbackground-color: rgb(230, 230, 255);\n\tborder: 0.25em solid #ffffff;\n\tborder-radius: 1em;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-ep-droppanel {\n\tposition: absolute;\n\tmargin: -8em;\n\twidth: calc(16em);\n\theight: calc(16em);\n\tbackground: radial-gradient( rgba(200,200,255, 0.8), rgba(200,200,255, 0.4), rgba(200,200,255, 0.4), rgba(200,200,255, 0.8) );\n\tborder-radius: 50%;\n\tz-index: 3;\n}\n\n\t.horc-ep-droppanel .horc-circle {\n\t\tcolor: #000000;\n\t\ttext-align: center;\n\t\tpointer-events: initial;\n\t\tposition: absolute;\n\t\tdisplay: table;\n\t\tmargin: calc(4.5em * -0.5);\n\t\twidth: calc(4.5em);\n\t\theight: calc(4.5em);\n\t\tborder-radius: 50%;\n\t}\n\n\t\t.horc-ep-droppanel .horc-circle div {\n\t\t\tdisplay: table-cell;\n\t\t\tvertical-align: middle;\n\t\t\tuser-select: none;\n\t\t}\n\n#watchcircle {\n\tleft: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,255,120, 0.75);\n}\n\n\t#watchcircle:hover {\n\t\tbackground-color: rgba(140,255,140, 1.0);\n\t}\n\n#dropcircle {\n\tright: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,140,120, 0.75);\n}\n\n\t#dropcircle:hover {\n\t\tbackground-color: rgba(140,140,140, 1.0);\n\t}\n\n#hidecircle {\n\tleft: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,140,120, 0.75);\n}\n\n\t#hidecircle:hover {\n\t\tbackground-color: rgba(255,140,140, 1.0);\n\t}\n\n#clearcircle {\n\tright: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,255,255, 0.75);\n}\n\n\t#clearcircle:hover {\n\t\tbackground-color: rgba(255,255,255, 1.0);\n\t}\n\n\n\n/* Both Drag UI */\n\n.openhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/openhand.cur) 8 4, move;\n}\n\n.closedhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/closedhand.cur) 8 4, move;\n}\n\n.draggable a, .draggable button {\n\tcursor: default;\n}\n\n.noselect {\n\t-webkit-touch-callout: none;\n\t-webkit-user-select: none;\n\t-khtml-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n";

	options.createpositions = function () { //	Create possible UI positions
		MainUI.create_sidebar($('<div>').insertBefore($('h2:contains("Releases")')), undefined, ['position', 'absolute', 'right', '4em', ]);
		MainUI.create_embed($('<div>').insertBefore($('h2:contains("Releases")')));
		MainUI.create_embed($('<div>').insertAfter($('.episodecontainer')));
		MainUI.create_embed($('<div>').prependTo($('#sidebar')));
		MainUI.create_embed($('<div class="horc-default">').insertAfter($('#text-16')));
		MainUI.create_embed($('<div>').insertAfter($('#text-8')));
		MainUI.create_embed($('<div>').appendTo($('#sidebar')));
	};

	options.filteroptions = {
		//	What contains the episode listing?
		epcontainer: '.episodecontainer',

		//	How to get an episode element?
		epselector: '.episodecontainer .episode',

		//	How to get anime name from an episode element?
		getanimename: default_getanime,

		//	How to grep episode container for an anime?
		epsearch: function (animename) {
			return $('.episodecontainer .episode').filter(':contains("' + animename + '")');
		},
	};

	options.run = function () {
		//	Find every way to update content
		function refresh(e) {


			var lastcount = 0;
			var timeout = 8000;
			var _check = setInterval(function () {
				var episodes = $('.episodecontainer .episode:not(.draggable)');

				if (lastcount != episodes.length) {
					lastcount = episodes.length;
					document.refreshfilters();
					timeout = 1000;
					return;
				} else if (0 < timeout) {
					timeout -= 50;
					return;
				}

				clearInterval(_check);


			}, 50);
		}

		function refresh_enter(e) { if (e.which == 13) refresh(); }
		function refresh_click(e) { refresh(); }
		$('.searchbar').on('keyup', refresh_enter);
		$('.refreshbutton').on('keyup', refresh_enter);
		$('.refreshbutton').on('mouseup', refresh_click);
		$('.morebox').on('keyup', '.morebutton, .searchmorebutton', refresh_enter);
		$('.morebox').on('mouseup', '.morebutton, .searchmorebutton', refresh_click);
	};

	document.mod = options;
}
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />
/// <reference path="_ Host All.js" />


if ('www.nyaa.se' === window.location.host) {
	var options = {};

	options.style = "/* Configuration for www.nyaa.se */\n\n.horc-sidebar {\n\twidth: 100px;\n\ttop: 280px;\n}\n\n\t.horc-sidebar:hover {\n\t\twidth: 44em;\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\twidth: 40em;\n\t}\n\n#ddpanel {\n\tfont-size: 1.5em;\n}\n\n.horc-episode-orig:not(.watch):not(.drop):not(.hide) * {\n\tfont-weight: normal !important;\n}\n\n.horc-episode-orig.watch .tlistname {\n\tfont-weight: 800 !important;\n}\n\n.horc-episode-orig.drop {\n\topacity: 0.30;\n}\n\n\n\n/* Original episode list */\n\n.horc-episode-orig.watch {\n\tbackground-color: rgba(0, 255, 0, 0.25);\n}\n\n.horc-episode-orig.drop {\n\tcolor: rgba(0, 0, 0, 0.15);\n}\n\n.horc-episode-orig.hide {\n\tdisplay: none;\n\tvisibility: hidden;\n}\n\n.horc-episode-orig.dragging {\n\tfont-weight: bolder;\n}\n\n\n\n/* Main UI */\n\n#horc-statusbar {\n\tfont-size: 1.2em;\n\ttext-align: center;\n\tbackground-color: rgba(200,255,200, 0.9);\n\twidth: 100%;\n\tposition: fixed;\n\tpadding: 1em 0 1em 0;\n\tmargin: 0;\n\tleft: 0;\n\tbottom: 0;\n\tborder-radius: 1em;\n}\n\n\t#horc-statusbar > div {\n\t\tmargin: 0 1em 0 1em;\n\t}\n\n.horc-slot {\n\ttext-align: center;\n\twidth: 100%;\n}\n\n#horc-mainui {\n\ttext-align: center;\n\tbackground-color: rgba(230, 230, 230, 0.96);\n\tuser-select: none;\n}\n\n\t#horc-mainui > div {\n\t\tmargin: 1em 0;\n\t\tcursor: initial;\n\t\tuser-select: initial;\n\t}\n\n\t#horc-mainui h2 {\n\t\tcolor: #000000;\n\t\tpadding: 0.2em;\n\t\tmargin: 0;\n\t}\n\n.horc-sidebar {\n\tposition: absolute;\n\tright: 0;\n\toverflow: hidden;\n\tz-index: 1;\n\ttransition: all 0.4s;\n\topacity: 0.25;\n\t-webkit-filter: blur(2px);\n\t-moz-filter: blur(2px);\n\tfilter: blur(2px);\n}\n\n\t.horc-sidebar:hover {\n\t\topacity: 1;\n\t\t-webkit-filter: blur(0px);\n\t\t-moz-filter: blur(0px);\n\t\tfilter: blur(0px);\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\tpadding: 2em;\n\t\tpadding-bottom: 0.5em;\n\t\tborder-radius: 2em;\n\t}\n\n\n\n/* Anime Filter */\n\n.horc-listhead {\n\tcursor: default;\n\tuser-select: none;\n}\n\n\t.horc-listhead.horc-listhead-hide {\n\t\tcolor: #888888 !important;\n\t}\n\n.horc-listcounter {\n\tfont-family: Arial;\n\tfont-weight: initial;\n\tmargin-right: -100%;\n\tfloat: left;\n}\n\n#horc-watchlist {\n\tbackground-color: rgba(0, 255, 0, 0.05);\n}\n\n\t#horc-watchlist > .horc-content {\n\t\tbackground-color: rgba(0, 255, 0, 0.1);\n\t}\n\n#horc-droplist {\n\tbackground-color: rgba(0, 0, 0, 0.05);\n}\n\n\t#horc-droplist > .horc-content {\n\t\tbackground-color: rgba(0, 0, 0, 0.1);\n\t}\n\n#horc-hidelist {\n\tbackground-color: rgba(255, 0, 0, 0.05);\n}\n\n\t#horc-hidelist > .horc-content {\n\t\tbackground-color: rgba(255, 0, 0, 0.1);\n\t}\n\n#horc-mainui .horc-episode-filter {\n}\n\n\t#horc-mainui .horc-episode-filter:not(:last-child) {\n\t\tborder-bottom: 0.2em solid rgba(255, 255, 255, 0.8);\n\t}\n\n\n\n/* Position Drag UI */\n\n.horc-posdrag-icon {\n\twidth: 4em;\n\theight: 6em;\n\tmargin-left: -1em;\n\tmargin-top: -1em;\n\tbackground-color: rgb(230, 230, 230);\n\tborder: 0.25em solid #ffffff;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-slot .horc-ui-droppanel {\n}\n\n\t.horc-slot .horc-ui-droppanel .horc-circle {\n\t\tposition: absolute;\n\t\tdisplay: inline-block;\n\t\tz-index: 8;\n\t\twidth: 8em;\n\t\theight: 8em;\n\t\tmargin: -4em;\n\t\tpadding: 0;\n\t\tbackground: radial-gradient( rgba(100,100,255, 1.0), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8) );\n\t\tborder-radius: 50%;\n\t}\n\n\n\n/* Episode Drag UI */\n\n.horc-episode-icon {\n\tfont-family: Arial, Helvetica, sans-serif;\n\tcolor: #000000;\n\tfont-size: 1.2em;\n\tfont-weight: bold;\n\ttext-align: center;\n\tmax-width: 24em;\n\tmargin-left: -1em;\n\tmargin-top: -3em;\n\tpadding: 1em;\n\tbackground-color: rgb(230, 230, 255);\n\tborder: 0.25em solid #ffffff;\n\tborder-radius: 1em;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-ep-droppanel {\n\tposition: absolute;\n\tmargin: -8em;\n\twidth: calc(16em);\n\theight: calc(16em);\n\tbackground: radial-gradient( rgba(200,200,255, 0.8), rgba(200,200,255, 0.4), rgba(200,200,255, 0.4), rgba(200,200,255, 0.8) );\n\tborder-radius: 50%;\n\tz-index: 3;\n}\n\n\t.horc-ep-droppanel .horc-circle {\n\t\tcolor: #000000;\n\t\ttext-align: center;\n\t\tpointer-events: initial;\n\t\tposition: absolute;\n\t\tdisplay: table;\n\t\tmargin: calc(4.5em * -0.5);\n\t\twidth: calc(4.5em);\n\t\theight: calc(4.5em);\n\t\tborder-radius: 50%;\n\t}\n\n\t\t.horc-ep-droppanel .horc-circle div {\n\t\t\tdisplay: table-cell;\n\t\t\tvertical-align: middle;\n\t\t\tuser-select: none;\n\t\t}\n\n#watchcircle {\n\tleft: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,255,120, 0.75);\n}\n\n\t#watchcircle:hover {\n\t\tbackground-color: rgba(140,255,140, 1.0);\n\t}\n\n#dropcircle {\n\tright: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,140,120, 0.75);\n}\n\n\t#dropcircle:hover {\n\t\tbackground-color: rgba(140,140,140, 1.0);\n\t}\n\n#hidecircle {\n\tleft: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,140,120, 0.75);\n}\n\n\t#hidecircle:hover {\n\t\tbackground-color: rgba(255,140,140, 1.0);\n\t}\n\n#clearcircle {\n\tright: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,255,255, 0.75);\n}\n\n\t#clearcircle:hover {\n\t\tbackground-color: rgba(255,255,255, 1.0);\n\t}\n\n\n\n/* Both Drag UI */\n\n.openhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/openhand.cur) 8 4, move;\n}\n\n.closedhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/closedhand.cur) 8 4, move;\n}\n\n.draggable a, .draggable button {\n\tcursor: default;\n}\n\n.noselect {\n\t-webkit-touch-callout: none;\n\t-webkit-user-select: none;\n\t-khtml-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n";

	options.createpositions = function () { //	Create possible UI positions
		MainUI.create_sidebar($('<div>').insertBefore($('#main')), undefined, ['position', 'absolute', 'right', '4em', 'top', '20em']);
		MainUI.create_embed($('<div>').insertBefore($('#main .tlistsortorder')), ['width', '100%', 'text-align', 'center']);
		MainUI.create_embed($('<div class="horc-default">').insertAfter($('#main .torrentsubcatlist')), ['width', '100%', 'text-align', 'center']);
	};

	options.filteroptions = {
		//	What contains the episode listing?
		epcontainer: '.tlist',

		//	How to get an episode element?
		epselector: '.tlist .tlistrow',

		//	How to get anime name from an episode element?
		getanimename: default_getanime,

		//	How to grep episode container for an anime?
		epsearch: function (animename) {
			return $('.tlist .tlistrow').has('.tlistname:contains("' + animename + '")');
		},
	};

	options.run = function () {
	};

	document.mod = options;
}
//    String Compare Case-Sensitive
function strcmp(lhs, rhs) {
	for (var i = 0; i < lhs.length && i < rhs.length; ++i) {
		if (lhs[i] === rhs[i]) continue;
		return lhs[i] < rhs[i] ? -1 : 1;
	}
	if (lhs.length === rhs.length) return 0;
	return lhs.length < rhs.length ? -1 : 1;
}

//    String Compare Case-Insensitive
function strcmpi(lhs, rhs) {
	for (var i = 0; i < lhs.length && i < rhs.length; ++i) {
		if (lhs[i].toLowerCase() === rhs[i].toLowerCase()) continue;
		return lhs[i].toLowerCase() < rhs[i].toLowerCase() ? -1 : 1;
	}
	if (lhs.length === rhs.length) return 0;
	return lhs.length < rhs.length ? -1 : 1;
}


//    Binary Search Template
//    
//        In the options, method_options below, you can override the methods:
//            
//            int compare(element_to_find, element_in_container)
//                Expected return: -1, 0, 1
//                Determines if the element is less or greater than
//                Behavior should match this: http://www.cplusplus.com/reference/cstring/strcmp/
//            
//            int length(container)
//                Expected return: int
//                Redefines how to check the size of the container [default container.length]
//            
//            ElementType get(i, container)
//                Expected return: a type matching the parameters of compare(e0, e1)
//                Redefines how to access the container by some index
//            
//            * found(i, container)
//                Expected return: anything you require
//                Redefines what to return
//            
//            * notfound(i, container)
//                Expected return: anything you require
//                Redefines what to return when not found [default null]
//                This function is useful to ask "where to insert", since
//                    "i" represent the position to insert the new element, using splice:
//                    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
//    
//    Usage [2]: int, null binsearch(string_to_find, array_of_strings)
//    Usage [3]: custom binsearch(element_to_find, container_object, method_options)
function binsearch(element, container, options) {
	if (options === undefined) options = {
		compare: undefined,
		length: undefined,
		get: undefined,
		found: undefined,
		notfound: undefined,
	};

	var compare = options.compare;
	var length = options.length;
	var get = options.get;
	var found = options.found;
	var notfound = options.notfound;

	if (compare === undefined) compare = strcmpi;


	function default_len(container) { return container.length; }
	if (length === undefined) length = default_len;


	function default_arrget(i, container) { return container[i]; }
	if (get === undefined) get = default_arrget;


	function default_arrret(i, container) { return i; }
	if (found === undefined) found = default_arrret;


	function default_arrnotret(i, container) { return null; }
	if (notfound === undefined) notfound = default_arrnotret;



	if (!length(container)) return notfound(0, container);




	var begin = 0;
	var end = length(container) - 1;
	var mid = begin + (end - begin) / 2 | 0;
	while (true) {



		if (begin === end) {
			switch (compare(element, get(mid, container))) {
				case -1:
					return notfound(mid, container);
				case 0:
					return found(mid, container);
				case 1:
					return notfound(mid + 1, container);
			}
		}

		switch (compare(element, get(mid, container))) {
			case -1:
				end = mid;
				mid = begin + (end - begin) / 2 | 0;
				break;
			case 0:
				return found(mid, container);
			case 1:
				begin = mid + 1;
				mid = begin + (end - begin) / 2 | 0;
				break;
		}
	}
}
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />
/// <reference path="Algorithms.js" />
/// <reference path="Class Store.js" />


var FLAG_WATCH = 0;
var FLAG_DROP = 1;
var FLAG_HIDE = 2;
var ANIME_NAME = 0;
var ANIME_FLAG = 1;
var ANIME_DATE = 2;
var ANIME_EXPIRE = 3;


//    Anime Structure
//    
//    Holds information on an anime
//    
//    Member Data:
//        - name     | Name of the anime according to the tracker
//        - flag     | View flag
//        - date_y   | Date added
//        - date_m   |  ^
//        - date_d   |  ^
//        - expire_y | Date this will expire on [-1 for never]
//        - expire_m |  ^
//        - expire_d |  ^
//    
//    Member Functions:
//        - isgood         | Test if expired
//        - setexpirecours | Set the expiration date ## "anime network season" from today
//        - setexpireweeks | Set the expiration date ## weeks from today
//        - setexpiredays  | Set the expiration date ## days from today
//        - compress       | Compress the data for storage
//        - decompress     | Copies data from a compressed data array
//    
//    Usage [0]: new Anime()
//    Usage [1]: new Anime(anime_dataset)
var Anime = function (anime_dataset) {
	var thisanime = this;

	this.Anime = function (anime_dataset) {


		if (typeof anime_dataset === 'object') {
			thisanime.decompress(anime_dataset);
		} else {
			var d = new Date();
			thisanime.date_y = d.getFullYear();
			thisanime.date_m = d.getMonth() + 1;
			thisanime.date_d = d.getDate();
			delete d;
		}
	};

	this.isgood = function () {
		if (thisanime.expire_y === -1) return true;

		var diff = (new Date(thisanime.expire_y, thisanime.expire_m - 1, thisanime.expire_d - 1)) - (new Date(thisanime.date_y, thisanime.date_m - 1, thisanime.date_d));
		if (-1 < diff) return true;

		return false;
	};

	this.setexpiredays = function (amount) {
		var d = new Date();
		var e = new Date(d.getFullYear(), d.getMonth(), d.getDate() + amount);
		thisanime.expire_y = e.getFullYear();
		thisanime.expire_m = e.getMonth() + 1;
		thisanime.expire_d = e.getDate();
	};
	this.setexpireweeks = function (amount) {
		thisanime.setexpiredays(amount * 7);
	};
	this.setexpirecours = function (amount) {
		thisanime.setexpiredays(amount * 7 * 13);
	};


	this.compress = function () {
		var anime = [];

		anime[ANIME_NAME] = thisanime.name; // Name of anime
		anime[ANIME_FLAG] = thisanime.flag; // w d h

		anime[ANIME_DATE] = thisanime.date_y;
		anime[ANIME_DATE] = anime[ANIME_DATE] * 100 + thisanime.date_m;
		anime[ANIME_DATE] = anime[ANIME_DATE] * 100 + thisanime.date_d;

		if (thisanime.expire_y === -1) {
			anime[ANIME_EXPIRE] = -1;
		} else {
			anime[ANIME_EXPIRE] = thisanime.expire_y;
			anime[ANIME_EXPIRE] = anime[ANIME_EXPIRE] * 100 + thisanime.expire_m;
			anime[ANIME_EXPIRE] = anime[ANIME_EXPIRE] * 100 + thisanime.expire_d;
		}

		return anime;
	};

	this.decompress = function (anime_dataset) {
		thisanime.name = anime_dataset[ANIME_NAME]; // Name of anime_dataset
		thisanime.flag = anime_dataset[ANIME_FLAG]; // w d h

		var value = anime_dataset[ANIME_DATE];
		thisanime.date_d = Math.floor(value % 100);
		value /= 100;
		thisanime.date_m = Math.floor(value % 100);
		value /= 100;
		thisanime.date_y = Math.floor(value);

		var value = anime_dataset[ANIME_EXPIRE];
		if (value === -1) {
			thisanime.expire_y = thisanime.expire_m = thisanime.expire_d = -1;
		} else {
			thisanime.expire_d = Math.floor(value % 100);
			value /= 100;
			thisanime.expire_m = Math.floor(value % 100);
			value /= 100;
			thisanime.expire_y = Math.floor(value);
		}

		return thisanime;
	};


	this.name = ''; // Name of anime
	this.flag = -1; // w d h
	this.date_y = -1;
	this.date_m = -1;
	this.date_d = -1;
	this.expire_y = -1;
	this.expire_m = -1;
	this.expire_d = -1;


	this.Anime(anime_dataset);
};



//    Anime List Manager
//    
//    Holds information on an anime
//    
//    Public Methods
//        - get         | Get an anime entry; If not found, return the insertion index
//        - add         | Add an anime by Anime struct
//        - rm          | Set the expiration date ## weeks from today
//        - save        | Save data to storage
//        - load        | Reload data from storage
//    
//    Usage [0]: AnimeList()
var AnimeList = function () {
	var thisanimelist = this;

	var store = new function () {
		this.anime = new Store('horc-animes', [], 'object');
	};
	store.anime.list = [];


	this.AnimeList = function () {



		thisanimelist.load();
	};



	//    Add Anime
	//    
	//    Inserts the anime to the list & storage
	//    
	//    Usage [1]: add(Anime_structure)
	this.add = function (anime) {




		var find = thisanimelist.get(anime.name);
		if (typeof find === 'number') { // Not found
			store.anime.list.splice(find, 0, anime); // insert
		} else { // Found
			store.anime.list.splice(find[0], 1, anime); // replace
		}

		thisanimelist.save();
	};

	//    Remove Anime
	//    
	//    Remove the anime from the list & storage
	//    
	//    Usage [1]: rm(string_name)
	this.rm = function (name) {


		var find = thisanimelist.get(name);
		if (typeof find === 'number') { // Not found
		} else { // Found
			store.anime.list.splice(find[0], 1); // replace
		}

		thisanimelist.save();
	};

	//    Get Anime
	//    
	//    Return:
	//         If found, the array [ index, Anime structure ]
	//         If not found, the index at which to insert the anime
	//    
	//    Usage [1]: get(string_name)
	this.get = function (name) {


		return binsearch(name, store.anime.list, {
			get: function (i, container) { return container[i].name; },
			found: function (i, container) { return [i, container[i]]; },
			notfound: function (i, container) { return i; },
		});
	};

	//    Save Anime List
	//    
	//    Save the anime list to storage
	//    
	//    Usage [0]: save()
	this.save = function () {
		var compressed = $(store.anime.list).map(function (i, e) {
			return [e.compress()];
		});

		store.anime.set(compressed);
	};

	//    Load Anime List
	//    
	//    Reload the anime list from storage
	//    
	//    Note:
	//        This will also delete expired entries from storage.
	//    
	//    Usage [0]: load()
	this.load = function () {
		var deletions = false;

		store.anime.list = $(store.anime.get()).map(function (i, e) {
			var anime = new Anime(e);
			if (anime.isgood()) return anime;
			deletions = true;




		});

		if (deletions) thisanimelist.save();
	};



	this.AnimeList();
};
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />
/// <reference path="Class Anime.js" />
/// <reference path="Class Store.js" />
/// <reference path="Class Touch.js" />


//    AnimeFilter module
//    
//    Constructor takes the string selector for slots the UI may position itself in.
//    Events are listened to & fired in $(document)
//    
//    Events this will listen for:
//        - set-active         | Make the program interactable
//        - clear-active       | Make the program non-interactable
//        - set-ui-droppanel   | Open the MainUI position drop panel
//        - clear-ui-droppanel | Close the MainUI position drop panel
//        - set-ep-droppanel   | Open the episode list drop panel
//        - clear-ep-droppanel | Close the episode list drop panel
//        - update-animelist   | Update the anime list count
//    
//    Events this will trigger:
//        - 
//    
//    Options:
//        - 
//    
//    Note:
//        You are required to allocate at least 1 position as empty elements before constructing this.
var AnimeFilter = function (options) {
	var thisanimefilter = this;

	var animelist = null;


	this.AnimeFilter = function () {

		document.animelist = animelist = new AnimeList();








		//	Bind episodes now & add to document
		thisanimefilter.refreshfilters();
		document.refreshfilters = thisanimefilter.refreshfilters;

		firstbindtouch();
		$(document).on('update-animelist', updatelist);
		$(document).trigger('update-animelist');
	};



	function firstbindtouch() {
		//    Search an element, then it's parents for a selector
		function treehas(element, selector) {



			//	Check this
			var jqe = $(element).filter(selector);
			if (jqe.length) return $(element);

			//	Check parents
			var jqe = $(element).parents(selector);
			if (jqe.length) return jqe;

			return null;
		}
		function typeofelement(jqe) {
			if (jqe.hasClass('horc-episode-filter')) return 'fi'; // Episode Filter
			if (jqe.hasClass('horc-episode-orig')) return 'ep'; // Episode Original
			if (jqe.hasClass('horc-circle')) return 'ep'; // Episode Original
			if (jqe.hasClass('horc-slot')) return 'ui'; // UI
			if (jqe.hasClass('horc-ui')) return 'ui'; // UI
			return '';
		}
		function shouldreject(jqe) {
			return !!treehas(jqe, 'a,button,datalist,input,keygen,output,select,textarea');
		}

		var touch = new Touch();

		var container = $('body');
		container.on('mousedown', touch.mousedown);
		container.on('mousemove', touch.mousemove);
		container.on('mouseup', touch.mouseup);
		container.on('touchstart', touch.touchstart);
		container.on('touchmove', touch.touchmove);
		container.on('touchend', touch.touchend);

		touch.onstart = function (ids, changes, e) {



		};
		touch.onend = function (ids, changes, e) {



		};
		touch.ondragstart = function (ids, changes, e) {



			//	If requires control & pressing control don't match, cancel
			if (e.ctrlKey ^ document.store.requirectrl.get()) return;

			$(changes).map(function (i, changed) {
				if (id < -1) return; // ignore middle & right click
				var changed = e.originalEvent.changedTouches[i];
				var id = changed.identifier;

				if (shouldreject(changed.target)) return;

				//	Check if drag exists
				var jqedrag = treehas(changed.target, '.draggable');
				if (!jqedrag) return;

				//	Check type of drag & drop
				var typeofdrag = typeofelement(jqedrag);

				switch (typeofdrag) {
					case 'ep':
						var panel = $('.horc-ep-droppanel');
						if (!panel.is(':visible')) {
							panel.css('left', changed.pageX);
							panel.css('top', changed.pageY);
						}
						$(document).trigger('set-ep-droppanel')
						try {
							addepicon(id, options.getanimename(jqedrag));
						} catch (err) {
							setstatus('<br>Could\'t get the anime\'s name, please report this:<br>\n<span style="color: #800000;">' + jqedrag.text() + '</span>');
						}
						break;
					case 'fi':
						var panel = $('.horc-ep-droppanel');
						if (!panel.is(':visible')) {
							panel.css('left', changed.pageX);
							panel.css('top', changed.pageY);
						}
						$(document).trigger('set-ep-droppanel')
						addepicon(id, jqedrag.text());
						break;
					case 'ui':
						$(document).trigger('set-ui-droppanel')
						$('#horc-mainui').hide();
						adduiicon(id);
						break;


				}

				e.preventDefault();
			});
		};
		touch.ondragend = function (ids, changes, e) {



			$(changes).map(function (i, changed) {
				if (id < -1) return; // ignore middle & right click
				var changed = e.originalEvent.changedTouches[i];
				var id = changed.identifier;

				if (shouldreject(changed.target)) return;

				rmepicon(id);
				rmuiicon(id);

				//	Check if drag & drop both exists
				var jqedrag = treehas(changed.target, '.draggable');
				if (!jqedrag) return;
				var jqedrop = treehas(changed.targetnow, '.droppable');
				if (!jqedrop) $('#horc-mainui').show();
				if (!jqedrop) return;

				//	Check type of drag & drop
				var typeofdrag = typeofelement(jqedrag);
				var typeofdrop = typeofelement(jqedrop);

				switch (typeofdrag) {
					case 'ep':
						switch (typeofdrop) {
							case 'ep':
								try {
									filteranime(options.getanimename(jqedrag), jqedrop);
								} catch (err) {
									setstatus('<br>Could\'t get the anime\'s name, please report this:<br>\n<span style="color: #800000;">' + jqedrag.text() + '</span>');
								}
								break;
							case 'ui':

								break;


						}
						break;
					case 'fi':
						switch (typeofdrop) {
							case 'ep':
								filteranime(jqedrag.text(), jqedrop);
								break;
							case 'ui':

								break;


						}
						break;
					case 'ui':
						switch (typeofdrop) {
							case 'ep':

								break;
							case 'ui':
								$('.horc-slot').map(function (i, element) {
									if (jqedrop[0] === element) document.store.position.set(i);
								});

								$('#horc-mainui').appendTo(jqedrop.children('.horc-content')).show();


								break;


						}
						break;


				}

				e.preventDefault();
			});
		};
		touch.onmove = function (ids, changes, e) {



			$(changes).map(function (i, changed) {
				if (id < -1) return; // ignore middle & right click
				var id = changed.identifier;

				var icon = $('#horc-ep' + id + ',#horc-pos' + id);
				if (icon.length) {
					icon.css('left', changed.clientX);
					icon.css('top', changed.clientY);
					e.preventDefault();
				}
			});
		};
		touch.onfirst = function (ids, changes, e) {




			if (e.ctrlKey ^ document.store.requirectrl.get()) return;

			$('#horc-mainui,' + options.epcontainer).addClass('noselect');
		};
		touch.onlast = function (ids, changes, e) {



			$(document).trigger('clear-ep-droppanel');
			$(document).trigger('clear-ui-droppanel');

			$('#horc-mainui,' + options.epcontainer).removeClass('noselect');
		};
	}

	function updatelist() {
		$('#horc-mainui .horc-episode-filter').remove();

		var eps = $('.horc-episode-orig');
		eps.map(function (i, element) {
			var ep = $(element);
			var animename = '';
			try {
				animename = options.getanimename(ep);
			} catch (err) {
				return;
			}

			//	Check which filter this episode falls under
			var flag = -1;
			if (ep.hasClass('watch')) flag = FLAG_WATCH;
			else if (ep.hasClass('drop')) flag = FLAG_DROP;
			else if (ep.hasClass('hide')) flag = FLAG_HIDE;
			if (flag == -1) return;

			//	Construct the list selector
			var whichselector = '';
			switch (flag) {
				case FLAG_WATCH:
					whichselector = 'watch';
					break;
				case FLAG_DROP:
					whichselector = 'drop';
					break;
				case FLAG_HIDE:
					whichselector = 'hide';
					break;


			}

			//	Populate the filter lists, since they were cleared before this map

			//	Check if the list has the anime
			var list = $('#horc-' + whichselector + 'list .horc-content');
			var inlist = !!list.find('.horc-episode-filter').map(function (i, element) {
				var filter = $(element);
				var filtername = filter.text();
				if (filtername === animename) return filtername;
			}).length;

			//	Add it maybe
			if (!inlist) {
				var filter = $('<div class="horc-episode-filter draggable">');
				filter.text(animename);
				list.append(filter);
			}
		});
	}


	//    Rebind Episodes
	//    
	//        Rebinds the drag event to new episode elements
	//    
	//    Note:
	//        You will need to call this anytime episode lists are populated dynamically.
	this.refreshfilters = function () {
		$(options.epselector).filter(':not(.horc-episode-orig)').map(function (i, element) {
			var jqe = $(element);
			jqe.addClass('horc-episode-orig draggable');

			var animename = '';
			try {
				animename = options.getanimename(jqe);
			} catch (err) {
				return;
			}


			var search = animelist.get(animename);
			if (typeof search === 'number') { // Not yet marked
				return; // Dont highlight
			}

			var flag = search[1].flag;

			var whichselector = '';
			switch (flag) {
				case FLAG_WATCH:
					whichselector = 'watch';

					break;
				case FLAG_DROP:
					whichselector = 'drop';

					break;
				case FLAG_HIDE:
					whichselector = 'hide';

					break;


			}

			jqe.removeClass('watch drop hide');
			jqe.addClass(whichselector);
		});

		$(document).trigger('update-animelist');
	};


	function filteranime(animename, jqedrop) {
		//	Check which filter this episode falls under
		var flag = 0;
		if (jqedrop.filter('#clearcircle').length) flag = -1;
		else if (jqedrop.filter('#watchcircle').length) flag = FLAG_WATCH;
		else if (jqedrop.filter('#dropcircle').length) flag = FLAG_DROP;
		else if (jqedrop.filter('#hidecircle').length) flag = FLAG_HIDE;

		//	Construct the list selector
		var whichselector = '';
		switch (flag) {
			case FLAG_WATCH:
				whichselector = 'watch';

				break;
			case FLAG_DROP:
				whichselector = 'drop';

				break;
			case FLAG_HIDE:
				whichselector = 'hide';

				break;
			case -1:

				break;


		}


		if (flag === -1) { // Clear
			//	Remove highlight
			var eps = options.epsearch(animename);

			eps.removeClass('watch drop hide');

			//	Remove from list
			animelist.rm(animename);

			$(document).trigger('update-animelist');
			return;
		}

		///////////////////////
		//  Watch/Drop/Hide  //

		//	Re-highlight
		var eps = options.epsearch(animename);
		eps.removeClass('watch drop hide');
		eps.addClass(whichselector);

		var search = animelist.get(animename);
		if (typeof search === 'number') { // Not yet added
			var newanime = new Anime();

			//	Update
			newanime.name = animename;
			newanime.flag = flag;
			newanime.setexpirecours(2);

			//	Save
			animelist.add(newanime);
			animelist.save();
		} else { // Already there; update it
			if (search[1].flag !== flag) { // update the flag
				//	Update
				search[1].flag = flag;

				animelist.save();
			} // Else do nothing
		}


		$(document).trigger('update-animelist');
	}


	function addepicon(id, name) {



		rmepicon(id);

		//	Drag icon
		var icon = $('<div id="horc-ep' + id + '" class="horc-episode-icon">' + name + '</div>');
		$('body').append(icon);
	}
	function adduiicon(id) {


		rmuiicon(id);

		//	Drag icon
		var icon = $('<div id="horc-pos' + id + '" class="horc-posdrag-icon">');
		$('body').append(icon);
	}
	function rmepicon(id) {


		$('#horc-ep' + id).remove();
	}
	function rmuiicon(id) {


		$('#horc-pos' + id).remove();
	}


	var timer_statusbar = null;
	function setstatus(msg) {
		var statusbar = $('#horc-statusbar');
		var msgbox = statusbar.children();

		if (timer_statusbar === null) {
			msgbox.empty().append(msg);

			statusbar.show({ direction: 'down' }, 250);
			timer_statusbar = setTimeout(function () {
				statusbar.hide({ direction: 'down' }, 250);
			}, 10000);
		} else {
			clearTimeout(timer_statusbar);
			timer_statusbar = null;
			setstatus(msg);
		}
	}



	this.AnimeFilter();
};
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />
/// <reference path="Class Store.js" />


//    MainUI module
//    
//    Constructor takes the string selector for slots the UI may position itself in.
//    Events are listened to & fired in $(document)
//    
//    Events this will listen for:
//        - set-active          | Make the program interactable
//        - clear-active        | Make the program non-interactable
//        - set-ui-droppanel    | Open the MainUI position drop panel
//        - clear-ui-droppanel  | Close the MainUI position drop panel
//        - set-ep-droppanel    | Open the episode list drop panel
//        - clear-ep-droppanel  | Close the episode list drop panel
//        - update-animelist    | Update the anime list count
//    
//    Static methods to know of:
//        - MainUI.create_embed    | Creates an embedded position
//        - MainUI.create_sidebar  | Creates a position for a sidebar
//    
//    Note:
//        You are required to allocate at least 1 position as empty elements before constructing this.
//        Pass the string selector to your allocated positions.
var MainUI = function (default_css) {
	var thismainui = this;


	if (document.store == undefined) document.store = {};
	document.store.css = new Store('horc-style', default_css, 'string');
	document.store.handedness = new Store('horc-handedness', 1, 'number');
	document.store.position = new Store('horc-position', -1, 'number');
	document.store.requirectrl = new Store('horc-requirectrl', false, 'boolean');
	document.store.settings = new Store('horc-settings', false, 'boolean');
	document.store.updater = new Store('horc-updater', true, 'boolean');
	document.store.showwatch = new Store('horc-show-watch', true, 'boolean');
	document.store.showdrop = new Store('horc-show-drop', false, 'boolean');
	document.store.showhide = new Store('horc-show-hide', false, 'boolean');


	this.MainUI = function () {


		var slots = $('.horc-slot');
		if (slots.length === 0) throw 'No positions for the UI to take';



		//	Load CSS
		$('<style id="horc-css">').text(document.store.css.get()).appendTo($('head'));

		create_mainui();
		create_statusbar();
		create_epdragui();

		create_events();
	};



	//    Get Position Value [+1 overloads]
	//    
	//    Return [0]:
	//        Returns the position of the current selection
	//        The range is from including  -1 to number_of_slots
	//    
	//    Return [1]:
	//        Returns the position value passed in, but corrected for out of bounds
	//        The range is from including  -1 to number_of_slots
	//    
	//    Note:
	//        Use if you need to calculate a new position that might be out of bounds.
	//    
	//    Usage [0]: getpositionvalue()
	//    Usage [1]: getpositionvalue(position_value_to_test)
	function getpositionvalue(pos) {


		var len = $('.horc-slot').length;

		if (pos === undefined) pos = document.store.position.get();

		if (pos < -1) pos = -1;
		if (len <= pos) pos = len - 1;

		return pos;
	};

	//    Get Currently Selected Position
	//    
	//    Return:
	//        Returns the elements that is chosen to hold the UI
	//    
	//    Usage [0]: getposition()
	function getposition() {
		var slots = $('.horc-slot > .horc-content');
		var pos = getpositionvalue();
		if (pos === -1) { // Find default slot
			var defaultslot = slots.parent('.horc-default').children('.horc-content');
			if (defaultslot.length) {
				return $(defaultslot[0]);
			} else {
				return $(slots[0]);
			}
		}
		return $(slots[pos]);
	};


	//    Create Main UI
	function create_mainui() {
		if (0 < $('#horc-mainui').length) throw 'Stopped UserScript from loading again'; // when UserScript loads twice, cancel load

		//	Create main ui area
		var mainui = $('<div id="horc-mainui" class="horc-ui draggable">');
		getposition().append(mainui);


		//	Add title & logo
		var title = $('<h1>' + HORC_NAME + '</h1>');
		var logo = $('<img>');
		title.prepend(logo);
		logo.css('width', '2em');
		logo.css('height', '2em');
		logo.css('margin-right', '0.5em');
		logo.css('vertical-align', 'middle');
		logo.on('load', function () {
			setTimeout(function () {
				title.slideUp(250, function () {
					title.remove();
				});
			}, 5000);
		});
		logo.prop('src', 'https://bitbucket.org/horc/anime-lighter/raw/master/img/Logo.png');
		mainui.append(title);


		//	Updater
		var updater = $('<div>');
		updater.hide().appendTo(mainui);
		var url = $('<span>[<a href="https://openuserjs.org/scripts/RandomClown/Anime_Lighter">OpenUserJS</a>]</span>');

		function nothing(newversion) { console.log('New version available: v' + newversion); }
		function versionchecker(newversion) {
			updater.slideDown(250);
			setTimeout(function () {
				updater.slideUp(250, function () {
					updater.remove();
				});
			}, 10000);
		}

		if (greasyfork) {
			updater.text('May be out-dated. ');
			updater.append(url);

			document.versionchecker = nothing;

			versionchecker('0.0');
		} else {
			updater.text('New version available! ');
			updater.append(url);

			document.versionchecker = document.store.updater.get() ? versionchecker : nothing;
		}


		//	Create individual filter lists
		var watchlist = $('<div id="horc-watchlist">');
		var droplist = $('<div id="horc-droplist">');
		var hidelist = $('<div id="horc-hidelist">');
		mainui.append(watchlist);
		mainui.append(droplist);
		mainui.append(hidelist);


		//	List headers
		var watchhead = $('<h2 class="horc-listhead">Watch List</h2>');
		var drophead = $('<h2 class="horc-listhead">Drop List</h2>');
		var hidehead = $('<h2 class="horc-listhead">Hide List</h2>');
		watchlist.append(watchhead);
		droplist.append(drophead);
		hidelist.append(hidehead);
		if (!document.store.showwatch.get()) watchhead.addClass('horc-listhead-hide');
		if (!document.store.showdrop.get()) drophead.addClass('horc-listhead-hide');
		if (!document.store.showhide.get()) hidehead.addClass('horc-listhead-hide');

		var watchcontent = $('<div class="horc-content">');
		var dropcontent = $('<div class="horc-content">');
		var hidecontent = $('<div class="horc-content">');
		watchlist.append(watchcontent);
		droplist.append(dropcontent);
		hidelist.append(hidecontent);
		if (!document.store.showwatch.get()) watchcontent.hide();
		if (!document.store.showdrop.get()) dropcontent.hide();
		if (!document.store.showhide.get()) hidecontent.hide();

		watchhead.click(function () {
			if ($(this).prop('disabled')) return;

			watchcontent.slideToggle(250)
			watchhead.toggleClass('horc-listhead-hide');
			document.store.showwatch.set(!document.store.showwatch.get());
		});
		drophead.click(function () {
			if ($(this).prop('disabled')) return;

			dropcontent.slideToggle(250)
			drophead.toggleClass('horc-listhead-hide');
			document.store.showdrop.set(!document.store.showdrop.get());
		});
		hidehead.click(function () {
			if ($(this).prop('disabled')) return;

			hidecontent.slideToggle(250)
			hidehead.toggleClass('horc-listhead-hide');
			document.store.showhide.set(!document.store.showhide.get());
		});


		var watchcount = $('<span class="horc-listcounter">0</span>')
		var dropcount = $('<span class="horc-listcounter">0</span>')
		var hidecount = $('<span class="horc-listcounter">0</span>')
		watchhead.append(watchcount);
		drophead.append(dropcount);
		hidehead.append(hidecount);
		var delay = null;
		$('#horc-mainui').on('anime-update', function () {
			if (!delay) delay = setTimeout(function () {
				delay = null;

				watchcount.text(watchlist.find('.episode').length);
				dropcount.text(droplist.find('.episode').length);
				hidecount.text(hidelist.find('.episode').length);
			}, 100);
		});

		create_settings();
	}

	//    Create Settings
	//    
	//        This will only show if the "settings" flag in localStorage is set
	function create_settings() {
		var mainui = $('#horc-mainui');

		//	Create Settings button
		var settingsbutton = $('<button>Settings</button>');
		settingsbutton.click(function () {
			var settings = document.store.settings.get();
			document.store.settings.set(!settings);

			$('#horc-settings').slideToggle(200);
		});
		mainui.append(settingsbutton);


		//	Create settings area
		var settingui = $('<div id="horc-settings">');
		mainui.append(settingui);
		settingui.css('text-align', 'center');
		if (!document.store.settings.get()) $('#horc-settings').hide();


		//	Create ctrl mode button
		var ctrlmod = $('<input id="horc-ctrlmod" type="checkbox">')
		settingui.append(ctrlmod);
		settingui.append('Require &lt;ctrl&gt; modifier');
		ctrlmod.prop('checked', document.store.requirectrl.get())
		ctrlmod.on('change', function () {
			document.store.requirectrl.set($(ctrlmod).prop('checked'));
		});


		//	Create CSS Update button
		var cssupdate = $('<button>Update CSS</button>');
		cssupdate.css('float', 'left');
		cssupdate.click(function () {


			var css = $('#horc-cssbox').val();
			var stringified = JSON.stringify(css);

			//	update current CSS style
			document.store.css.set(css);
			$('#horc-css').text(css);

			try {
				StyleFix.styleElement($('#horc-css')[0]);
			} catch (err) {
				// Prefix Free doesnt exist
			}

			//	update css dev box
			$('#stringify').val(stringified);
		});


		//	Create Clear Storage button
		var clearstorage = $('<button>Clear Storage</button>');
		clearstorage.css('float', 'right');
		clearstorage.css('background-color', 'rgba(255, 0, 0, 0.4)');
		clearstorage.click(function () {
			localStorage.clear();
			window.location.reload(true);
		});


		//	Create CSS box
		var cssbox = $('<textarea id="horc-cssbox">');
		cssbox.css('width', '100%');
		cssbox.css('height', '10em');
		cssbox.val(document.store.css.get());
		$(document).on('keydown', '#horc-cssbox', function (e) {
			//	Detect save
			if ((e.which == '115' || e.which == '83') && (e.ctrlKey || e.metaKey)) {
				e.preventDefault();
				cssupdate.trigger('click');
			}
		});


		//	Create Stringified box
		var stringbox = $('<textarea id="stringify">');
		stringbox.attr('disabled', 'true');
		stringbox.css('width', '100%');
		stringbox.css('height', '8em');
		stringbox.val(JSON.stringify(document.store.css.get()));


		//	Append CSS text editors
		settingui.append($('<br>'));
		settingui.append($('<br>'));

		settingui.append(clearstorage);
		settingui.append(cssupdate);
		settingui.append($('<div>Current CSS</div>'));
		settingui.append(cssbox);

		settingui.append($('<br>'));
		settingui.append($('<br>'));

		settingui.append($('<div>Stringified CSS</div>'));
		settingui.append(stringbox);
	}

	//    Create status bar
	function create_statusbar() {
		var statusbar = $('<div id="horc-statusbar"><div></div></div>').hide();
		$('body').append(statusbar);
	}

	//    Create Episode Drag & Drop UI
	function create_epdragui() {
		//	Find panel
		var ep_droppanel = $('<div class="horc-ep-droppanel">').hide();
		$('body').append(ep_droppanel);
		ep_droppanel.css('left', '10em');
		ep_droppanel.css('top', '30em');


		//	Create circles
		var watchcircle = $('<div id="watchcircle" class="horc-circle droppable"><div>Watch</div></div>');
		var dropcircle = $('<div id="dropcircle" class="horc-circle droppable"><div>Drop</div></div>');
		var hidecircle = $('<div id="hidecircle" class="horc-circle droppable"><div>Hide</div></div>');
		var clearcircle = $('<div id="clearcircle" class="horc-circle droppable"><div>Clear</div></div>');
		ep_droppanel.append(watchcircle);
		ep_droppanel.append(dropcircle);
		ep_droppanel.append(hidecircle);
		ep_droppanel.append(clearcircle);
	}


	//    Create Events
	function create_events() {
		//	Make the program interactable
		$(document).on('set-active', function (e) {
			$('#horc-mainui').find('.horc-listhead, .horc-filter-episode, button, input').prop('disabled', false);
		});
		$(document).on('clear-active', function (e) {
			$('#horc-mainui').find('.horc-listhead, .horc-filter-episode, button, input').prop('disabled', true)
		});

		//	Make the main UI disappear into a small dragged window
		//	Show the position drop panels
		$(document).on('set-ui-droppanel', function (e) {
			$('.horc-ui-droppanel').fadeIn(100);
			$('.horc-posdrag-icon').show();
			//$('.horc-ui-droppanel').find('.horc-circle')
		});
		$(document).on('clear-ui-droppanel', function (e) {
			$('.horc-ui-droppanel').fadeOut(100);
			$('.horc-posdrag-icon').hide();
		});

		//	Make a small box displaying an episode entry
		//	Show the anime drop panels
		$(document).on('set-ep-droppanel', function (e) {
			var panel = $('.horc-ep-droppanel');
			panel.fadeIn(100);
			$('.horc-episode-icon').show();

			//panel.css('left', document.prop.mousex + 'px');
			//panel.css('top', document.prop.mousey + 'px');
		});
		$(document).on('clear-ep-droppanel', function (e) {
			$('.horc-ep-droppanel').fadeOut(100);
			$('.horc-episode-icon').hide();
		});

		//	Update the counters on anime lists
		$(document).on('update-animelist', function (e) {
			//	Clear mainui anime list
			$('#horc-watchlist, #horc-droplist, #horc-hidelist').find('.horc-content').empty();

			//	Repopulate with the episode listing
			$('.horc-episode').map(function (i, element) {
				var jqe = $(element);

				console.warn('Repopulate Incomplete');
			});

			//	Count number of animes in list
			var eps = $('.horc-episode-orig');
			var watchcount = eps.filter('.watch').length;
			var dropcount = eps.filter('.drop').length;
			var hidecount = eps.filter('.hide').length;
			$('#horc-watchlist').find('.horc-listcounter').text(watchcount);
			$('#horc-droplist').find('.horc-listcounter').text(dropcount);
			$('#horc-hidelist').find('.horc-listcounter').text(hidecount);
		});
	}



	this.MainUI();
};

MainUI.create_embed = function (jqe, contentstyles, draguistyles) {
	jqe.empty().addClass('horc-slot droppable');


	var dragui = $('<div class="horc-ui-droppanel">').hide();
	dragui.append($('<div class="horc-circle">'));
	jqe.append(dragui);


	var content = $('<div class="horc-content horc-embed">');
	jqe.append(content);


	if (contentstyles) {
		for (var i = 0; i < contentstyles.length; i += 2) {
			content.css(contentstyles[i], contentstyles[i + 1]);;
		}
	}

	if (draguistyles) {
		for (var i = 0; i < draguistyles.length; i += 2) {
			dragui.css(draguistyles[i], draguistyles[i + 1]);
		}
	}
}

MainUI.create_sidebar = function (jqe, contentstyles, draguistyles) {
	jqe.empty().addClass('horc-slot droppable');


	var dragui = $('<div class="horc-ui-droppanel">').hide();
	dragui.append($('<div class="horc-circle">'));
	jqe.append(dragui);


	var content = $('<div class="horc-content horc-sidebar">');
	jqe.append(content);


	if (contentstyles) {
		for (var i = 0; i < contentstyles.length; i += 2) {
			content.css(contentstyles[i], contentstyles[i + 1]);;
		}
	}

	if (draguistyles) {
		for (var i = 0; i < draguistyles.length; i += 2) {
			dragui.css(draguistyles[i], draguistyles[i + 1]);
		}
	}
}
//    Storage Management
//    
//        This will create a new persistant variable
//    
//    Note:
//        Constructor[3] has type checking.
//    
//    Usage [2]: new Store(keyname, value_default)
//    Usage [3]: new Store(keyname, value_default, expected_type)
var Store = function (keyname, value_default, type) {
	var thisstore = this;

	this.Store = function (keyname, value_default, type) {





		var value = localStorage.getItem(thisstore._keyname);
		if (value === null) {
			thisstore._cache = JSON.parse(thisstore._default);
		} else {
			value = JSON.parse(value);

			if (type && typeof value !== type) {
				console.warn('Loaded data for "' + keyname + '" is not of type "' + type + '"; Using default value');

				thisstore._cache = JSON.parse(thisstore._default);
			} else {
				thisstore._cache = value;
			}
		}
	};



	this.get = function () {
		return thisstore._cache;
	};

	this.set = function (value) {
		if (type && typeof value !== type) {


			thisstore._cache = JSON.parse(thisstore._default);
		}

		thisstore._cache = value;
		localStorage.setItem(thisstore._keyname, JSON.stringify(value));
		if (thisstore.onset) thisstore.onset();
	};

	this.rm = function () {
		localStorage.removeItem(thisstore._keyname);
	};



	this._keyname = keyname;
	this._default = JSON.stringify(value_default);
	this._type = type;

	this._cache = null;

	this.onset = null;



	this.Store(keyname, value_default, type);
};
//    Store Updater
//    
//        This will force storage to clear for a specific key.
//    
//    Note:
//        You should force refresh the page, in case your other modules loaded 1st.
//    
//    Usage [2]: new StoreUpdater(keyname, new_version)
var StoreUpdater = function (keyname, newversion) {



	//	Initialize position for the UI
	if (!document.getElementById('horc-updater')) {
		var updater = document.createElement('div');
		document.body.insertBefore(updater, document.body.childNodes[0]);
		updater.id = 'horc-updater';
		updater.style.position = 'fixed';
		updater.style.left = '0';
		updater.style.top = '0';
		updater.style.zIndex = '100';
	}


	if (document.horc_updated === undefined) document.horc_updated = false;


	var moduleversion = localStorage.getItem(keyname + '.version');


	if (moduleversion !== newversion) {
		// Site theme changed since last time; Delete key & prepare to reboot

		document.horc_updated = true;

		localStorage.removeItem(keyname);
		localStorage.setItem(keyname + '.version', newversion);
		localStorage.removeItem(keyname + '.updated');

		return;
	}


	if (null === localStorage.getItem(keyname + '.updated')) {
		// Tell the user it updated

		localStorage.setItem(keyname + '.updated', 'true');

		var container = document.createElement('div');
		container.style.color = '#000000';
		container.style.backgroundColor = '#eeffee';
		container.style.padding = '.5em 2em';
		container.style.margin = '.5em';
		container.style.marginLeft = '1em';
		container.style.width = '16em';
		container.style.borderRadius = '0.5em';
		container.style.boxShadow = '0 0 1em #000000';

		var icon = document.createElement('img');
		icon.src = 'https://bitbucket.org/horc/anime-lighter/raw/master/img/Logo.png';
		icon.style.width = '1.2em'
		icon.style.verticalAlign = 'middle'

		var msg = ' <b>' + keyname + '</b> was reset';

		var counter = document.createElement('span');
		counter.style.cssFloat = 'right';

		var desc = document.createElement('span');
		desc.innerHTML = msg;
		desc.appendChild(counter);

		container.appendChild(icon);
		container.appendChild(desc);

		document.getElementById('horc-updater').appendChild(container);

		var time = 10000;
		var _updater = setInterval(function () {
			if (time <= 0) {
				clearInterval(_updater);
				container.remove();
			} else {
				var t = (time / 100 | 0) / 10;
				if (t % 1 === 0) t = t + '.0';
				counter.innerHTML = t;
				time -= 100;
			}
		}, 100);

		container.onclick = function () {
			clearInterval(_updater);
			container.remove();
		}
	}
}
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />


//    Mouse & Touch module
//    
//        Makes mouse & touch interaction consistent
//        This guarantees that for every "start" event, there is a matching "end" event
//        Assign the event handler to the correct functions of these
//    
//    If you need to test against left/middle/right click, negate the number before testing
//        Real touch events will use positive numbers: 0, 1, 2, +
//        Mouse events will use negative numbers, -1, -2, -3
//    
//    Important functions:
//        .mousedown  | Event proxies that you must pass to an object when binding
//        .mouseup    |  ^
//        .mousemove  |  ^
//        .touchstart |  ^
//        .touchend   |  ^
//        .touchmove  |  ^
//    
//    Important Variables:
//        .onstart     | Callback for mouse or touch start event
//        .onend       | Callback for matching end event
//        .ondragstart | Callback for when a drag is detected
//        .ondragend   | Callback for matching end event
//        .onmove      | Callback for matching move event
//        .onfirst     | Callback for 1st start event
//        .onlast      | Callback for last end event
//    
//    Constructor:
//        Constructor takes 3 functions, listed below
//        Make sure each of those are defined with parameters(identifiers, changedtouches, event)
//    
//    Usage[0]: new Touch()
var Touch = function () {
	var thistouch = this;



	var touchsupport = 'ontouchstart' in window;

	this.Touch = function () {

	};



	this.mousedown = function (e) {


		if (e.which < 1) return;
		if (thistouch.ismdown) return;
		thistouch.ismdown = true;
		var isfirst = !thistouch.dragging;

		thistouch.mousetouch = new VirtualTouchEvent(e, -e.which);
		thistouch.target = e.originalEvent.target;

		recheck_mouse(e);

		$(document).on('mouseup mousemove mouseenter mouseleave', mousemove_correction);

		var changes = e.originalEvent.changedTouches;

		//	Trigger first
		if (thistouch.onfirst && isfirst) thistouch.onfirst([-e.which], changes, e);

		//	Trigger mouse start

		if (thistouch.onstart) thistouch.onstart([-e.which], changes, e);

		e.done = true; // Notify mouse correction handler
	};
	this.mousemove = function (e) {


		if (e.which < 1) return;
		if (!thistouch.ismdown) return;

		recheck_mouse(e);

		var changes = e.originalEvent.changedTouches;

		//	Trigger dragging
		if (!isdragging(-e.which)) {
			setdragging(-e.which);
			if (thistouch.ondragstart) thistouch.ondragstart([-e.which], changes, e);
		}

		//	Trigger moving
		if (thistouch.onmove) thistouch.onmove([-e.which], changes, e);

		e.done = true; // Notify mouse correction handler
	};
	this.mouseup = function (e) {


		if (e.which < 1) return;
		if (!thistouch.ismdown) return;
		thistouch.ismdown = false;

		e.originalEvent.target = thistouch.target;

		recheck_mouse(e);

		var changes = e.originalEvent.changedTouches;

		//	Trigger dragging
		if (isdragging(-e.which)) {
			cleardragging(-e.which);
			if (thistouch.ondragend) thistouch.ondragend([-e.which], e.originalEvent.changedTouches, e);
		}

		//	Trigger mouse end
		if (thistouch.onend) thistouch.onend([-e.which], changes, e);


		//	Trigger last
		if (thistouch.onlast) thistouch.onlast([-e.which], changes, e);

		thistouch.mousetouch = null; // Delete last mouse

		e.done = true; // Notify mouse correction handler
	};


	this.touchstart = function (e) {


		normalize(e);

		var isfirst = !thistouch.istdown;
		thistouch.istdown = true;

		//	For all changes
		var changes = e.originalEvent.changedTouches;
		var ids = $(changes).map(function (i, changed) { return changed.identifier; });



		//	Trigger first
		if (isfirst && thistouch.onfirst) thistouch.onfirst(ids, changes, e);

		//	Trigger touch start
		if (thistouch.onstart) thistouch.onstart(ids, changes, e);

		e.done = true; // Notify mouse correction handler
	};
	this.touchmove = function (e) {


		try { // Unknown error possible where the user taps once extremely fast [takes a lot of tries to replicate]
			normalize(e);
		} catch (err) {
			return; // Just skip this move event
		}

		//	For all changes
		var changes = e.originalEvent.changedTouches;
		var ids = $(changes).map(function (i, changed) { return changed.identifier; });
		var changes_start = [];
		var ids_start = [];
		$(changes).map(function (i, changed) {
			var id = changed.identifier;
			if (!isdragging(id)) {
				setdragging(id);
				changes_start.push(changed);
				ids_start.push(id);
			}
		});

		//	Trigger dragging
		if (ids_start.length && thistouch.ondragstart) thistouch.ondragstart(ids_start, changes_start, e);

		//	Trigger moving
		if (thistouch.onmove) thistouch.onmove(ids, changes, e);

		e.done = true; // Notify mouse correction handler
	};
	this.touchend = function (e) {


		try { // Unknown error possible where the user taps once extremely fast [takes a lot of tries to replicate]
			normalize(e);
		} catch (err) {
			console.error('Error: Unknown issue where user performs 2 finger right click.\nThat breaks: normalize::importchanges::changes.push'); // Allow cleanup to happen, but alert developer
			console.warn('Attempting to recover from error')
		}

		//	For all changes
		var changes = e.originalEvent.changedTouches;
		var ids = $(changes).map(function (i, changed) { return changed.identifier; });
		var changes_end = [];
		var ids_end = [];
		$(changes).map(function (i, changed) {
			var id = changed.identifier;
			if (isdragging(id)) {
				cleardragging(id);
				changes_end.push(changed);
				ids_end.push(id);
			}
		});

		//	Trigger dragging
		if (ids_end.length && thistouch.ondragend) thistouch.ondragend(ids_end, changes_end, e);

		//	Trigger touch end
		if (thistouch.onend) thistouch.onend(ids, changes, e);


		//	Trigger last
		if (!e.originalEvent.touches.length) thistouch.istdown = false;
		var islast = !thistouch.istdown;
		if (thistouch.onlast && islast) thistouch.onlast(ids, changes, e);

		e.done = true; // Notify mouse correction handler
	};


	//    Unified handler for all
	this.onstart = null;
	this.onend = null;
	this.ondragstart = null;
	this.ondragend = null;
	this.onmove = null;
	this.onfirst = null;
	this.onlast = null;


	//    Check Dragging State
	//    
	//        Used to keep track of whats being dragged
	//    
	//    Usage [1]: setdragging(id)
	function isdragging(id) {

		var shift = (0x1 << (id + 3));
		return !!(thistouch.dragging & shift);
	}
	//    Set Dragging State
	//    
	//        Used to keep track of whats being dragged
	//    
	//    Usage [1]: setdragging(id)
	//    Usage [2]: setdragging(ids)
	function setdragging(ids) {

		if (ids.length === undefined) {
			var shift = (0x1 << (ids + 3));
			thistouch.dragging |= shift;
		} else {
			$(ids).map(function (i, id) { setdragging(id); });
		}
	}
	//    Clear Dragging State
	//    
	//        Used to keep track of whats being dragged
	//    
	//    Usage [1]: cleardragging(id)
	//    Usage [2]: cleardragging(ids)
	function cleardragging(ids) {

		if (ids.length === undefined) {
			var shift = (0x1 << (ids + 3));
			thistouch.dragging &= ~shift;
		} else {
			$(ids).map(function (i, id) { cleardragging(id); });
		}
	}


	//    Normalize
	//    
	//        Combines .which, .client, .changedTouches, .touches, & .targetnow
	//    
	//    Usage [1]: normalize(event)
	function normalize(e) {

		if (e.done) return;

		if (e.originalEvent.changedTouches === undefined) e.originalEvent.changedTouches = [];
		if (e.originalEvent.touches === undefined) e.originalEvent.touches = [];

		for (var i = 0; i < e.originalEvent.changedTouches.length; i++) {
			var changed = e.originalEvent.changedTouches[i];
			changed.target = thistouch.target;
			changed.targetnow = gettargetnow(e);
		}

		var changed = e.originalEvent.changedTouches;
		if (e.which && changed.length) {
			delete changed.target; // a bug requires original target to be deleted before reassignment
			changed.target = thistouch.target;
		}

		var changes = e.originalEvent.changedTouches;
		if (thistouch.mousetouch) changes.push(thistouch.mousetouch);

		var touches = e.originalEvent.touches;
		if (thistouch.mousetouch) touches.push(thistouch.mousetouch);
	}
	//    Recheck Mouse
	//    
	//        For mouse events, this ensures consistency between mouse & touch.
	//    
	//    Note:
	//        Only for mouse events; Don't call this from a touch event.
	//    
	//    Usage [1]: recheck_mouse(event)
	function recheck_mouse(e) {


		normalize(e);

		if (thistouch.mousetouch) {
			thistouch.mousetouch.copyfrom(e);
		}

		if (e.which !== thistouch.lastmouse) {
			if (thistouch.lastmouse) {
				var nowwhich = e.which;
				e.which = thistouch.lastmouse;
				thistouch.mouseup(e);
				e.which = nowwhich;
			}

			thistouch.lastmouse = e.which;
		}
	}
	//    Mouse Move Correction
	//    
	//        Bound when a mousedown happens
	//        This ensures that a mouse up will happen
	//    
	//    Note:
	//        Only for mouse events; Don't call this from a touch event.
	//    
	//    Usage [1]: recheck_mouse(event)
	function mousemove_correction(e) {
		if (e.done) return;
		recheck_mouse(e);
		if (!thistouch.mousetouch) {
			$(document).off('mouseup mousemove mouseenter mouseleave', mousemove_correction);
			thistouch.lastmouse = 0;
			thistouch.mouseup(e);
		} else {
			thistouch.mousemove(e);
		}
	}


	//    Get Cursor Position
	//    
	//    Note:
	//        Requires at least 1 touch or mouse point to exist.
	//    
	//    Usage [1]: getcursor(event)
	function getcursor(e) {


		var changed = e.originalEvent.changedTouches;
		if (changed && 0 < changed.length) {
			var touch = e.originalEvent.changedTouches[0];
			return [touch.clientX, touch.clientY];
		} else if (e.clientX !== undefined) {
			return [e.clientX, e.clientY];
		}


		return null;
	}
	//    Get Target Now
	//    
	//    Note:
	//        Requires at least 1 touch or mouse point to exist.
	//    
	//    Usage [1]: gettargetnow(event)
	function gettargetnow(e) {


		var pos = getcursor(e);
		if (pos) return document.elementFromPoint(pos[0], pos[1]);
		return null;
	}


	//    Virtual Touch Event
	//    
	//    Note:
	//        Constructor Takes the mouse event & which mouse.
	//        It has a copyfrom() method to copy new XY values.
	//        copyfrom() will not override the id or element target.
	var VirtualTouchEvent = function (e, id) {




		this.clientX = e.originalEvent.clientX;
		this.clientY = e.originalEvent.clientY;
		this.force = 0; // Pressure
		this.identifier = id; // ID of this touch event
		this.pageX = e.originalEvent.pageX;
		this.pageY = e.originalEvent.pageY;
		this.radiusX = 25; // Default to 25 for mouse
		this.radiusY = 25; //  ^
		this.screenX = e.originalEvent.screenX;
		this.screenY = e.originalEvent.screenY;
		this.target = e.originalEvent.target; // Element that triggered the event
		this.targetnow = gettargetnow(e); // Element the mouse is focused on

		this.copyfrom = function (e) {
			this.clientX = e.originalEvent.clientX;
			this.clientY = e.originalEvent.clientY;
			this.pageX = e.originalEvent.pageX;
			this.pageY = e.originalEvent.pageY;
			this.screenX = e.originalEvent.screenX;
			this.screenY = e.originalEvent.screenY;
			this.targetnow = gettargetnow(e); // Element the mouse is focused on
		};
	}

	this.mousetouch = null;
	this.dragging = 0x0; // dragging flag
	this.target = null;

	this.lastmouse = 0;

	this.ismdown = false;
	this.istdown = false;


	this.Touch();
};
///	Dependencies:
/// <reference path="http://code.jquery.com/jquery-2.1.3.js" />
/// <reference path="https://code.jquery.com/ui/1.11.4/jquery-ui.js" />


//    Get URL Search   +1 overloads
//    
//        This will read a URL for the search query
//    
//    Examples of accepted URLs:
//        http://horc.bitbucket.org/
//        http://horc.bitbucket.org?
//        http://horc.bitbucket.org/?path=projects
//        ?path=projects/test/
//        path=projects/test&
//    
//    Return:
//        Object with the key & string values
//    
//    Usage [0]: geturlsearch()
//    Usage [1]: geturlsearch(string_url)
function geturlsearch(url) {
	if (typeof url !== 'string') {
		if (url === undefined) url = location.search;

	}

	function cleanurlsearch(url) {
		console.warn('\t' + url);
		if (-1 < url.search(/:\/\//)) { // This is a url
			var p = url.search(/\?/);
			if (p === -1) url = '';
			else url = url.substr(p + 1);
		}

		if (url.substr(0, 1) === '?') url = url.substr(1);

		return url;
	}

	url = cleanurlsearch(url);

	var params = {};
	var split = url.split('&');
	for (var i = 0; i < split.length; i++) {
		var u = split[i];
		if (u.length) {
			var p = u.search('=');
			if (-1 < p) {
				var lhs = u.substr(0, p);
				var rhs = u.substr(p + 1);
				if (!lhs.length) continue;
				params[lhs] = rhs;
			} else {
				var lhs = u;
				params[lhs] = '';
			}
		}
	}
	return params;
}


//    Load Script   +3 overloads
//    
//        This will sequentially load 1 script at a time.
//        Run multiple times to load in parallel
//    
//    Usage [1]: loadscript(string_url)
//    Usage [2]: loadscript(string_url, oncomplete_function)
//    Usage [1]: loadscript(array_of_urls)
//    Usage [2]: loadscript(array_of_urls, oncomplete_function)
function loadscript(urls, oncomplete) {
	if (typeof urls === 'object') {

		if (!urls.length) {
			if (oncomplete) return oncomplete();
			return;
		}

		loadscript(urls[0], function () {
			loadscript(urls.slice(1), oncomplete);
		});
	} else if (typeof urls === 'string') {
		var url = urls;

		var scripts = document.getElementsByTagName('script');
		for (var i = 0; i < scripts.length; ++i) {
			if (scripts[i].src === url) {
				console.log('	Script already loaded:\n' + url);
				if (oncomplete) oncomplete();
				return;
			}
		}

		var e = document.createElement('script');
		e.src = url;
		e.onload = function () {
			console.log('	Injected script:\n' + url);
			if (oncomplete) oncomplete();
		}
		e.onerror = function () {
			e.remove();
			console.warn('	Failed to injected script:\n' + url);
			if (oncomplete) oncomplete();
		}
		document.head.appendChild(e);

	} else {

	}
}


//    Load Style   +3 overloads
//    
//        This will sequentially load 1 style at a time.
//        Run multiple times to load in parallel
//    
//    Usage [1]: loadstyle(string_url)
//    Usage [2]: loadstyle(string_url, oncomplete_function)
//    Usage [1]: loadstyle(array_of_urls)
//    Usage [2]: loadstyle(array_of_urls, oncomplete_function)
function loadstyle(urls, oncomplete) {
	if (typeof urls === 'object') {

		if (!urls.length) {
			if (oncomplete) return oncomplete();
			return;
		}

		loadstyle(urls[0], function () {
			loadstyle(urls.slice(1), oncomplete);
		});
	} else if (typeof urls === 'string') {
		var url = urls;

		var links = document.getElementsByTagName('link');
		for (var i = 0; i < links.length; ++i) {
			if (links[i].href === url) {
				console.log('	Style already loaded:\n' + url);
				if (oncomplete) oncomplete();
				return;
			}
		}
		var styles = document.getElementsByTagName('style');
		for (var i = 0; i < styles.length; ++i) {
			if (styles[i].attributes && (url === styles[i].attributes.src.value)) {
				console.log('	Style already loaded:\n' + url);
				if (oncomplete) oncomplete();
				return;
			}
		}

		var e = document.createElement('link');
		e.rel = 'stylesheet';
		e.type = 'text/css';
		e.href = url;
		e.onload = function () {
			console.log('	Injected style:\n' + url);
			if (oncomplete) oncomplete();
		}
		e.onerror = function () {
			e.remove();
			console.warn('	Failed to injected style:\n' + url);
			if (oncomplete) oncomplete();
		}
		document.head.appendChild(e);

	} else {

	}
}


function recheck_prefixfree() {
	var sleeptime = 200;
	var maxtime = 10000;
	var addtime = Date.now();

	function helper() {
		var realtimemax = maxtime + addtime;
		if (realtimemax < Date.now()) return;

		var time = Date.now();

		setTimeout(function () {
			try {
				StyleFix.link; // test for Prefix Free

				var links = document.getElementsByTagName('link');
				if (links.length) {
					addtime = Date.now();
					for (var i = 0; i < links.length; ++i) {
						StyleFix.link(links[i]);
					}
				}
			} catch (err) {
			}

			helper();
		}, sleeptime);
	}

	helper();
}



function benchmark(f) {
	s = Date.now();
	for (var i = 0; i < 1000000; ++i) f();
	s = Date.now() - s;
	return s / 1000;
}