Greasy Fork

Batoto MyFollows

Filter your follows from comic search, info button for the old follows page, links between Batoto-MU-Mal and other features.

目前为 2014-11-19 提交的版本。查看 最新版本

// ==UserScript== //
// @name         Batoto MyFollows
// @version      14.11.15.2
// @description  Filter your follows from comic search, info button for the old follows page, links between Batoto-MU-Mal and other features.
// @namespace    https://greasyfork.org/users/168
// @match        *://bato.to/*
// @match        *://www.mangaupdates.com/*
// @match        *://myanimelist.net/manga/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceURL
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// @noframes
// @resource     r_followingIcon   https://bato.to/forums/public/style_images/Sylo/star.png
// @resource     r_muIcon          https://www.mangaupdates.com/favicon.ico
// @resource     r_malIcon         http://cdn.myanimelist.net/images/faviconv5.ico
// @resource     r_batotoIcon      https://bato.to/forums/favicon.ico
// @resource     r_infoIcon        https://bato.to/forums/public/style_images/master/information.png
// ==/UserScript==

//-----------------------------------------------------
// Default settings
//-----------------------------------------------------

var defaultsOpts = {

    interlinks: {
        searchEngine: 'google', // 'google' || 'duckduckgo'
        saveUrls: true,

        //comic page
        batoto_mu: true,
        batoto_mal: true,
        mu_mal: true
    },

    batoto: {
        // general
        icon: getResourceURL('r_batotoIcon') || 'https://bato.to/forums/favicon.ico',
        infoIcon: getResourceURL('r_infoIcon') || 'https://bato.to/forums/public/style_images/master/information.png',
        followingIcon: getResourceURL('r_followingIcon') || 'https://bato.to/forums/public/style_images/Sylo/star.png',

        hideBloodHeader: true,
        comicsPopupDelay: 1000, //ms
        defaultToOldFollows: true,

        // home page
        followingIcon_home: true,

        // old follows page
        addInfoBtns: true,

        showTotalFollows: true,
        totalTextColor: '#DDDDDD',
        addBoxToNewFollows: true,
        categorizeFollows: true,

        // comic page
        hyperlinkDesc: true,
        showMoreRecentBtn: true,
        comicsPopup_comic: true,

        // search
        followingIcon_search: true,

        // forums
        comicsPopup_forums: true
    },

    mu: {
        //general
        icon: getResourceURL('r_muIcon') || 'https://www.mangaupdates.com/favicon.ico'
    },

    mal: {
        //general
        icon: getResourceURL('r_malIcon') || 'http://cdn.myanimelist.net/images/faviconv5.ico'
    }
};
// * change the icons in the metadata (@resource) and also in defaultsOpts

//-----------------------------------------------------
// Batoto
//-----------------------------------------------------

