// ==UserScript==
// @name Anime Lighter
// @namespace horc.net
// @version 3.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
// @match http://www.nyaa.se/*
// @match http://horriblesubs.info
// @downloadURL none
// ==/UserScript==
//
// Source available: https://bitbucket.org/horc/anime-lighter/
//
///
///
///
var HORC_VERSION = 'v2'; // Updating this will force the localStorage to clear
document.addEventListener('DOMContentLoaded', function _injectscripts(e) {
document.removeEventListener('DOMContentLoaded', _injectscripts);
userscript_ready();
});
function userscript_ready() {
new Updater('Anime Highlighter', HORC_VERSION); // Major update to force storage clear
loadscript(['http://code.jquery.com/jquery-2.1.3.js',
'https://code.jquery.com/ui/1.11.4/jquery-ui.js'],
run_horc_anime_filter);
loadscript('https://rawgit.com/LeaVerou/prefixfree/gh-pages/prefixfree.js');
loadscript('https://rawgit.com/LeaVerou/prefixfree/gh-pages/plugins/prefixfree.jquery.js');
loadscript('https://rawgit.com/LeaVerou/prefixfree/gh-pages/plugins/prefixfree.dynamic-dom.js');
}
function run_horc_anime_filter() {
console.log(' Host: ' + window.location.host);
switch (window.location.host) {
case 'horriblesubs.info':
run_horriblesubs();
break;
case 'www.nyaa.se':
run_nyaa();
break;
}
}
////////////////////////////////////////
//// Styles
var HORRIBLESUBS_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";
var NYAA_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";
////////////////////////////////////////
//// Run Per Site
function run_horriblesubs() {
{ // Create possible UI positions
MainUI.create_sidebar($('
').insertBefore($('h2:contains("Releases")')), undefined, ['position', 'absolute', 'right', '4em', ]);
MainUI.create_embed($('
').insertBefore($('h2:contains("Releases")')));
MainUI.create_embed($('
').insertAfter($('.episodecontainer')));
MainUI.create_embed($('
').prependTo($('#sidebar')));
MainUI.create_embed($('
').insertAfter($('#text-16')));
MainUI.create_embed($('
').insertAfter($('#text-8')));
MainUI.create_embed($('
').appendTo($('#sidebar')));
}
// Create Main UI
new MainUI(HORRIBLESUBS_STYLE);
// Create Anime Filter
var options = {
// 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 + '")');
},
};
new AnimeFilter(options);
// 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
}
function run_nyaa() {
{ // Create possible UI positions
MainUI.create_sidebar($('
').insertBefore($('#main')), undefined, ['position', 'absolute', 'right', '4em', 'top', '20em']);
MainUI.create_embed($('
').insertBefore($('#main .torrentcats')), ['width', '100%', 'text-align', 'center']);
MainUI.create_embed($('
').insertAfter($('#main .torrentsubcatlist')), ['width', '100%', 'text-align', 'center']);
}
// Create Main UI
new MainUI(NYAA_STYLE);
// Create Anime Filter
var options = {
// 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').filter('.tlistname:contains("' + animename + '")');
},
};
new AnimeFilter(options);
}
////////////////////////////////////////
//// Get Functions
// 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;
}
// 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;
}
}
}
///
///
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();
};
///
///
///
///
// 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)');
// 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 = $(options.epcontainer);
container = $(document);
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
};
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');
};
}
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);
var flag = -1;
if (ep.filter('.watch').length) flag = FLAG_WATCH;
else if (ep.filter('.drop').length) flag = FLAG_DROP;
else if (ep.filter('.hide').length) flag = FLAG_HIDE;
if (flag == -1) return;
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
}
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;
if (!inlist) {
var filter = $('
');
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('
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;
default: // DEBUG
throw 'Missed a case'; // DEBUG
}
jqe.removeClass('watch drop hide');
jqe.addClass(whichselector);
});
$(document).trigger('update-animelist');
};
function filteranime(animename, jqedrop) {
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;
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 = $('' + name + '
');
$('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 = $('');
$('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();
};
///
///
///
///
// 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.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
$('