// ==UserScript== // @name Anime Lighter // @namespace horc.net // @version 3.7.1 // @description Filter for anime trackers [planning for many] // @author RandomClown @ HoRC // @copyright © 2015 // @homepage http://git.horc.net // @icon https://bitbucket.org/horc/anime-lighter/raw/master/img/Logo.png // @grant GM_xmlhttpRequest // @grant GM_log // @require http://code.jquery.com/jquery-2.1.3.min.js // @require https://code.jquery.com/ui/1.11.4/jquery-ui.min.js // @match http://www.nyaa.se/* // @match http://horriblesubs.info // @downloadURL https://update.greasyfork.icu/scripts/9491/Anime%20Lighter.user.js // @updateURL https://update.greasyfork.icu/scripts/9491/Anime%20Lighter.meta.js // ==/UserScript== var greasyfork = true; // // // Source code available: https://bitbucket.org/horc/anime-lighter/ // // /// /// /// /// /// /// /// var HORC_NAME = 'Anime Lighter Beta'; // Wait for ready new function () { var runonce = 0; function complete() { if (document.readyState === 'complete') { document.removeEventListener('DOMContentLoaded', complete, false); window.removeEventListener('load', complete, false); if (runonce++) return; setTimeout(ready); } else { document.addEventListener('DOMContentLoaded', complete, false); window.addEventListener('load', complete, false); } } complete(); } // Now ready function ready() { new StoreUpdater('horc-animes', '1.0'); // <-- refrain from updating this one new StoreUpdater('horc-position', '1.1'); new StoreUpdater('horc-style', '1.0'); if (document.horc_updated) { location.reload(true); return; } run_animelighter(); } // Run main module function run_animelighter() { // Load site mod var mod = document.mod; delete document.readymods; console.log(' Host: ' + window.location.host); mod.createpositions(); new MainUI(mod.style); new AnimeFilter(mod.filteroptions); mod.run(); } /// Dependencies: /// /// // Get Anime - Generic // // Parse an episode text for the anime name // Works for most things function default_getanime(jqe) { var str = jqe.text(); return smartget(str); } //////////////////////////////////////// //// Useful Functions function smartget(str) { // Make it sane var str = trimws(str.replace(/_/g, ' ').replace(/\s\s+/g, ' ')); if (!str.length) throw 'Expected a string'; // Get rid of leading subber if (-1 < str.charAt(0).search(/[\[\(\{]/)) str = trimws(consumetag(str)); // Get rid of next tag & beyond str = trimws(str.substr(0, str.search(/[\[\(\{]/))); // Get rid of episode number[s] of format str = trimws(consumenumber(str)); if (!str.length) throw 'Couldn\'t get the name'; return str; } // Consume Tag // // Gets rid of 1 tag: [subber], [1080p], [etc] function consumetag(str) { var mode = false, done = false; var count = 0; var s = 0, e = 0; for (var i = 0; i < str.length; ++i) { if (done) break; switch (str.charAt(i)) { case '[': case '(': case '{': mode = true; count++; break; case ']': case ')': case '}': count--; if (mode && !count) done = true; break; default: break; } if (mode) e++; else s++, e++; } return str.substr(0, s) + str.substr(e); } // Consume Number // // Gets rid of the episode number function consumenumber(str) { // Consumes numbers with a tack before function cnum_tack(str) { for (var s = 0; s < str.length ; ++s) { var hitnum = false; if (str.charAt(s) === '-' || str.charAt(s) === '‒') { var n = s + 1; // Consume ws for (; n < str.length; ++n) if (!isws(str.charAt(n))) break; // Consume number for (; n < str.length; ++n) { if (-1 < str.charAt(n).search(/\d/)) hitnum = true; break; } if (hitnum) { return str.substr(0, s); } } } return str; } // Consumes numbers without a tack before function cnum_none(str) { var digits = 0; var i = 0; for (i = str.length - 1; 0 <= i; --i) { if (-1 < str.charAt(i).search(/\d/)) digits++; if (str.charAt(i).search(/\d/) === -1) break; } if (i === 0) return str; // reached the end if (1 < digits && isws(str.charAt(i))) return str.substr(0, i); // lone number & at least 2 digits return str; // some mixed number like: S2 } str = cnum_tack(str); str = cnum_none(str); return str; } // Trim Whitespace // // Gets rid of beginning & ending whitespace function trimws(str) { var mode = false, done = false; var count = 0; var s = 0, e = 0; for (s = 0; s < str.length; ++s) { if (str.charAt(s) === ' ') continue; if (str.charAt(s) === '\t') continue; if (str.charAt(s) === '\r') continue; if (str.charAt(s) === '\n') continue; break; } for (e = str.length - 1; 0 <= e; --e) { if (str.charAt(e) === ' ') continue; if (str.charAt(e) === '\t') continue; if (str.charAt(e) === '\r') continue; if (str.charAt(e) === '\n') continue; break; } return str.substr(s, e - s + 1); } function isws(str) { for (var i = 0; i < str.length; ++i) if (!(str.charAt(i) === ' ' || str.charAt(i) === '\t' || str.charAt(i) === '\n' || str.charAt(i) === '\r' || str.charAt(i) === ' ')) return false; return true; } /// Dependencies: /// /// /// if ('horriblesubs.info' === window.location.host) { var options = {}; options.style = "/* Configuration for horriblesubs.info */\n\n.horc-sidebar {\n\tfont-size: 0.75em;\n\twidth: 10em;\n}\n\n\t.horc-sidebar:hover {\n\t\twidth: 44em;\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\twidth: 40em;\n\t}\n\n.horc-ep-droppanel {\n\tfont-size: 1.5em;\n}\n\n\n\n/* Original episode list */\n\n.horc-episode-orig.watch {\n\tbackground-color: rgba(0, 255, 0, 0.25);\n}\n\n.horc-episode-orig.drop {\n\tcolor: rgba(0, 0, 0, 0.15);\n}\n\n.horc-episode-orig.hide {\n\tdisplay: none;\n\tvisibility: hidden;\n}\n\n.horc-episode-orig.dragging {\n\tfont-weight: bolder;\n}\n\n\n\n/* Main UI */\n\n#horc-statusbar {\n\tfont-size: 1.2em;\n\ttext-align: center;\n\tbackground-color: rgba(200,255,200, 0.9);\n\twidth: 100%;\n\tposition: fixed;\n\tpadding: 1em 0 1em 0;\n\tmargin: 0;\n\tleft: 0;\n\tbottom: 0;\n\tborder-radius: 1em;\n}\n\n\t#horc-statusbar > div {\n\t\tmargin: 0 1em 0 1em;\n\t}\n\n.horc-slot {\n\ttext-align: center;\n\twidth: 100%;\n}\n\n#horc-mainui {\n\ttext-align: center;\n\tbackground-color: rgba(230, 230, 230, 0.96);\n\tuser-select: none;\n}\n\n\t#horc-mainui > div {\n\t\tmargin: 1em 0;\n\t\tcursor: initial;\n\t\tuser-select: initial;\n\t}\n\n\t#horc-mainui h2 {\n\t\tcolor: #000000;\n\t\tpadding: 0.2em;\n\t\tmargin: 0;\n\t}\n\n.horc-sidebar {\n\tposition: absolute;\n\tright: 0;\n\toverflow: hidden;\n\tz-index: 1;\n\ttransition: all 0.4s;\n\topacity: 0.25;\n\t-webkit-filter: blur(2px);\n\t-moz-filter: blur(2px);\n\tfilter: blur(2px);\n}\n\n\t.horc-sidebar:hover {\n\t\topacity: 1;\n\t\t-webkit-filter: blur(0px);\n\t\t-moz-filter: blur(0px);\n\t\tfilter: blur(0px);\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\tpadding: 2em;\n\t\tpadding-bottom: 0.5em;\n\t\tborder-radius: 2em;\n\t}\n\n\n\n/* Anime Filter */\n\n.horc-listhead {\n\tcursor: default;\n\tuser-select: none;\n}\n\n\t.horc-listhead.horc-listhead-hide {\n\t\tcolor: #888888 !important;\n\t}\n\n.horc-listcounter {\n\tfont-family: Arial;\n\tfont-weight: initial;\n\tmargin-right: -100%;\n\tfloat: left;\n}\n\n#horc-watchlist {\n\tbackground-color: rgba(0, 255, 0, 0.05);\n}\n\n\t#horc-watchlist > .horc-content {\n\t\tbackground-color: rgba(0, 255, 0, 0.1);\n\t}\n\n#horc-droplist {\n\tbackground-color: rgba(0, 0, 0, 0.05);\n}\n\n\t#horc-droplist > .horc-content {\n\t\tbackground-color: rgba(0, 0, 0, 0.1);\n\t}\n\n#horc-hidelist {\n\tbackground-color: rgba(255, 0, 0, 0.05);\n}\n\n\t#horc-hidelist > .horc-content {\n\t\tbackground-color: rgba(255, 0, 0, 0.1);\n\t}\n\n#horc-mainui .horc-episode-filter {\n}\n\n\t#horc-mainui .horc-episode-filter:not(:last-child) {\n\t\tborder-bottom: 0.2em solid rgba(255, 255, 255, 0.8);\n\t}\n\n\n\n/* Position Drag UI */\n\n.horc-posdrag-icon {\n\twidth: 4em;\n\theight: 6em;\n\tmargin-left: -1em;\n\tmargin-top: -1em;\n\tbackground-color: rgb(230, 230, 230);\n\tborder: 0.25em solid #ffffff;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-slot .horc-ui-droppanel {\n}\n\n\t.horc-slot .horc-ui-droppanel .horc-circle {\n\t\tposition: absolute;\n\t\tdisplay: inline-block;\n\t\tz-index: 8;\n\t\twidth: 8em;\n\t\theight: 8em;\n\t\tmargin: -4em;\n\t\tpadding: 0;\n\t\tbackground: radial-gradient( rgba(100,100,255, 1.0), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8) );\n\t\tborder-radius: 50%;\n\t}\n\n\n\n/* Episode Drag UI */\n\n.horc-episode-icon {\n\tfont-family: Arial, Helvetica, sans-serif;\n\tcolor: #000000;\n\tfont-size: 1.2em;\n\tfont-weight: bold;\n\ttext-align: center;\n\tmax-width: 24em;\n\tmargin-left: -1em;\n\tmargin-top: -3em;\n\tpadding: 1em;\n\tbackground-color: rgb(230, 230, 255);\n\tborder: 0.25em solid #ffffff;\n\tborder-radius: 1em;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-ep-droppanel {\n\tposition: absolute;\n\tmargin: -8em;\n\twidth: calc(16em);\n\theight: calc(16em);\n\tbackground: radial-gradient( rgba(200,200,255, 0.8), rgba(200,200,255, 0.4), rgba(200,200,255, 0.4), rgba(200,200,255, 0.8) );\n\tborder-radius: 50%;\n\tz-index: 3;\n}\n\n\t.horc-ep-droppanel .horc-circle {\n\t\tcolor: #000000;\n\t\ttext-align: center;\n\t\tpointer-events: initial;\n\t\tposition: absolute;\n\t\tdisplay: table;\n\t\tmargin: calc(4.5em * -0.5);\n\t\twidth: calc(4.5em);\n\t\theight: calc(4.5em);\n\t\tborder-radius: 50%;\n\t}\n\n\t\t.horc-ep-droppanel .horc-circle div {\n\t\t\tdisplay: table-cell;\n\t\t\tvertical-align: middle;\n\t\t\tuser-select: none;\n\t\t}\n\n#watchcircle {\n\tleft: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,255,120, 0.75);\n}\n\n\t#watchcircle:hover {\n\t\tbackground-color: rgba(140,255,140, 1.0);\n\t}\n\n#dropcircle {\n\tright: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,140,120, 0.75);\n}\n\n\t#dropcircle:hover {\n\t\tbackground-color: rgba(140,140,140, 1.0);\n\t}\n\n#hidecircle {\n\tleft: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,140,120, 0.75);\n}\n\n\t#hidecircle:hover {\n\t\tbackground-color: rgba(255,140,140, 1.0);\n\t}\n\n#clearcircle {\n\tright: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,255,255, 0.75);\n}\n\n\t#clearcircle:hover {\n\t\tbackground-color: rgba(255,255,255, 1.0);\n\t}\n\n\n\n/* Both Drag UI */\n\n.openhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/openhand.cur) 8 4, move;\n}\n\n.closedhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/closedhand.cur) 8 4, move;\n}\n\n.draggable a, .draggable button {\n\tcursor: default;\n}\n\n.noselect {\n\t-webkit-touch-callout: none;\n\t-webkit-user-select: none;\n\t-khtml-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n"; options.createpositions = function () { // Create possible UI positions MainUI.create_sidebar($('
').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'))); }; options.filteroptions = { // What contains the episode listing? epcontainer: '.episodecontainer', // How to get an episode element? epselector: '.episodecontainer .episode', // How to get anime name from an episode element? getanimename: default_getanime, // How to grep episode container for an anime? epsearch: function (animename) { return $('.episodecontainer .episode').filter(':contains("' + animename + '")'); }, }; options.run = function () { // Find every way to update content function refresh(e) { var lastcount = 0; var timeout = 8000; var _check = setInterval(function () { var episodes = $('.episodecontainer .episode:not(.draggable)'); if (lastcount != episodes.length) { lastcount = episodes.length; document.refreshfilters(); timeout = 1000; return; } else if (0 < timeout) { timeout -= 50; return; } clearInterval(_check); }, 50); } function refresh_enter(e) { if (e.which == 13) refresh(); } function refresh_click(e) { refresh(); } $('.searchbar').on('keyup', refresh_enter); $('.refreshbutton').on('keyup', refresh_enter); $('.refreshbutton').on('mouseup', refresh_click); $('.morebox').on('keyup', '.morebutton, .searchmorebutton', refresh_enter); $('.morebox').on('mouseup', '.morebutton, .searchmorebutton', refresh_click); }; document.mod = options; } /// Dependencies: /// /// /// if ('www.nyaa.se' === window.location.host) { var options = {}; options.style = "/* Configuration for www.nyaa.se */\n\n.horc-sidebar {\n\twidth: 100px;\n\ttop: 280px;\n}\n\n\t.horc-sidebar:hover {\n\t\twidth: 44em;\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\twidth: 40em;\n\t}\n\n#ddpanel {\n\tfont-size: 1.5em;\n}\n\n.horc-episode-orig:not(.watch):not(.drop):not(.hide) * {\n\tfont-weight: normal !important;\n}\n\n.horc-episode-orig.watch .tlistname {\n\tfont-weight: 800 !important;\n}\n\n.horc-episode-orig.drop {\n\topacity: 0.30;\n}\n\n\n\n/* Original episode list */\n\n.horc-episode-orig.watch {\n\tbackground-color: rgba(0, 255, 0, 0.25);\n}\n\n.horc-episode-orig.drop {\n\tcolor: rgba(0, 0, 0, 0.15);\n}\n\n.horc-episode-orig.hide {\n\tdisplay: none;\n\tvisibility: hidden;\n}\n\n.horc-episode-orig.dragging {\n\tfont-weight: bolder;\n}\n\n\n\n/* Main UI */\n\n#horc-statusbar {\n\tfont-size: 1.2em;\n\ttext-align: center;\n\tbackground-color: rgba(200,255,200, 0.9);\n\twidth: 100%;\n\tposition: fixed;\n\tpadding: 1em 0 1em 0;\n\tmargin: 0;\n\tleft: 0;\n\tbottom: 0;\n\tborder-radius: 1em;\n}\n\n\t#horc-statusbar > div {\n\t\tmargin: 0 1em 0 1em;\n\t}\n\n.horc-slot {\n\ttext-align: center;\n\twidth: 100%;\n}\n\n#horc-mainui {\n\ttext-align: center;\n\tbackground-color: rgba(230, 230, 230, 0.96);\n\tuser-select: none;\n}\n\n\t#horc-mainui > div {\n\t\tmargin: 1em 0;\n\t\tcursor: initial;\n\t\tuser-select: initial;\n\t}\n\n\t#horc-mainui h2 {\n\t\tcolor: #000000;\n\t\tpadding: 0.2em;\n\t\tmargin: 0;\n\t}\n\n.horc-sidebar {\n\tposition: absolute;\n\tright: 0;\n\toverflow: hidden;\n\tz-index: 1;\n\ttransition: all 0.4s;\n\topacity: 0.25;\n\t-webkit-filter: blur(2px);\n\t-moz-filter: blur(2px);\n\tfilter: blur(2px);\n}\n\n\t.horc-sidebar:hover {\n\t\topacity: 1;\n\t\t-webkit-filter: blur(0px);\n\t\t-moz-filter: blur(0px);\n\t\tfilter: blur(0px);\n\t}\n\n\t.horc-sidebar #horc-mainui {\n\t\tpadding: 2em;\n\t\tpadding-bottom: 0.5em;\n\t\tborder-radius: 2em;\n\t}\n\n\n\n/* Anime Filter */\n\n.horc-listhead {\n\tcursor: default;\n\tuser-select: none;\n}\n\n\t.horc-listhead.horc-listhead-hide {\n\t\tcolor: #888888 !important;\n\t}\n\n.horc-listcounter {\n\tfont-family: Arial;\n\tfont-weight: initial;\n\tmargin-right: -100%;\n\tfloat: left;\n}\n\n#horc-watchlist {\n\tbackground-color: rgba(0, 255, 0, 0.05);\n}\n\n\t#horc-watchlist > .horc-content {\n\t\tbackground-color: rgba(0, 255, 0, 0.1);\n\t}\n\n#horc-droplist {\n\tbackground-color: rgba(0, 0, 0, 0.05);\n}\n\n\t#horc-droplist > .horc-content {\n\t\tbackground-color: rgba(0, 0, 0, 0.1);\n\t}\n\n#horc-hidelist {\n\tbackground-color: rgba(255, 0, 0, 0.05);\n}\n\n\t#horc-hidelist > .horc-content {\n\t\tbackground-color: rgba(255, 0, 0, 0.1);\n\t}\n\n#horc-mainui .horc-episode-filter {\n}\n\n\t#horc-mainui .horc-episode-filter:not(:last-child) {\n\t\tborder-bottom: 0.2em solid rgba(255, 255, 255, 0.8);\n\t}\n\n\n\n/* Position Drag UI */\n\n.horc-posdrag-icon {\n\twidth: 4em;\n\theight: 6em;\n\tmargin-left: -1em;\n\tmargin-top: -1em;\n\tbackground-color: rgb(230, 230, 230);\n\tborder: 0.25em solid #ffffff;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-slot .horc-ui-droppanel {\n}\n\n\t.horc-slot .horc-ui-droppanel .horc-circle {\n\t\tposition: absolute;\n\t\tdisplay: inline-block;\n\t\tz-index: 8;\n\t\twidth: 8em;\n\t\theight: 8em;\n\t\tmargin: -4em;\n\t\tpadding: 0;\n\t\tbackground: radial-gradient( rgba(100,100,255, 1.0), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8), rgba(100,100,255, 0.8) );\n\t\tborder-radius: 50%;\n\t}\n\n\n\n/* Episode Drag UI */\n\n.horc-episode-icon {\n\tfont-family: Arial, Helvetica, sans-serif;\n\tcolor: #000000;\n\tfont-size: 1.2em;\n\tfont-weight: bold;\n\ttext-align: center;\n\tmax-width: 24em;\n\tmargin-left: -1em;\n\tmargin-top: -3em;\n\tpadding: 1em;\n\tbackground-color: rgb(230, 230, 255);\n\tborder: 0.25em solid #ffffff;\n\tborder-radius: 1em;\n\tposition: fixed;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.horc-ep-droppanel {\n\tposition: absolute;\n\tmargin: -8em;\n\twidth: calc(16em);\n\theight: calc(16em);\n\tbackground: radial-gradient( rgba(200,200,255, 0.8), rgba(200,200,255, 0.4), rgba(200,200,255, 0.4), rgba(200,200,255, 0.8) );\n\tborder-radius: 50%;\n\tz-index: 3;\n}\n\n\t.horc-ep-droppanel .horc-circle {\n\t\tcolor: #000000;\n\t\ttext-align: center;\n\t\tpointer-events: initial;\n\t\tposition: absolute;\n\t\tdisplay: table;\n\t\tmargin: calc(4.5em * -0.5);\n\t\twidth: calc(4.5em);\n\t\theight: calc(4.5em);\n\t\tborder-radius: 50%;\n\t}\n\n\t\t.horc-ep-droppanel .horc-circle div {\n\t\t\tdisplay: table-cell;\n\t\t\tvertical-align: middle;\n\t\t\tuser-select: none;\n\t\t}\n\n#watchcircle {\n\tleft: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,255,120, 0.75);\n}\n\n\t#watchcircle:hover {\n\t\tbackground-color: rgba(140,255,140, 1.0);\n\t}\n\n#dropcircle {\n\tright: 25%;\n\ttop: 25%;\n\tbackground-color: rgba(140,140,120, 0.75);\n}\n\n\t#dropcircle:hover {\n\t\tbackground-color: rgba(140,140,140, 1.0);\n\t}\n\n#hidecircle {\n\tleft: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,140,120, 0.75);\n}\n\n\t#hidecircle:hover {\n\t\tbackground-color: rgba(255,140,140, 1.0);\n\t}\n\n#clearcircle {\n\tright: 25%;\n\tbottom: 25%;\n\tbackground-color: rgba(255,255,255, 0.75);\n}\n\n\t#clearcircle:hover {\n\t\tbackground-color: rgba(255,255,255, 1.0);\n\t}\n\n\n\n/* Both Drag UI */\n\n.openhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/openhand.cur) 8 4, move;\n}\n\n.closedhand {\n\tcursor: url(http://www.google.com/intl/en_ALL/mapfiles/closedhand.cur) 8 4, move;\n}\n\n.draggable a, .draggable button {\n\tcursor: default;\n}\n\n.noselect {\n\t-webkit-touch-callout: none;\n\t-webkit-user-select: none;\n\t-khtml-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n"; options.createpositions = function () { // Create possible UI positions MainUI.create_sidebar($('
').insertBefore($('#main')), undefined, ['position', 'absolute', 'right', '4em', 'top', '20em']); MainUI.create_embed($('
').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 $('