Greasy Fork

Greasy Fork is available in English.

Anime Lighter

Filter for anime trackers [planning for many]

当前为 2015-05-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Anime Lighter
// @namespace    horc.net
// @version      3.6
// @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.js
// @require      https://code.jquery.com/ui/1.11.4/jquery-ui.js
// @match        http://www.nyaa.se/*
// @match        http://horriblesubs.info
// ==/UserScript==
// 
//     Source 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 MainUI.js" />
/// <reference path="Class Run.js" />
/// <reference path="Standard.js" />

var HORC_NAME = 'Anime Lighter Beta';

console.warn(GM_info.script.description);

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












	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) {
	if (typeof jqe !== 'object') throw 'Expected an element containing the anime name'; // DEBUG
	var str = jqe.text();

	return smartget(str);
}


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

function smartget(str) {
	if (typeof str !== 'string') throw 'Expected a string'; // DEBUG

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

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

	return str;
}

//    Consume Tag
//    
//        Gets rid of 1 tag: [subber], [1080p], [etc]
function consumetag(str) {
	if (typeof str !== 'string') throw 'Expected a string'; // DEBUG

	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) {
	if (typeof str !== 'string') throw 'Expected a string'; // DEBUG

	//	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) {
	if (typeof str !== 'string') throw 'Expected a string'; // DEBUG

	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 */\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-slot {\n\ttext-align: center;\n\twidth: 100%;\n}\n\n#horc-mainui {\n\ttext-align: center;\n\tbackground-color: rgba(230, 230, 255, 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: 2em;\n\theight: 3.23em;\n\tmargin-left: -1em;\n\tmargin-top: -1em;\n\tbackground-color: #0000ff;\n\tposition: fixed;\n\tz-index: 10;\n\topacity: 0.8;\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\ttext-align: center;\n\twidth: 24em;\n\theight: 4em;\n\tmargin-left: -12em;\n\tmargin-top: -1em;\n\tbackground-color: #ff0000;\n\tposition: fixed;\n\tz-index: 10;\n\topacity: 0.8;\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>').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) {
			console.log('Waiting for more content');

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

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

				clearInterval(_check);

				$('.morebutton').off('keyup', refresh_enter);
				$('.morebutton').off('mouseup', refresh_click);
				$('.morebutton').on('keyup', refresh_enter); // this will reset on refresh
				$('.morebutton').on('mouseup', refresh_click); // this will reset on refresh

				document.refreshfilters();
				$(document).trigger('clearloading');

				console.log('Done');
			}, 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);
		$('.morebutton').on('keyup', refresh_enter); // this will reset on refresh
		$('.morebutton').on('mouseup', refresh_click); // this will reset on refresh
	};

	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 Nyaa */\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-slot {\n\ttext-align: center;\n\twidth: 100%;\n}\n\n#horc-mainui {\n\ttext-align: center;\n\tbackground-color: rgba(230, 230, 255, 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: 2em;\n\theight: 3.23em;\n\tmargin-left: -1em;\n\tmargin-top: -1em;\n\tbackground-color: #0000ff;\n\tposition: fixed;\n\tz-index: 10;\n\topacity: 0.8;\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\ttext-align: center;\n\twidth: 24em;\n\theight: 4em;\n\tmargin-left: -12em;\n\tmargin-top: -1em;\n\tbackground-color: #ff0000;\n\tposition: fixed;\n\tz-index: 10;\n\topacity: 0.8;\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 .torrentcats')), ['width', '100%', 'text-align', 'center']);
		MainUI.create_embed($('<div>').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 binsearch(string_to_find, array_of_strings)
//    Usage [3]: int 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;
	if (typeof compare !== 'function') throw 'Expected a function'; // DEBUG

	function default_len(container) { return container.length; }
	if (length === undefined) length = default_len;
	if (typeof length !== 'function') throw 'Expected a function'; // DEBUG

	function default_arrget(i, container) { return container[i]; }
	if (get === undefined) get = default_arrget;
	if (typeof get !== 'function') throw 'Expected a function'; // DEBUG

	function default_arrret(i, container) { return i; }
	if (found === undefined) found = default_arrret;
	if (typeof found !== 'function') throw 'Expected a function'; // DEBUG

	function default_arrnotret(i, container) { return null; }
	if (notfound === undefined) notfound = default_arrnotret;
	if (typeof notfound !== 'function') throw 'Expected a function'; // DEBUG


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

	var errorcount = 0; // DEBUG
	var errorcountmax = 100 + Math.log(length(container)) / Math.log(2) | 0; // DEBUG

	var begin = 0;
	var end = length(container) - 1;
	var mid = begin + (end - begin) / 2 | 0;
	while (true) {
		//console.log('		[loop ' + errorcount + ']  ' + begin + '   ' + mid + '   ' + end + '  |  direction: ' + compare(element, get(mid, container))); // DEBUG
		if (errorcountmax < errorcount++) throw 'Problem with the methods defined; infinite looping?'; // DEBUG

		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="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 (anime_dataset !== undefined && typeof anime_dataset !== 'object') throw 'Expected an array of an Anime\'s data'; // DEBUG

		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 () {
		document.animelist = this; // DEBUG
		document.animelist_store = store; // DEBUG

		thisanimelist.load();
	};



	//    Add Anime
	//    
	//    Inserts the anime to the list & storage
	//    
	//    Usage [1]: add(Anime_structure)
	this.add = function (anime) {
		if (typeof anime !== 'object') throw 'Expected an Anime structure'; // DEBUG
		if (typeof anime.name !== 'string' && !anime.name.length) throw 'Expected a string'; // DEBUG
		if (typeof anime.flag !== 'number' && anime.flag === -1) throw 'Expected a watch flag'; // DEBUG

		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) {
		if (typeof name !== 'string' && !name.length) throw 'Expected a string'; // DEBUG

		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) {
		if (typeof name !== 'string' && !name.length) throw 'Expected a string'; // DEBUG

		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;

			try { // DEBUG
				console.log('Anime entry expired: ' + anime.name); // DEBUG
			} catch (err) { } // DEBUG
		});

		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 Touch.js" />
/// <reference path="Store.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.animefilter = this; // DEBUG
		document.animelist = animelist = new AnimeList();

		if (typeof options.getanimename !== 'function') throw 'Requires a function callback'; // DEBUG
		if (typeof options.epselector !== 'string') throw 'Invalid selector'; // DEBUG
		if ($(options.epselector).length === 0) throw 'No episodes found'; // DEBUG

		console.log('Binding .draggable to: ' + options.epselector + ':not(.draggable)'); // DEBUG


		//	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) {
			if (typeof element != 'object') throw 'Expected an element'; // DEBUG
			if (typeof selector != 'string') throw 'Expected a selector'; // DEBUG

			//	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) {
			if (typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected an array'; // DEBUG
			if (typeof changes !== 'object' && typeof changes.length !== 'number') throw 'Expected an array'; // DEBUG

		};
		touch.onend = function (ids, changes, e) {
			if (typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected an array'; // DEBUG
			if (typeof changes !== 'object' && typeof changes.length !== 'number') throw 'Expected an array'; // DEBUG

		};
		touch.ondragstart = function (ids, changes, e) {
			if (typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected an array'; // DEBUG
			if (typeof changes !== 'object' && typeof changes.length !== 'number') throw 'Expected an array'; // DEBUG

			//	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')
						addepicon(id, options.getanimename(jqedrag));
						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;
					default: // DEBUG
						throw 'Unhandled case ' + typeofdrag; // DEBUG
				}

				e.preventDefault();
			});
		};
		touch.ondragend = function (ids, changes, e) {
			if (typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected an array'; // DEBUG
			if (typeof changes !== 'object' && typeof changes.length !== 'number') throw 'Expected an array'; // DEBUG

			$(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':
								filteranime(options.getanimename(jqedrag), jqedrop);
								break;
							case 'ui':
								console.warn('Bad combination'); // DEBUG
								break;
							default: // DEBUG
								throw 'Unhandled case'; // DEBUG
						}
						break;
					case 'fi':
						switch (typeofdrop) {
							case 'ep':
								filteranime(jqedrag.text(), jqedrop);
								break;
							case 'ui':
								console.warn('Bad combination'); // DEBUG
								break;
							default: // DEBUG
								throw 'Unhandled case'; // DEBUG
						}
						break;
					case 'ui':
						switch (typeofdrop) {
							case 'ep':
								console.warn('Bad combination'); // DEBUG
								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();

								console.log('Moved MainUI'); // DEBUG
								break;
							default: // DEBUG
								throw 'Unhandled case'; // DEBUG
						}
						break;
					default: // DEBUG
						throw 'Unhandled case'; // DEBUG
				}

				e.preventDefault();
			});
		};
		touch.onmove = function (ids, changes, e) {
			if (typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected an array'; // DEBUG
			if (typeof changes !== 'object' && typeof changes.length !== 'number') throw 'Expected an array'; // DEBUG

			$(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 (typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected an array'; // DEBUG
			if (typeof changes !== 'object' && typeof changes.length !== 'number') throw 'Expected an array'; // DEBUG


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

			$('#horc-mainui,' + options.epcontainer).addClass('noselect');
		};
		touch.onlast = function (ids, changes, e) {
			if (typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected an array'; // DEBUG
			if (typeof changes !== 'object' && typeof changes.length !== 'number') throw 'Expected an array'; // DEBUG

			$(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 = options.getanimename(ep);

			//	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;
				default: // DEBUG
					throw 'Missed a case'; // DEBUG
			}

			//	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 = options.getanimename(jqe);


			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';
					console.log('<refresh> Watching ' + animename); // DEBUG
					break;
				case FLAG_DROP:
					whichselector = 'drop';
					console.log('<refresh> Dropping ' + animename); // DEBUG
					break;
				case FLAG_HIDE:
					whichselector = 'hide';
					console.log('<refresh> Hiding   ' + animename);   // DEBUG
					break;
				default: // DEBUG
					throw 'Missed a case'; // DEBUG
			}

			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';
				console.log('Watching ' + animename); // DEBUG
				break;
			case FLAG_DROP:
				whichselector = 'drop';
				console.log('Dropping ' + animename); // DEBUG
				break;
			case FLAG_HIDE:
				whichselector = 'hide';
				console.log('Hiding   ' + animename);   // DEBUG
				break;
			case -1:
				console.log('Clearing ' + animename); // DEBUG
				break;
			default: // DEBUG
				throw 'Missed a case'; // DEBUG
		}


		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) {
		if (typeof id !== 'number' && typeof id !== 'string') throw 'Expected a string or number'; // DEBUG
		if (typeof name !== 'string') throw 'Expected a string'; // DEBUG

		rmepicon(id);

		//	Drag icon
		var icon = $('<div id="horc-ep' + id + '" class="horc-episode-icon">' + name + '</div>');
		$('body').append(icon);
	}
	function adduiicon(id) {
		if (typeof id !== 'number' && typeof id !== 'string') throw 'Expected a string or number'; // DEBUG

		rmuiicon(id);

		//	Drag icon
		var icon = $('<div id="horc-pos' + id + '" class="horc-posdrag-icon">');
		$('body').append(icon);
	}
	function rmepicon(id) {
		if (typeof id !== 'number' && typeof id !== 'string') throw 'Expected a string or number'; // DEBUG

		$('#horc-ep' + id).remove();
	}
	function rmuiicon(id) {
		if (typeof id !== 'number' && typeof id !== 'string') throw 'Expected a string or number'; // DEBUG

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



	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="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
//    
//    Events this will trigger:
//        - 
//    
//    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', 0, '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 () {
		document.mainui = this; // DEBUG

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

		console.log('MainUI sees ' + slots.length + ' positions it can bind to, with query: $(\'.horc-slot\')'); // DEBUG

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

		create_mainui();
		create_epdragui();

		create_events();
	};



	//    Get Position Value [+1 overloads]
	//    
	//    Return [0]:
	//        Returns the position of the current selection
	//    
	//    Return [1]:
	//        Returns the position value passed in, but corrected for out of bounds
	//    
	//    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) {
		if (pos !== undefined && typeof pos !== 'number') throw 'Parameter is incorrect. See overload list for: .getpositionvalue()'; // DEBUG

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

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

		if (pos < 0) pos = 0;
		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 pos = getpositionvalue();
		return $($('.horc-slot > .horc-content')[pos]);
	};


	//    Create Main UI
	function create_mainui() {
		//	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>New version available </div>');
		updater.hide().appendTo(mainui);
		updater.append($('<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();
				});
			}, 20000);
		}
		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 () {
			console.log('Updated UI stylesheet'); // DEBUG

			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 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]);
		}
	}
}
///	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 verboseoutput = false; // DEBUG

	var touchsupport = 'ontouchstart' in window;

	this.Touch = function () {
		console.log(touchsupport ? '	Touch device detected; watching both mouse & touch' : '	Touch unsupported; watching mouse only'); // DEBUG
	};



	this.mousedown = function (e) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

		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 (verboseoutput) console.log('Mouse Start') // DEBUG
		if (thistouch.onstart) thistouch.onstart([-e.which], changes, e);

		e.done = true; // Notify mouse correction handler
	};
	this.mousemove = function (e) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

		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 (typeof e !== 'object') throw 'Expected an event'; // DEBUG

		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);
		if (verboseoutput) console.log('Mouse End') // DEBUG

		//	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) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

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

		if (verboseoutput) console.log('Touch Start') // DEBUG

		//	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) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

		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) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

		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);
		if (verboseoutput) console.log('Touch End') // DEBUG

		//	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) {
		if (typeof id !== 'number') throw 'Expected a number'; // DEBUG
		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 (typeof ids !== 'number' && typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected a number or array'; // DEBUG
		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 (typeof ids !== 'number' && typeof ids !== 'object' && typeof ids.length !== 'number') throw 'Expected a number or array'; // DEBUG
		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 (typeof e !== 'object') throw 'Expected an event'; // DEBUG
		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) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

		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) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

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

		console.warn('No touches exist'); // DEBUG
		return null;
	}
	//    Get Target Now
	//    
	//    Note:
	//        Requires at least 1 touch or mouse point to exist.
	//    
	//    Usage [1]: gettargetnow(event)
	function gettargetnow(e) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG

		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) {
		if (typeof e !== 'object') throw 'Expected an event'; // DEBUG
		if (typeof id !== 'number') throw 'Expected a number as the identifier'; // DEBUG
		if (0 <= id) throw 'This only needs to be used with mouse events [negative values]'; // DEBUG

		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;
		else throw 'Expected a string or undefined'; // DEBUG
	}

	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 (typeof urls.length !== 'number' || urls.length < 0) throw 'loadscript(urls) expected an array'; // DEBUG
		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 {
		throw 'loadscript(urls) expected a string or an array'; // DEBUG
	}
}