var batoto = {
    init: function() {
        if (opts.batoto.defaultToOldFollows) {
            this.defaultToOldFollows();
        }
        ready(function() {

            batoto.loadingImg.init();
            if (/^\/$/.test(path)) {
                batoto.page_home.init();
            } else if (/^\/myfollows/.test(path)) {
                batoto.page_newFollows.init();
            } else if (/^\/follows_comics/.test(path)) {
                batoto.page_oldFollows.init();
            } else if (/^\/search/.test(path)) {
                batoto.page_search.init();
            } else if (/^\/comic\/_\/comics\/.+/.test(path)) {
                batoto.page_comic.init();
            } else if (/^\/forums/.test(path)) {
                batoto.page_forums.init();
            }

        });
    },

    comicIdRegex: /bato\.to\/comic\/_\/.+\-r(\d+)\/?$/i,

    getComicId: function(url) {
        if (!url) return null;
        var id = url.match(batoto.comicIdRegex);
        return id !== null ? id[1] : id;
    },

    theme: null,

    getTheme: function() {
        if (this.theme === null) {
            var storedTheme = getStorage('batoto_theme');
            if (this.isLoggedIn()) {
                ready(function() {
                    var pageTheme = querySel('#new_skin_menucontent > .selected > a');
                    pageTheme = pageTheme.textContent.toLowerCase().replace(/ .*/, '');
                    if (pageTheme !== storedTheme) {
                        setStorage('batoto_theme', pageTheme);
                        setTimeout(function() {
                            location.reload();
                        }, 0);
                    }
                });
            } else {
                setStorage('batoto_theme', 'subway');
            }
            this.theme = storedTheme || 'subway';
        }
        return this.theme;
    },

    loggedIn: null,

    isLoggedIn: function() {
        if (this.loggedIn === null) {
            var storedLoggedId = getStorage('batoto_logged_in');
            ready(function() {
                var pageLoggedId = Window.ipb.vars['member_id'] !== 0;
                if (pageLoggedId !== storedLoggedId) {
                    setStorage('batoto_logged_in', pageLoggedId);
                    setTimeout(function() {
                        location.reload();
                    }, 0);
                }
            });
            this.loggedIn = storedLoggedId || false;
        }
        return this.loggedIn;
    },

    defaultToOldFollows: function() {
        var oldFollows = 'http://bato.to/follows_comics';
        if (path === '/myfollows' && location.search === '') {
            location.replace(oldFollows);
        }
        ready(function() {
            document.getElementById('nav_menu_4_trigger').href = oldFollows;
            if (path === '/') {
                querySel('#hook_watched_items > div:last-child > a').href = oldFollows;
            }
        });
    },

    setComicPopup: function() {
        //uses the same ipb function that previews the profiles
        var links = document.getElementsByTagName('a'),
            popupConf, anchor, id;
        for (var i = 0, len = links.length; i < len; i++) {
            anchor = links[i];
            id = batoto.getComicId(anchor.getAttribute('href'));
            if (id !== null) {
                anchor.className += ' _hovertrigger';
                anchor.setAttribute('hovercard-ref', 'comicPopup');
                anchor.setAttribute('hovercard-id', id);
            }
        }
        popupConf = {
            'w': '680px',
            'delay': opts.batoto.comicsPopupDelay,
            'position': 'auto',
            'ajaxUrl': Window.ipb.vars['home_url'] + '/comic_pop?',
            'getId': true,
            'setIdParam': 'id'
        };
        if (typeof cloneInto !== 'undefined') {
            popupConf = cloneInto(popupConf, unsafeWindow);
        }
        Window.ipb.hoverCardRegister.initialize('comicPopup', popupConf);
    },

    followsList: {
        updating: false,

        update: function(callback) {
            if (!this.updating) {
                var self = this;
                self.updating = true;
                batoto.loadingImg.fadeIn();

                var request = new XMLHttpRequest();
                request.onreadystatechange = function() {
                    if (request.readyState === 4) {
                        if (request.status === 200) {
                            var followsList = [],
                                regex = /"View topic"><strong>(.*?)<\/strong>/g,
                                title = regex.exec(request.responseText);

                            while (title) {
                                followsList.push(decodeHtmlEntity(title[1]));
                                title = regex.exec(request.responseText);
                            }

                            setStorage('follows_list', toCharObject(followsList));

                            if (typeof callback === 'function') {
                                callback();
                            }
                        }
                        batoto.loadingImg.fadeOut();
                        self.updating = false;
                    }
                };
                request.open('GET', '/follows_comics', true);
                request.send();
            }
        }
    },

    loadingImg: {
        init: function() {
            var div = document.getElementById('ajax_loading');
            if (!div) {
                div = createElemHTML(Window.ipb.templates['ajax_loading']);
                div.style.display = 'none';
                document.getElementById('ipboard_body').appendChild(div);
            }
            if (typeof cloneInto !== 'undefined') {
                this.config = cloneInto(this.config, Window);
            }
        },

        config: {
            duration: 0.25
        },

        queue: 0,

        fadeIn: function() {
            this.queue++;
            if (this.queue === 1) {
                if (this.effect) {
                    this.effect.cancel();
                    this.effect = null;
                }
                this.effect = new Window.Effect.Appear('ajax_loading', this.config);
            }
        },

        fadeOut: function() {
            this.queue--;
            if (this.queue === 0) {
                if (this.effect) {
                    this.effect.cancel();
                    this.effect = null;
                }
                this.effect = new Window.Effect.Fade('ajax_loading', this.config);
            }
        }
    }
};


//------------------
// Specific pages
//------------------

batoto.page_home = {
    init: function() {
        if (opts.batoto.followingIcon_home && batoto.isLoggedIn()) {
            this.addFollowsIcons();
        }
    },

    addFollowsIcons: function() {
        var followsList = getStorage('follows_list'),
            icon = createElem('img', {
                title: 'Following',
                src: opts.batoto.followingIcon,
                class: 'bmf_following_icon'
            }),
            titles = querySelAll("td > a[style='font-weight:bold;']"),
            comic;
        if (!followsList) {
            batoto.followsList.update(this.addFollowsIcons);
            return;
        }
        for (var i = 1, len = titles.length; i < len; i += 2) {
            comic = titles[i];
            if (inCharObject(followsList, comic.textContent)) {
                comic.parentNode.insertBefore(icon.cloneNode(false), comic.previousSibling);
            }
        }
    }
};

batoto.page_newFollows = {
    init: function() {
        this.saveFollowsList();
    },

    saveFollowsList: function() {
        var followsList = [],
            titles = document.getElementById('categories').parentNode.children;
        for (var i = 7, len = titles.length - 7; i < len; i++) {
            followsList.push(titles[i].textContent);
        }
        setStorage('follows_list', toCharObject(followsList));
    }
};

