Anime Lighter

Filter for anime trackers [planning for many]

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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