//    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 (typeof urls.length !== 'number' || urls.length < 0) throw 'loadstyle(urls) expected an array'; // DEBUG
		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 {
		throw 'loadstyle(urls) expected a string or an array'; // DEBUG
	}
}


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;
}
//    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) {
		if (typeof keyname !== 'string' || !keyname.length) throw 'Store(keyname, value_default, type) expected a non-empty string for keyname'; // DEBUG
		if (value_default === undefined) throw 'Store(keyname, value_default, type) expected a default value'; // DEBUG
		if (type !== undefined && (typeof type !== 'string' || !type.length)) throw 'Store(keyname, value_default, type) expected a non-empty string for type'; // DEBUG


		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) {
			throw 'Expected type "' + type + '" for "' + keyname + '"'; // DEBUG

			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);
};
//    Updater
//    
//    Note:
//        This will force a storage clear.
//    
//    Usage [2]: new Store(modulename, new_version)
var Updater = function (modulename, newversion) {
	if (typeof modulename !== 'string') throw 'Expected a string'; // DEBUG
	if (typeof newversion !== 'string') throw 'Expected a string'; // DEBUG

	var version = localStorage.getItem('horc-version');
	var siteversion_updated = localStorage.getItem('horc-version.updated');

	if (version == undefined) {
		// 1st time visit

		localStorage.setItem('horc-version', newversion);
	} else if (newversion != version) {
		// Site theme changed since last time; Reboot

		localStorage.clear();
		localStorage.setItem('horc-version', newversion);
		localStorage.setItem('horc-version.updated', 'true');
		window.location.reload(true);
	} else if (siteversion_updated != undefined) {
		// Tell the user it updated

		localStorage.removeItem('horc-version.updated');

		var counter = document.createElement('span');
		counter.style.cssFloat = 'right';
		counter.style.marginLeft = '-100%';

		var msg = document.createElement('div');
		msg.innerHTML = '<b>Major Update - HoRC ' + modulename + '</b><br><br>Some code was incompatible with the last version, so localStorage was cleared (for this site).<br>You can reconfigure your settings now.<br><br>Have a good day.';
		msg.style.textAlign = 'center';
		msg.style.color = '#000000';
		msg.style.backgroundColor = '#eeffee';
		msg.style.padding = '.5em 2em';
		msg.style.margin = '.5em';
		msg.style.width = '24em';
		msg.style.display = 'inline-block';
		msg.style.borderRadius = '2em';
		msg.style.boxShadow = '0 0 1em #000000';
		msg.appendChild(counter);

		var wrapper = document.createElement('div');
		wrapper.style.textAlign = 'center';
		wrapper.style.fontSize = '1.2em';
		wrapper.style.width = '98vw';
		wrapper.style.padding = '0';
		wrapper.style.margin = '0';
		wrapper.style.position = 'absolute';
		wrapper.style.zIndex = '100';
		wrapper.appendChild(msg);

		document.body.insertBefore(wrapper, document.body.childNodes[0]);

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