').insertBefore($('#main .tlistsortorder')), ['width', '100%', 'text-align', 'center']);
MainUI.create_embed($('
').insertAfter($('#main .torrentsubcatlist')), ['width', '100%', 'text-align', 'center']);
};
options.filteroptions = {
// What contains the episode listing?
epcontainer: '.tlist',
// How to get an episode element?
epselector: '.tlist .tlistrow',
// How to get anime name from an episode element?
getanimename: default_getanime,
// How to grep episode container for an anime?
epsearch: function (animename) {
return $('.tlist .tlistrow').has('.tlistname:contains("' + animename + '")');
},
};
options.run = function () {
};
document.mod = options;
}
// String Compare Case-Sensitive
function strcmp(lhs, rhs) {
for (var i = 0; i < lhs.length && i < rhs.length; ++i) {
if (lhs[i] === rhs[i]) continue;
return lhs[i] < rhs[i] ? -1 : 1;
}
if (lhs.length === rhs.length) return 0;
return lhs.length < rhs.length ? -1 : 1;
}
// String Compare Case-Insensitive
function strcmpi(lhs, rhs) {
for (var i = 0; i < lhs.length && i < rhs.length; ++i) {
if (lhs[i].toLowerCase() === rhs[i].toLowerCase()) continue;
return lhs[i].toLowerCase() < rhs[i].toLowerCase() ? -1 : 1;
}
if (lhs.length === rhs.length) return 0;
return lhs.length < rhs.length ? -1 : 1;
}
// Binary Search Template
//
// In the options, method_options below, you can override the methods:
//
// int compare(element_to_find, element_in_container)
// Expected return: -1, 0, 1
// Determines if the element is less or greater than
// Behavior should match this: http://www.cplusplus.com/reference/cstring/strcmp/
//
// int length(container)
// Expected return: int
// Redefines how to check the size of the container [default container.length]
//
// ElementType get(i, container)
// Expected return: a type matching the parameters of compare(e0, e1)
// Redefines how to access the container by some index
//
// * found(i, container)
// Expected return: anything you require
// Redefines what to return
//
// * notfound(i, container)
// Expected return: anything you require
// Redefines what to return when not found [default null]
// This function is useful to ask "where to insert", since
// "i" represent the position to insert the new element, using splice:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
//
// Usage [2]: int, null binsearch(string_to_find, array_of_strings)
// Usage [3]: custom binsearch(element_to_find, container_object, method_options)
function binsearch(element, container, options) {
if (options === undefined) options = {
compare: undefined,
length: undefined,
get: undefined,
found: undefined,
notfound: undefined,
};
var compare = options.compare;
var length = options.length;
var get = options.get;
var found = options.found;
var notfound = options.notfound;
if (compare === undefined) compare = strcmpi;
function default_len(container) { return container.length; }
if (length === undefined) length = default_len;
function default_arrget(i, container) { return container[i]; }
if (get === undefined) get = default_arrget;
function default_arrret(i, container) { return i; }
if (found === undefined) found = default_arrret;
function default_arrnotret(i, container) { return null; }
if (notfound === undefined) notfound = default_arrnotret;
if (!length(container)) return notfound(0, container);
var begin = 0;
var end = length(container) - 1;
var mid = begin + (end - begin) / 2 | 0;
while (true) {
if (begin === end) {
switch (compare(element, get(mid, container))) {
case -1:
return notfound(mid, container);
case 0:
return found(mid, container);
case 1:
return notfound(mid + 1, container);
}
}
switch (compare(element, get(mid, container))) {
case -1:
end = mid;
mid = begin + (end - begin) / 2 | 0;
break;
case 0:
return found(mid, container);
case 1:
begin = mid + 1;
mid = begin + (end - begin) / 2 | 0;
break;
}
}
}
/// Dependencies:
///
///
///
///
var FLAG_WATCH = 0;
var FLAG_DROP = 1;
var FLAG_HIDE = 2;
var ANIME_NAME = 0;
var ANIME_FLAG = 1;
var ANIME_DATE = 2;
var ANIME_EXPIRE = 3;
// Anime Structure
//
// Holds information on an anime
//
// Member Data:
// - name | Name of the anime according to the tracker
// - flag | View flag
// - date_y | Date added
// - date_m | ^
// - date_d | ^
// - expire_y | Date this will expire on [-1 for never]
// - expire_m | ^
// - expire_d | ^
//
// Member Functions:
// - isgood | Test if expired
// - setexpirecours | Set the expiration date ## "anime network season" from today
// - setexpireweeks | Set the expiration date ## weeks from today
// - setexpiredays | Set the expiration date ## days from today
// - compress | Compress the data for storage
// - decompress | Copies data from a compressed data array
//
// Usage [0]: new Anime()
// Usage [1]: new Anime(anime_dataset)
var Anime = function (anime_dataset) {
var thisanime = this;
this.Anime = function (anime_dataset) {
if (typeof anime_dataset === 'object') {
thisanime.decompress(anime_dataset);
} else {
var d = new Date();
thisanime.date_y = d.getFullYear();
thisanime.date_m = d.getMonth() + 1;
thisanime.date_d = d.getDate();
delete d;
}
};
this.isgood = function () {
if (thisanime.expire_y === -1) return true;
var diff = (new Date(thisanime.expire_y, thisanime.expire_m - 1, thisanime.expire_d - 1)) - (new Date(thisanime.date_y, thisanime.date_m - 1, thisanime.date_d));
if (-1 < diff) return true;
return false;
};
this.setexpiredays = function (amount) {
var d = new Date();
var e = new Date(d.getFullYear(), d.getMonth(), d.getDate() + amount);
thisanime.expire_y = e.getFullYear();
thisanime.expire_m = e.getMonth() + 1;
thisanime.expire_d = e.getDate();
};
this.setexpireweeks = function (amount) {
thisanime.setexpiredays(amount * 7);
};
this.setexpirecours = function (amount) {
thisanime.setexpiredays(amount * 7 * 13);
};
this.compress = function () {
var anime = [];
anime[ANIME_NAME] = thisanime.name; // Name of anime
anime[ANIME_FLAG] = thisanime.flag; // w d h
anime[ANIME_DATE] = thisanime.date_y;
anime[ANIME_DATE] = anime[ANIME_DATE] * 100 + thisanime.date_m;
anime[ANIME_DATE] = anime[ANIME_DATE] * 100 + thisanime.date_d;
if (thisanime.expire_y === -1) {
anime[ANIME_EXPIRE] = -1;
} else {
anime[ANIME_EXPIRE] = thisanime.expire_y;
anime[ANIME_EXPIRE] = anime[ANIME_EXPIRE] * 100 + thisanime.expire_m;
anime[ANIME_EXPIRE] = anime[ANIME_EXPIRE] * 100 + thisanime.expire_d;
}
return anime;
};
this.decompress = function (anime_dataset) {
thisanime.name = anime_dataset[ANIME_NAME]; // Name of anime_dataset
thisanime.flag = anime_dataset[ANIME_FLAG]; // w d h
var value = anime_dataset[ANIME_DATE];
thisanime.date_d = Math.floor(value % 100);
value /= 100;
thisanime.date_m = Math.floor(value % 100);
value /= 100;
thisanime.date_y = Math.floor(value);
var value = anime_dataset[ANIME_EXPIRE];
if (value === -1) {
thisanime.expire_y = thisanime.expire_m = thisanime.expire_d = -1;
} else {
thisanime.expire_d = Math.floor(value % 100);
value /= 100;
thisanime.expire_m = Math.floor(value % 100);
value /= 100;
thisanime.expire_y = Math.floor(value);
}
return thisanime;
};
this.name = ''; // Name of anime
this.flag = -1; // w d h
this.date_y = -1;
this.date_m = -1;
this.date_d = -1;
this.expire_y = -1;
this.expire_m = -1;
this.expire_d = -1;
this.Anime(anime_dataset);
};
// Anime List Manager
//
// Holds information on an anime
//
// Public Methods
// - get | Get an anime entry; If not found, return the insertion index
// - add | Add an anime by Anime struct
// - rm | Set the expiration date ## weeks from today
// - save | Save data to storage
// - load | Reload data from storage
//
// Usage [0]: AnimeList()
var AnimeList = function () {
var thisanimelist = this;
var store = new function () {
this.anime = new Store('horc-animes', [], 'object');
};
store.anime.list = [];
this.AnimeList = function () {
thisanimelist.load();
};
// Add Anime
//
// Inserts the anime to the list & storage
//
// Usage [1]: add(Anime_structure)
this.add = function (anime) {
var find = thisanimelist.get(anime.name);
if (typeof find === 'number') { // Not found
store.anime.list.splice(find, 0, anime); // insert
} else { // Found
store.anime.list.splice(find[0], 1, anime); // replace
}
thisanimelist.save();
};
// Remove Anime
//
// Remove the anime from the list & storage
//
// Usage [1]: rm(string_name)
this.rm = function (name) {
var find = thisanimelist.get(name);
if (typeof find === 'number') { // Not found
} else { // Found
store.anime.list.splice(find[0], 1); // replace
}
thisanimelist.save();
};
// Get Anime
//
// Return:
// If found, the array [ index, Anime structure ]
// If not found, the index at which to insert the anime
//
// Usage [1]: get(string_name)
this.get = function (name) {
return binsearch(name, store.anime.list, {
get: function (i, container) { return container[i].name; },
found: function (i, container) { return [i, container[i]]; },
notfound: function (i, container) { return i; },
});
};
// Save Anime List
//
// Save the anime list to storage
//
// Usage [0]: save()
this.save = function () {
var compressed = $(store.anime.list).map(function (i, e) {
return [e.compress()];
});
store.anime.set(compressed);
};
// Load Anime List
//
// Reload the anime list from storage
//
// Note:
// This will also delete expired entries from storage.
//
// Usage [0]: load()
this.load = function () {
var deletions = false;
store.anime.list = $(store.anime.get()).map(function (i, e) {
var anime = new Anime(e);
if (anime.isgood()) return anime;
deletions = true;
});
if (deletions) thisanimelist.save();
};
this.AnimeList();
};
/// Dependencies:
///
///
///
///
///
// AnimeFilter module
//
// Constructor takes the string selector for slots the UI may position itself in.
// Events are listened to & fired in $(document)
//
// Events this will listen for:
// - set-active | Make the program interactable
// - clear-active | Make the program non-interactable
// - set-ui-droppanel | Open the MainUI position drop panel
// - clear-ui-droppanel | Close the MainUI position drop panel
// - set-ep-droppanel | Open the episode list drop panel
// - clear-ep-droppanel | Close the episode list drop panel
// - update-animelist | Update the anime list count
//
// Events this will trigger:
// -
//
// Options:
// -
//
// Note:
// You are required to allocate at least 1 position as empty elements before constructing this.
var AnimeFilter = function (options) {
var thisanimefilter = this;
var animelist = null;
this.AnimeFilter = function () {
document.animelist = animelist = new AnimeList();
// Bind episodes now & add to document
thisanimefilter.refreshfilters();
document.refreshfilters = thisanimefilter.refreshfilters;
firstbindtouch();
$(document).on('update-animelist', updatelist);
$(document).trigger('update-animelist');
};
function firstbindtouch() {
// Search an element, then it's parents for a selector
function treehas(element, selector) {
// Check this
var jqe = $(element).filter(selector);
if (jqe.length) return $(element);
// Check parents
var jqe = $(element).parents(selector);
if (jqe.length) return jqe;
return null;
}
function typeofelement(jqe) {
if (jqe.hasClass('horc-episode-filter')) return 'fi'; // Episode Filter
if (jqe.hasClass('horc-episode-orig')) return 'ep'; // Episode Original
if (jqe.hasClass('horc-circle')) return 'ep'; // Episode Original
if (jqe.hasClass('horc-slot')) return 'ui'; // UI
if (jqe.hasClass('horc-ui')) return 'ui'; // UI
return '';
}
function shouldreject(jqe) {
return !!treehas(jqe, 'a,button,datalist,input,keygen,output,select,textarea');
}
var touch = new Touch();
var container = $('body');
container.on('mousedown', touch.mousedown);
container.on('mousemove', touch.mousemove);
container.on('mouseup', touch.mouseup);
container.on('touchstart', touch.touchstart);
container.on('touchmove', touch.touchmove);
container.on('touchend', touch.touchend);
touch.onstart = function (ids, changes, e) {
};
touch.onend = function (ids, changes, e) {
};
touch.ondragstart = function (ids, changes, e) {
// If requires control & pressing control don't match, cancel
if (e.ctrlKey ^ document.store.requirectrl.get()) return;
$(changes).map(function (i, changed) {
if (id < -1) return; // ignore middle & right click
var changed = e.originalEvent.changedTouches[i];
var id = changed.identifier;
if (shouldreject(changed.target)) return;
// Check if drag exists
var jqedrag = treehas(changed.target, '.draggable');
if (!jqedrag) return;
// Check type of drag & drop
var typeofdrag = typeofelement(jqedrag);
switch (typeofdrag) {
case 'ep':
var panel = $('.horc-ep-droppanel');
if (!panel.is(':visible')) {
panel.css('left', changed.pageX);
panel.css('top', changed.pageY);
}
$(document).trigger('set-ep-droppanel')
try {
addepicon(id, options.getanimename(jqedrag));
} catch (err) {
setstatus('
Could\'t get the anime\'s name, please report this:
\n
' + jqedrag.text() + '');
}
break;
case 'fi':
var panel = $('.horc-ep-droppanel');
if (!panel.is(':visible')) {
panel.css('left', changed.pageX);
panel.css('top', changed.pageY);
}
$(document).trigger('set-ep-droppanel')
addepicon(id, jqedrag.text());
break;
case 'ui':
$(document).trigger('set-ui-droppanel')
$('#horc-mainui').hide();
adduiicon(id);
break;
}
e.preventDefault();
});
};
touch.ondragend = function (ids, changes, e) {
$(changes).map(function (i, changed) {
if (id < -1) return; // ignore middle & right click
var changed = e.originalEvent.changedTouches[i];
var id = changed.identifier;
if (shouldreject(changed.target)) return;
rmepicon(id);
rmuiicon(id);
// Check if drag & drop both exists
var jqedrag = treehas(changed.target, '.draggable');
if (!jqedrag) return;
var jqedrop = treehas(changed.targetnow, '.droppable');
if (!jqedrop) $('#horc-mainui').show();
if (!jqedrop) return;
// Check type of drag & drop
var typeofdrag = typeofelement(jqedrag);
var typeofdrop = typeofelement(jqedrop);
switch (typeofdrag) {
case 'ep':
switch (typeofdrop) {
case 'ep':
try {
filteranime(options.getanimename(jqedrag), jqedrop);
} catch (err) {
setstatus('
Could\'t get the anime\'s name, please report this:
\n
' + jqedrag.text() + '');
}
break;
case 'ui':
break;
}
break;
case 'fi':
switch (typeofdrop) {
case 'ep':
filteranime(jqedrag.text(), jqedrop);
break;
case 'ui':
break;
}
break;
case 'ui':
switch (typeofdrop) {
case 'ep':
break;
case 'ui':
$('.horc-slot').map(function (i, element) {
if (jqedrop[0] === element) document.store.position.set(i);
});
$('#horc-mainui').appendTo(jqedrop.children('.horc-content')).show();
break;
}
break;
}
e.preventDefault();
});
};
touch.onmove = function (ids, changes, e) {
$(changes).map(function (i, changed) {
if (id < -1) return; // ignore middle & right click
var id = changed.identifier;
var icon = $('#horc-ep' + id + ',#horc-pos' + id);
if (icon.length) {
icon.css('left', changed.clientX);
icon.css('top', changed.clientY);
e.preventDefault();
}
});
};
touch.onfirst = function (ids, changes, e) {
if (e.ctrlKey ^ document.store.requirectrl.get()) return;
$('#horc-mainui,' + options.epcontainer).addClass('noselect');
};
touch.onlast = function (ids, changes, e) {
$(document).trigger('clear-ep-droppanel');
$(document).trigger('clear-ui-droppanel');
$('#horc-mainui,' + options.epcontainer).removeClass('noselect');
};
}
function updatelist() {
$('#horc-mainui .horc-episode-filter').remove();
var eps = $('.horc-episode-orig');
eps.map(function (i, element) {
var ep = $(element);
var animename = '';
try {
animename = options.getanimename(ep);
} catch (err) {
return;
}
// Check which filter this episode falls under
var flag = -1;
if (ep.hasClass('watch')) flag = FLAG_WATCH;
else if (ep.hasClass('drop')) flag = FLAG_DROP;
else if (ep.hasClass('hide')) flag = FLAG_HIDE;
if (flag == -1) return;
// Construct the list selector
var whichselector = '';
switch (flag) {
case FLAG_WATCH:
whichselector = 'watch';
break;
case FLAG_DROP:
whichselector = 'drop';
break;
case FLAG_HIDE:
whichselector = 'hide';
break;
}
// Populate the filter lists, since they were cleared before this map
// Check if the list has the anime
var list = $('#horc-' + whichselector + 'list .horc-content');
var inlist = !!list.find('.horc-episode-filter').map(function (i, element) {
var filter = $(element);
var filtername = filter.text();
if (filtername === animename) return filtername;
}).length;
// Add it maybe
if (!inlist) {
var filter = $('
');
filter.text(animename);
list.append(filter);
}
});
}
// Rebind Episodes
//
// Rebinds the drag event to new episode elements
//
// Note:
// You will need to call this anytime episode lists are populated dynamically.
this.refreshfilters = function () {
$(options.epselector).filter(':not(.horc-episode-orig)').map(function (i, element) {
var jqe = $(element);
jqe.addClass('horc-episode-orig draggable');
var animename = '';
try {
animename = options.getanimename(jqe);
} catch (err) {
return;
}
var search = animelist.get(animename);
if (typeof search === 'number') { // Not yet marked
return; // Dont highlight
}
var flag = search[1].flag;
var whichselector = '';
switch (flag) {
case FLAG_WATCH:
whichselector = 'watch';
break;
case FLAG_DROP:
whichselector = 'drop';
break;
case FLAG_HIDE:
whichselector = 'hide';
break;
}
jqe.removeClass('watch drop hide');
jqe.addClass(whichselector);
});
$(document).trigger('update-animelist');
};
function filteranime(animename, jqedrop) {
// Check which filter this episode falls under
var flag = 0;
if (jqedrop.filter('#clearcircle').length) flag = -1;
else if (jqedrop.filter('#watchcircle').length) flag = FLAG_WATCH;
else if (jqedrop.filter('#dropcircle').length) flag = FLAG_DROP;
else if (jqedrop.filter('#hidecircle').length) flag = FLAG_HIDE;
// Construct the list selector
var whichselector = '';
switch (flag) {
case FLAG_WATCH:
whichselector = 'watch';
break;
case FLAG_DROP:
whichselector = 'drop';
break;
case FLAG_HIDE:
whichselector = 'hide';
break;
case -1:
break;
}
if (flag === -1) { // Clear
// Remove highlight
var eps = options.epsearch(animename);
eps.removeClass('watch drop hide');
// Remove from list
animelist.rm(animename);
$(document).trigger('update-animelist');
return;
}
///////////////////////
// Watch/Drop/Hide //
// Re-highlight
var eps = options.epsearch(animename);
eps.removeClass('watch drop hide');
eps.addClass(whichselector);
var search = animelist.get(animename);
if (typeof search === 'number') { // Not yet added
var newanime = new Anime();
// Update
newanime.name = animename;
newanime.flag = flag;
newanime.setexpirecours(2);
// Save
animelist.add(newanime);
animelist.save();
} else { // Already there; update it
if (search[1].flag !== flag) { // update the flag
// Update
search[1].flag = flag;
animelist.save();
} // Else do nothing
}
$(document).trigger('update-animelist');
}
function addepicon(id, name) {
rmepicon(id);
// Drag icon
var icon = $('
' + name + '
');
$('body').append(icon);
}
function adduiicon(id) {
rmuiicon(id);
// Drag icon
var icon = $('
');
$('body').append(icon);
}
function rmepicon(id) {
$('#horc-ep' + id).remove();
}
function rmuiicon(id) {
$('#horc-pos' + id).remove();
}
var timer_statusbar = null;
function setstatus(msg) {
var statusbar = $('#horc-statusbar');
var msgbox = statusbar.children();
if (timer_statusbar === null) {
msgbox.empty().append(msg);
statusbar.show({ direction: 'down' }, 250);
timer_statusbar = setTimeout(function () {
statusbar.hide({ direction: 'down' }, 250);
}, 10000);
} else {
clearTimeout(timer_statusbar);
timer_statusbar = null;
setstatus(msg);
}
}
this.AnimeFilter();
};
/// Dependencies:
///
///
///
// MainUI module
//
// Constructor takes the string selector for slots the UI may position itself in.
// Events are listened to & fired in $(document)
//
// Events this will listen for:
// - set-active | Make the program interactable
// - clear-active | Make the program non-interactable
// - set-ui-droppanel | Open the MainUI position drop panel
// - clear-ui-droppanel | Close the MainUI position drop panel
// - set-ep-droppanel | Open the episode list drop panel
// - clear-ep-droppanel | Close the episode list drop panel
// - update-animelist | Update the anime list count
//
// Static methods to know of:
// - MainUI.create_embed | Creates an embedded position
// - MainUI.create_sidebar | Creates a position for a sidebar
//
// Note:
// You are required to allocate at least 1 position as empty elements before constructing this.
// Pass the string selector to your allocated positions.
var MainUI = function (default_css) {
var thismainui = this;
if (document.store == undefined) document.store = {};
document.store.css = new Store('horc-style', default_css, 'string');
document.store.handedness = new Store('horc-handedness', 1, 'number');
document.store.position = new Store('horc-position', -1, 'number');
document.store.requirectrl = new Store('horc-requirectrl', false, 'boolean');
document.store.settings = new Store('horc-settings', false, 'boolean');
document.store.updater = new Store('horc-updater', true, 'boolean');
document.store.showwatch = new Store('horc-show-watch', true, 'boolean');
document.store.showdrop = new Store('horc-show-drop', false, 'boolean');
document.store.showhide = new Store('horc-show-hide', false, 'boolean');
this.MainUI = function () {
var slots = $('.horc-slot');
if (slots.length === 0) throw 'No positions for the UI to take';
// Load CSS
$('