batoto.page_oldFollows = {
    init: function() {
        this.titles = [].slice.call(
            document.getElementsByClassName('ipb_table')[0].getElementsByTagName('strong'), 0);

        this.saveFollowsList();

        if (opts.batoto.showTotalFollows) {
            this.showTotalFollows();
        }
        if (opts.batoto.addInfoBtns) {
            this.addInfoBtns();
        }
        if (opts.batoto.addBoxToNewFollows) {
            this.addBoxToNewFollows();
        }
        if (opts.batoto.categorizeFollows) {
            this.categorizeFollows();
        }

        this.titles = null;
    },

    saveFollowsList: function() {
        var followsList = [],
            titles = this.titles;
        for (var i = 0, len = titles.length; i < len; i++) {
            followsList.push(titles[i].textContent);
        }
        setStorage('follows_list', toCharObject(followsList));
    },

    showTotalFollows: function() {
        var total = String(this.titles.length),
            elem = document.createElement('strong');
        elem.id = 'bmf_total_follows';
        elem.textContent = 'Total: ' + total + ' comics!';
        document.getElementsByClassName('maintitle')[0].appendChild(elem);
    },

    addInfoBtns: function() {
        var titles = this.titles,
            showInfo = this.showInfo,
            infoBtn = createElem('a', {
                href: 'javascript:void(0)',
                class: 'bmf_info_button'
            }),
            title;
        for (var i = 0, len = titles.length; i < len; i++) {
            title = titles[i].parentNode;
            title.parentNode.insertBefore(infoBtn.cloneNode(false), title);
            title.previousSibling.addEventListener('click', showInfo, false);
        }
    },

    showInfo: function(event) {
        // modification of the function used by batoto:
        // bato.to/js/shortcuts_20131231.js
        //(prototype.js)
        var infoBtn = this,
            comicId = batoto.getComicId(infoBtn.nextSibling.href),
            div = document.getElementById('cId_' + comicId);

        if (div === null) {
            div = batoto.page_oldFollows.addInfoContainer(infoBtn, comicId);
        }
        if (div.children.length === 0) {
            batoto.loadingImg.fadeIn();

            var request = new XMLHttpRequest();
            request.onreadystatechange = function() {
                if (request.readyState === 4) {
                    batoto.loadingImg.fadeOut();

                    if (request.status === 200) {
                        div.innerHTML = request.responseText;
                        setTimeout(function() {
                            div.style.display = '';
                        }, 30);
                    }
                }
            };
            request.open('GET', '/comic_pop?id=' + comicId, true);
            request.send();

        } else if (div.style.display === '') {
            div.style.display = 'none';
        } else {
            div.style.display = '';
        }
    },

    addInfoContainer: function(infoBtn, comicId) {
        var nextRow = infoBtn.parentNode.parentNode.nextElementSibling,
            div = createElem('div', {
                id: 'cId_' + comicId,
                style: 'display: none;'
            }),
            tr = createElem('tr', {
                class: nextRow.className.replace('altrow', '') + ' bmf_info_row'
            }),
            td = createElem('td', {
                colspan: '2',
                style: 'border-bottom-width:0 !important'
            });

        td.appendChild(div);
        tr.appendChild(td);
        querySel('.ipb_table > tbody').insertBefore(tr, nextRow);
        infoBtn.parentNode.previousElementSibling.setAttribute('rowspan', '3');
        return div;
    },

    categorizeFollows: function() {
        var rows = document.getElementsByTagName('table')[0].rows,
            read = 0,
            reading = 0,
            noReads = 0,
            className, row1, row2, link1, link2, viewOptionsBox, views;
        for (var i = 0, len = rows.length; i < len; i = i + 2) {
            row1 = rows[i];
            row2 = rows[i + 1];
            if (row1.children[2].textContent !== 'Last Read: No Record') {
                link1 = row1.children[2].children[0].getAttribute('href');
                link2 = row2.firstElementChild.children[1].getAttribute('href');
                if (link1 === link2) {
                    className = ' bmf_read';
                    read++;
                } else {
                    className = ' bmf_reading';
                    reading++;
                }
            } else {
                className = ' bmf_noreads';
                noReads++;
            }
            row1.className = row2.className += className;
        }
        viewOptionsBox = createElemHTML(
            '<div class="general_box clearfix"><h3>View Settings</h3><div class="_sbcollapsable">' +
            '<a href="javascript:void(0)" onclick=\"$(\'view\').toggle();">Alter Settings</a>' +
            '<div id="view" style="display: none;">' +
            '<table><tbody>' +
            '<tr><td>All read (' + read + ')</td><td style="text-align: left; vertical-align: top; padding: 6px 0;">' +
            '<label><input id="show1" type="radio" name="read" value="show" checked="checked"> Show</label>' +
            '<label><input id="hide1" type="radio" name="read" value="hide" style="margin-left: 6px"> Hide</label></td></tr>' +
            '<tr><td>Reading (' + reading + ')</td><td style="text-align: left; vertical-align: top; padding: 6px 0;">' +
            '<label><input id="show2" type="radio" name="reading" value="show" checked="checked"> Show</label>' +
            '<label><input id="hide2" type="radio" name="reading" value="hide" style="margin-left: 6px"> Hide</label></td></tr>' +
            '<tr><td>No reads (' + noReads + ')</td><td style="text-align: left; vertical-align: top; padding: 6px 0;">' +
            '<label><input id="show3" type="radio" name="noreads" value="show" checked="checked"> Show</label>' +
            '<label><input id="hide3" type="radio" name="noreads" value="hide" style="margin-left: 6px"> Hide</label></td></tr>' +
            '</tbody></table></div></div>');
        document.getElementById('index_stats').insertBefore(viewOptionsBox, null);
        views = function() {
            var table = document.getElementsByClassName('ipb_table')[0];
            if (this.value === 'hide') {
                table.classList.add('bmf_hide_' + this.name);
            } else {
                table.classList.remove('bmf_hide_' + this.name);
            }
        };
        for (var i = 1; i < 4; i++) {
            document.getElementById('show' + i).addEventListener('click', views, false);
            document.getElementById('hide' + i).addEventListener('click', views, false);
        }
    },

    addBoxToNewFollows: function() {
        var box = document.createElement('div');
        box.className = 'general_box alt clearfix';
        box.innerHTML = '<h3><img src=" ' + Window.ipb.vars['rate_img_on'] + '" alt="">' +
            '\nFollows by Chapters (new follows)</h3>' +
            '<div class="recent_activity _sbcollapsable">' +
            '<div class="tab_toggle_content" style="text-align:center;">' +
            '<a href="/myfollows?noRedirect">Right here!</a></div></div>';
        document.getElementById('index_stats').appendChild(box);
    }
};

batoto.page_comic = {
    init: function() {
        if (opts.batoto.comicsPopup_comic) {
            batoto.setComicPopup();
        }
        if (opts.interlinks.batoto_mu) {
            this.addInterlinkBtn('mu', 'Search in MangaUpdates');
        }
        if (opts.interlinks.batoto_mal) {
            this.addInterlinkBtn('mal', 'Search in MyAnimeList');
        }
        if (opts.batoto.hyperlinkDesc) {
            this.hyperlinkDesc();
        }
        if (opts.batoto.showMoreRecentBtn) {
            this.addShowMoreRecentBtn();
        }
    },

    addInterlinkBtn: function(target, title) {
        var comicTitle = document.getElementsByClassName('ipsType_pagetitle')[0],
            comicName = comicTitle.textContent.replace(/\(doujinshi\)/gi, '(Doujin)'),
            button = createImgBtn(title, 'bmf_interlink_btn ipsButton_secondary', opts[target].icon);
        comicTitle.insertBefore(button, comicTitle.firstChild);
        interlinks.getUrl(target, comicName, location.href, function(url) {
            button.href = url;
        });
        button.addEventListener('click', function(event) {
            if (!event.ctrlKey || !event.shiftKey) return;
            event.preventDefault();
            interlinks.saveFromPrompt(location.href, target);
        });
    },

    hyperlinkDesc: function() {
        var desc = document.getElementsByTagName('tbody')[0].children[6].children[1],
            regex = /(\b(https?):\/\/[\-A-Z0-9+&@#\/%?=~_|!:,.;]*[\-A-Z0-9+&@#\/%=~_|])/gim;
        desc.innerHTML = desc.innerHTML.replace(regex, '<a href="$1">$1</a>');
    },

    addShowMoreRecentBtn: function() {
        var btn = createElem('div', {
            id: 'bmf_show_recent_btn'
        });
        var a = createElem('a', {
            class: 'input_submit',
            href: '/comic/_/comics/?sort_col=record_saved&sort_order=desc'
        });
        a.textContent = 'Show More';
        btn.appendChild(a);
        querySelAll('.general_box')[2].appendChild(btn);
    }
};

batoto.page_search = {
    init: function() {
        if (batoto.isLoggedIn()) {
            this.runMatch('last');
            this.addUpdateFollowsBtn();
            this.addExcludeOption();
            this.watchForNewResults();
        }
    },

    runMatch: function(table) {
        var comics = this.getMatchComics(table),
            icon = createElem('img', {
                title: 'Following',
                src: opts.batoto.followingIcon,
                class: 'bmf_following_icon'
            }),
            matches = comics[0],
            mismatches = comics[1],
            comic, title;

        for (var i = 0, len = matches.length; i < len; i++) {
            comic = matches[i];
            title = comic.firstChild;
            if (opts.batoto.followingIcon_search && title.childNodes.length === 2) {
                title.insertBefore(icon.cloneNode(false), title.firstChild.nextSibling);
            }

            comic.parentNode.parentNode.setAttribute('class', 'bmf_match');
        }

        for (var i = 0, len = mismatches.length; i < len; i++) {
            // if the icon was added but then you unfollowed the comic
            // and updated the list, then css will hide the icon
            mismatches[i].parentNode.parentNode.setAttribute('class', 'bmf_mismatch');
        }
    },

    getMatchComics: function(table) {
        var followsList = getStorage('follows_list'),
            matches = [],
            mismatches = [],
            comics, comic, len, i;

        if (table === 'all') {
            comics = document.getElementsByTagName('strong');
            i = 1;
            len = comics.length - 3;
        } else if (table === 'last') {
            table = document.getElementsByClassName('chapters_list').length - 1;
            comics = document.getElementsByClassName('chapters_list')[table].getElementsByTagName('strong');
            i = 0;
            len = comics.length;
        }

        for (; i < len; i++) {
            comic = comics[i];
            if (inCharObject(followsList, comic.textContent.trim())) {
                matches.push(comic);
            } else {
                mismatches.push(comic);
            }
        }
        return [matches, mismatches];
    },

    matchesVisibility: function(action) {
        if (action === 'hide') {
            document.getElementById('comic_search_results').className = 'bmf_hide_matches';
            // hides the info boxes of matches if shown
            var infoBoxes = document.getElementsByClassName('ipsBox'),
                infoBox;
            for (var i = 0, len = infoBoxes.length; i < len; i++) {
                infoBox = infoBoxes[i].parentNode.parentNode;
                if (infoBox.previousElementSibling.className === 'bmf_match') {
                    infoBox.style.display = 'none';
                }
            }
        } else if (action === 'show') {
            document.getElementById('comic_search_results').className = '';
        }
    },

    addExcludeOption: function() {
        var self = this,
            optionsBar = document.getElementById('advanced_options').children[0],
            optionInput = document.createElement('tr');
        optionInput.innerHTML =
            '<tr><td style="text-align: right; font-weight: bold;">Include MyFollows:</td>' +
            '<td style="text-align: left; vertical-align: top; padding: 8px 0;">' +
            '<label><input id="incl_follows" type="radio" name="follows" value="show" checked="checked"> Yes</label>' +
            '<label><input id="excl_follows" type="radio" name="follows" value="hide" style="margin-left: 4px"> No</label>' +
            '</td></tr>';
        optionsBar.insertBefore(optionInput, optionsBar.children[6]);
        document.getElementById('incl_follows').addEventListener('click', function() {
            self.matchesHidden = false;
            self.matchesVisibility('show');
        }, false);
        document.getElementById('excl_follows').addEventListener('click', function() {
            self.matchesHidden = true;
            self.matchesVisibility('hide');
        }, false);

    },

    watchForNewResults: function() {
        var self = this,
            tablesContainer = document.getElementById('comic_search_results'),
            observer = new MutationObserver(function(mutations) {
                if (mutations.length === 2 || mutations.length === 4) {
                    self.runMatch('last');
                }
            });
        observer.observe(tablesContainer, {
            childList: true
        });
    },

    addUpdateFollowsBtn: function() {
        var self = this,
            searchBar = querySel('#comic_search_form > div > div'),
            updateBtn = createElem('a', {
                id: 'bmf_upd_follows_btn',
                class: 'input_submit',
                title: 'Update Follows',
                href: 'javascript:void(0)'
            });
        updateBtn.textContent = 'Update Follows';
        searchBar.appendChild(updateBtn);
        updateBtn.addEventListener('click', function() {
            batoto.followsList.update(function() {
                self.runMatch('all');
                if (self.matchesHidden) {
                    self.matchesVisibility('hide');
                }
            });
        }, false);
    }
};

batoto.page_forums = {
    init: function() {
        if (opts.batoto.comicsPopup_forums) {
            batoto.setComicPopup();
        }
    }
};


//-----------------------------------------------------
// MangaUpdates
//-----------------------------------------------------

var mu = {
    init: function() {
        ready(function() {

            var url = location.href;
            if (/\/series\.html\?(?:.+&|&?)id=[^&]+/.test(url)) {
                mu.page_comic.init();
            }

        });
    },

    comicIdRegex: /mangaupdates\.com\/series\.html\?(?:.+&|&?)id=([^&]+)/i,

    getComicId: function(url) {
        if (!url) return null;
        var id = url.match(mu.comicIdRegex);
        return id !== null ? id[1] : id;
    }
};

//------------------
// Specific pages
//------------------

mu.page_comic = {
    init: function() {
        if (opts.interlinks.mu_mal) {
            this.addInterlinkBtn('mal', ' Search in MyAnimeList');
        }
        if (opts.interlinks.batoto_mu) {
            this.addInterlinkBtn('batoto', ' Search in Batoto');
        }
    },

    addInterlinkBtn: function(target, title) {
        var comicTitle = document.getElementsByClassName('releasestitle')[0],
            comicName = comicTitle.textContent;
        if (comicName.indexOf('(Novel)') === -1) {
            var button = createImgBtn(title, 'bmf_interlink_btn', opts[target].icon);
            comicTitle.parentNode.insertBefore(button, comicTitle);
            interlinks.getUrl(target, comicName, location.href, function(url) {
                button.href = url;
            });
            button.addEventListener('click', function(event) {
                if (!event.ctrlKey || !event.shiftKey) return;
                event.preventDefault();
                interlinks.saveFromPrompt(location.href, target);
            });
        }
    }
};


//-----------------------------------------------------
// MyAnimeList
//-----------------------------------------------------

var mal = {
    init: function() {
        ready(function() {

            if (/manga/.test(path)) {
                mal.page_comic.init();
            }

        });
    },

    comicIdRegex: /myanimelist\.net\/manga\/(\d+)/i,

    getComicId: function(url) {
        if (!url) return null;
        var id = url.match(mal.comicIdRegex);
        return id !== null ? id[1] : id;
    }
};

mal.page_comic = {
    init: function() {
        if (opts.interlinks.mu_mal) {
            this.addInterlinkBtn('mu', 'Search in MangaUpdates');
        }
        if (opts.interlinks.batoto_mal) {
            this.addInterlinkBtn('batoto', 'Search in Batoto');
        }
    },

    addInterlinkBtn: function(target, title) {
        var comicTitle = querySel('#contentWrapper h1 span').previousSibling,
            comicName = comicTitle.textContent,
            button = createImgBtn(title, 'bmf_interlink_btn', opts[target].icon);
        comicTitle.parentNode.insertBefore(button, comicTitle);
        interlinks.getUrl(target, comicName, location.href, function(url) {
            button.href = url;
        });
        button.addEventListener('click', function(event) {
            if (!event.ctrlKey || !event.shiftKey) return;
            event.preventDefault();
            interlinks.saveFromPrompt(location.href, target);
        });
    }
};


//-----------------------------------------------------
// Interlinks between sites
//-----------------------------------------------------

var interlinks = {
    getUrl: function(targetName, comicName, sourceUrl, callback) {
        var target = this.sites[targetName],
            targetId, source, sourceName, sourceId, searchRequest, savedUrl, temporalUrl;
        if (sourceUrl) {
            for (sourceName in this.sites) {
                source = this.sites[sourceName];
                if (source.urlRegex.test(sourceUrl)) {
                    sourceId = source.getId(sourceUrl);
                    if (sourceId) {
                        savedUrl = this.getSaved(sourceName, sourceId, targetName);
                        if (savedUrl) {
                            callback(savedUrl, 'saved');
                            return;
                        }
                    }
                    break;
                }
            }
        }
        if (!savedUrl) {
            if (opts.interlinks.searchEngine === 'google') {
                searchRequest = googleRequest;
            } else if (opts.interlinks.searchEngine === 'duckduckgo') {
                searchRequest = duckRequest;
            }
            temporalUrl = searchRequest(comicName, target.queryUrl, function(res) {
                if (res.success && res.finalUrl.indexOf(target.queryUrl) !== -1) {
                    callback(res.finalUrl, 'finalUrl');
                    targetId = target.getId(res.finalUrl);
                    if (opts.interlinks.saveUrls && sourceId && targetId) {
                        interlinks.save(sourceName, sourceId, targetName, targetId);
                    }
                } else if (res.finalUrl !== 'not supported') {
                    callback(res.searchUrl, 'search');
                }
            });
            callback(temporalUrl, 'temporal');
        }
    },

    getSaved: function(sourceName, sourceId, targetName) {
        var key = 'interlinks_' + sourceName + '->' + targetName,
            stored = getStorage(key),
            targetId;
        if (!stored) {
            return false;
        }
        targetId = stored[sourceId];
        if (targetId) {
            return this.sites[targetName].comicUrl.replace('$comicId$', targetId);
        }
        return false;
    },

    save: function(sourceName, sourceId, targetName, targetId) {
        var key = 'interlinks_' + sourceName + '->' + targetName,
            stored = getStorage(key);
        if (!stored) {
            stored = {};
        }
        if (targetId === false) {
            delete stored[sourceId];
        } else {
            stored[sourceId] = targetId;
        }
        setStorage(key, stored);
    },

    saveFromPrompt: function(sourceUrl, targetName) {
        var sourceName, source, sourceId, userUrl, target, targetId;
        for (sourceName in this.sites) {
            source = this.sites[sourceName];
            if (source.urlRegex.test(sourceUrl)) {
                sourceId = source.getId(sourceUrl);
                break;
            }
        }
        if (!sourceId) {
            alert('Sorry. Couldn\'t get the ID for the comic here.');
            return;
        }
        userUrl = prompt('Write the URL for ' + targetName.toUpperCase() +
            ' (leave empty to remove):');
        if (userUrl === null) return;
        userUrl = userUrl.trim();
        if (userUrl === '') {
            this.save(sourceName, sourceId, targetName, false);
            alert('Removed successfully.');
            return;
        }
        target = this.sites[targetName];
        if (!target.urlRegex.test(userUrl)) {
            alert('Doesn\'t look like a valid URL for ' + targetName.toUpperCase() + '.');
            return;
        }
        targetId = this.sites[targetName].getId(userUrl);
        if (!targetId) {
            alert('Sorry. Couldn\'t get the ID for the comic.');
            return;
        }
        this.save(sourceName, sourceId, targetName, targetId);
        alert('Saved successfully.');
    },

    sites: {
        batoto: {
            urlRegex: /(bato\.to)|(batoto\.net)/i,
            getId: batoto.getComicId,
            comicUrl: 'http://bato.to/comic/_/comics/-r$comicId$',
            queryUrl: 'bato.to/comic/_/'
        },
        mu: {
            urlRegex: /mangaupdates\.com/i,
            getId: mu.getComicId,
            comicUrl: 'https://www.mangaupdates.com/series.html?id=$comicId$',
            queryUrl: 'mangaupdates.com/series.html?'
        },
        mal: {
            urlRegex: /myanimelist\.net/i,
            getId: mal.getComicId,
            comicUrl: 'http://myanimelist.net/manga/$comicId$',
            queryUrl: 'myanimelist.net/manga/'
        }
    }
};


//-----------------------------------------------------
// CSS
//-----------------------------------------------------

var CSS = {
    load: function(site) {
        var css = this[site]();

        if (!document.head) {
            setTimeout(function() {
                GM_addStyle(css);
            }, 1);
        } else {
            GM_addStyle(css);
        }
    },

    batoto: function() {
        var css = [
            // various
            '.bmf_following_icon {',
                'vertical-align: top;',
                'margin-left: 1px; }',

            '.general_box.clearfix img {',
                'vertical-align: bottom; }',

            '#pu_____hover___comicPopup_inner .ipsBox td {',
                'padding: 5px; }',

            '#pu_____hover___comicPopup_inner .ipsBox td span ~ span {',
                'display: none !important; }',

            '#pu_____hover___comicPopup_inner .ipsBox tr:last-child {',
                'display: none; }',

            // search
            '.bmf_hide_matches .bmf_match {',
                'display: none; }',

            '.bmf_mismatch .bmf_following_icon {',
                'display: none; }',

            '#bmf_upd_follows_btn {',
                'position: absolute;',
                'top: 10px;',
                'left: 110px;',
                'font-weight: normal; }',

            // old follows
            '.bmf_hide_read .bmf_read, ',
                '.bmf_hide_reading .bmf_reading, ',
                '.bmf_hide_noreads .bmf_noreads {',
                'display: none; }',

            '.bmf_read sup {',
                'display: none; }',

            '.bmf_info_row > td {',
                'padding: 0px !important;',
                'border: none !important; }',

            '.bmf_info_row .ipsBox {',
                'background-color: transparent; }',

            '.bmf_info_button {',
                'padding: 0px 8px;',
                'margin-right: 3px;',
                'background-repeat: no-repeat;',
                'background-position: center;',
                'background-image: url(' + opts.batoto.infoIcon + '); }',

            '#bmf_total_follows {',
                'font-size: 13px;',
                'padding-left: 8px;',
                'color: ' + opts.batoto.totalTextColor + ';',
                'display: inline;',
                'vertical-align: middle; }',

            // comic page
            '.bmf_interlink_btn {',
                'display: inline-block !important;',
                'width: 28px;',
                'height: 28px !important;',
                'vertical-align: -2px !important;',
                'text-align: center;',
                'margin: 0px 8px 0px 0px !important;',
                'padding: 0px !important; }',

            '.bmf_interlink_btn img {',
                'padding: 6px;',
                'margin: 0px !important;',
                'vertical-align: -2px !important; }',

            '#bmf_show_recent_btn {',
                'width: 100%;',
                'margin-top: 5px;',
                'position: absolute;',
                'display: flex; }',

            '#bmf_show_recent_btn a {',
                'margin: 0 auto;',
                'font-weight: bold;',
                'font-size: 12px; }'

        ].join('');

        if (opts.batoto.hideBloodHeader && batoto.getTheme() === 'blood') {
            css += [

                '#branding { ',
                    'position: absolute !important; } '

            ].join('');
        }

        return css;
    },

    mu: function() {
        var css = [
            // comic page
            '.bmf_interlink_btn {',
                'background: #e4e4e4;',
                'background: linear-gradient(to bottom ,#f6f6f6, #d7d7d7);',
                'box-shadow: -1px 1px 0px 0px #dfdcdc inset, 1px -1px 0px 0px #bfbfbf inset;',
                'border-radius: 4px;',
                'display: inline-block;',
                'width: 28px;',
                'height: 28px;',
                'margin-right: 5px;',
                'vertical-align: 3px;',
                'text-align: center; }',

            '.bmf_interlink_btn img {',
                'padding: 5px;',
                'vertical-align: -9px;',
                'margin-top: 1px; }'

        ].join('');

        return css;
    },

    mal: function() {
        var css = [
            // comic page
            '.bmf_interlink_btn {',
                'background: #e4e4e4;',
                'background: linear-gradient(to bottom ,#f6f6f6, #d7d7d7);',
                'box-shadow: -1px 1px 0px 0px #dfdcdc inset, 1px -1px 0px 0px #bfbfbf inset;',
                'border-radius: 4px;',
                'display: inline-block;',
                'width: 28px;',
                'height: 28px;',
                'margin-right: 5px;',
                'vertical-align: 3px;',
                'text-align: center; }',

            '.bmf_interlink_btn img {',
                'padding: 5px;',
                'vertical-align: -9px;',
                'margin-top: 1px; }'

        ].join('');

        return css;
    }
};


//-----------------------------------------------------
// Utility functions
//-----------------------------------------------------

function googleRequest(query, queryUrl, callback) {
    var encodedQuery = query.toLowerCase().replace(/\||-|~|"|\*|:/g, ' ').replace(/\s+/g, ' '),
        encodedQuery = encodeURIComponent(encodedQuery),
        encodedQueryUrl = encodeURIComponent(queryUrl),
        temporalUrl = 'https://www.google.com/webhp?#btnI=I&q=' + encodedQuery + '&sitesearch=' + encodedQueryUrl,
        requestUrl = 'https://www.google.com/search?btnI=I&q=' + encodedQuery + '&sitesearch=' + encodedQueryUrl,
        searchUrl = 'https://www.google.com/search?q=' + encodedQuery + '&sitesearch=' + encodedQueryUrl;
    GM_xmlhttpRequest({
        method: 'HEAD',
        url: requestUrl,
        headers: {
            referer: 'https://www.google.com/'
        },
        onload: function(response) {
            var finalUrl = response.finalUrl;
            callback({
                'success': (finalUrl && finalUrl.indexOf('https://www.google.com/') !== 0),
                'query': query,
                'queryUrl': queryUrl,
                'finalUrl': finalUrl || 'not supported',
                'searchUrl': searchUrl
            });
        }
    });
    return temporalUrl;
}

function duckRequest(query, queryUrl, callback) {
    var encodedQuery = query.toLowerCase().replace(/\\|"|!|-|:/g, ' ').replace(/\s+/g, ' '),
        encodedQuery = encodeURIComponent(encodedQuery),
        encodedQueryUrl = encodeURIComponent(queryUrl),
        requestUrl = 'https://duckduckgo.com/?kp=-1&q=%5C' + encodedQuery + '+site%3A' + encodedQueryUrl,
        searchUrl = 'https://duckduckgo.com/?kp=-1&q=' + encodedQuery + '+site%3A' + encodedQueryUrl;
    GM_xmlhttpRequest({
        method: 'GET',
        url: requestUrl,
        onload: function(response) {
            var finalUrl = response.responseText.match(/&uddg=(.+?)'/);
            finalUrl = decodeURIComponent(finalUrl[1]);
            callback({
                'success': (finalUrl && finalUrl.indexOf('https://duckduckgo.com/') !== 0),
                'query': query,
                'queryUrl': queryUrl,
                'finalUrl': finalUrl || false,
                'searchUrl': searchUrl
            });
        }
    });
    // temporalUrl = requestUrl
    return requestUrl;
}

function getStorage(key) {
    var value = localStorage.getItem('BMF_' + key);
    if (value !== undefined && value !== null) {
        value = JSON.parse(value);
    }
    return value;
}

function setStorage(key, value) {
    localStorage.setItem('BMF_' + key, JSON.stringify(value));
}

function decodeHtmlEntity(encoded) {
    var div = document.createElement('div');
    div.innerHTML = encoded;
    return div.firstChild.nodeValue;
}

function createImgBtn(title, className, src, href) {
    var button = createElem('a', {
        title: title || '',
        class: className || '',
        href: href || 'javascript:void(0)'
    });
    var img = createElem('img', {
        src: src,
        alt: ''
    });
    button.appendChild(img);
    return button;
}

function querySel(selector, doc) {
    doc = doc || document;
    return doc.querySelector(selector);
}

function querySelAll(selector, doc, toArray) {
    doc = doc || document;
    var nodes = doc.querySelectorAll(selector);
    return toArray ? [].slice.call(nodes, 0) : nodes;
}

function createElem(tag, attributes, doc) {
    doc = doc || document;
    var elem = doc.createElement(tag);
    for (var attr in attributes) {
        elem.setAttribute(attr, attributes[attr]);
    }
    return elem;
}

function createElemHTML(html, doc) {
    doc = doc || document;
    var elem = doc.createElement('div');
    elem.innerHTML = html;
    return elem.firstElementChild;
}

function toCharObject(stringArray) {
    var object = {},
        firstChar;
    for (var i = 0, len = stringArray.length; i < len; i++) {
        firstChar = stringArray[i].charAt(0);
        object[firstChar] = object[firstChar] || [];
        object[firstChar].push(stringArray[i]);
    }
    return object;
}

function inCharObject(charObject, string) {
    var charArray = charObject[string.charAt(0)] || [];
    return (charArray.indexOf(string) !== -1);
}

function ready(callback) {
    ready.queue = ready.queue || [];
    ready.fired = ready.fired || false;

    if (ready.fired) {
        setTimeout(callback, 0);
        return;
    } else if (document.readyState === 'complete') {
        ready.fired = true;
        setTimeout(callback, 0);
        return;
    }
    if (!ready.whenReady) {
        ready.whenReady = function() {
            if (!ready.fired) {
                ready.fired = true;
                for (var i = 0; i < ready.queue.length; i++) {
                    ready.queue[i].call(window);
                }
                ready.queue = [];
            }
        };
        document.addEventListener('DOMContentLoaded', ready.whenReady, false);
        document.onreadystatechange = function() {
            if (document.readyState === 'complete') {
                ready.whenReady();
            }
        };
    }
    ready.queue.push(callback);
}

function getResourceURL(key) {
    if (typeof GM_getResourceURL !== 'undefined') {
        return GM_getResourceURL(key);
    }
    return false;
}


//-----------------------------------------------------
// Run Script
//-----------------------------------------------------

var Window = this.unsafeWindow || unsafeWindow || window,
    host = location.host,
    path = location.pathname,
    opts = defaultsOpts;


if (host === 'bato.to') {
    CSS.load('batoto');
    batoto.init();
} else if (host === 'www.mangaupdates.com') {
    CSS.load('mu');
    mu.init();
} else if (host === 'myanimelist.net') {
    CSS.load('mal');
    mal.init();
}