- // ==UserScript==
- // @name Manga Loader
- // @namespace http://www.fuzetsu.com/MangaLoader
- // @version 1.3.0
- // @description Loads manga chapter into one page in a long strip format, supports switching chapters and works for a variety of sites, minimal script with no dependencies, easy to implement new sites
- // @copyright 2014+, fuzetsu
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_deleteValue
- // @grant GM_registerMenuCommand
- // @match http://bato.to/read/*
- // @match http://mangafox.me/manga/*/*/*
- // @match http://readms.com/r/*/*/*/*
- // @match http://g.e-hentai.org/s/*/*
- // @match http://exhentai.org/s/*/*
- // @match http://www.fakku.net/*/*/read*
- // @match http://www.mangareader.net/*/*
- // @match http://www.mangahere.co/manga/*/*
- // @match http://www.mangapanda.com/*/*
- // @match http://mangadeer.com/manga/*/*/*/*
- // @match http://mngacow.com/*/*
- // ==/UserScript==
-
- // set to true for manga load without prompt
- var BM_MODE = false;
-
- var scriptName = 'Manga Loader';
-
- /**
- Sample Implementation:
- {
- match: "http://domain.com/.*" // the url to react to for manga loading
- , img: '#image' // css selector to get the page's manga image
- , next: '#next_page' // css selector to get the link to the next page
- , numpages: '#page_select' // css selector to get the number of pages. elements like (select, span, etc)
- , curpage: '#page_select' // css selector to get the current page. usually the same as numPages if it's a select element
- , nextchap: '#next_chap' // css selector to get the link to the next chapter
- , prevchap: '#prev_chap' // same as above except for previous
- , wait: 3000 // how many ms to wait before auto loading (to wait for elements to load)
- , pages: function(next_url, current_page_number, callback, extract_function) {
- // gets called requesting a certain page number (current_page_number)
- // to continue loading execute callback with img to append as first parameter and next url as second parameter
- // only really needs to be used on sites that have really unusual ways of loading images or depend on javascript
- }
-
- Any of the CSS selectors can be functions instead that return the desired value.
- }
- */
-
- var implementations = [
- { // Batoto
- match: "http://bato.to/read/.*"
- , img: '#comic_page'
- , next: '#full_image + div > a'
- , numpages: '#page_select'
- , curpage: '#page_select'
- , nextchap: 'select[name=chapter_select]'
- , prevchap: 'select[name=chapter_select]'
- , invchap: true
- }
- , { // MangaPanda
- match: "http://www.mangapanda.com/.*/[0-9]*"
- , img: '#img'
- , next: '.next a'
- , numpages: '#pageMenu'
- , curpage: '#pageMenu'
- , nextchap: 'td.c5 + td a'
- , prevchap: 'table.c6 tr:last-child td:last-child a'
- }
- , { // MangaFox
- match: "http://mangafox.me/manga/.*/.*/.*"
- , img: '#image'
- , next: '.next_page'
- , numpages: 'select.m'
- , curpage: 'select.m'
- , nextchap: '#chnav p + p a'
- , prevchap: '#chnav a'
- }
- , { // MangaStream
- match: "http://readms.com/r/.*/.*/.*"
- , img: '#manga-page'
- , next: '.next a'
- , numpages: function() {
- var lastPage = getEl('.subnav-wrapper .controls .btn-group:last-child ul li:last-child');
- return parseInt(lastPage.textContent.match(/[0-9]/g).join(''), 10);
- }
- , nextchap: function(prev) {
- var found;
- var chapters = [].slice.call(document.querySelectorAll('.controls > div:first-child > .dropdown-menu > li a'));
- chapters.pop();
- for(var i = 0; i < chapters.length; i++) {
- if(window.location.href.indexOf(chapters[i].href) !== -1) {
- found = chapters[i + (prev ? 1 : -1)];
- if(found) return found.href;
- }
- }
- }
- , prevchap: function() {
- return this.nextchap(true);
- }
- }
- , { // MangaReader
- match: "http://www.mangareader.net/.*/.*"
- , img:'#img'
- , next: '.next a'
- , numpages: '#pageMenu'
- , curpage: '#pageMenu'
- , nextchap: 'td.c5 + td a'
- , prevchap: 'table.c6 tr:last-child td:last-child a'
- }
- , { // MangaCow
- match: "^http://mngacow\.com/.*/[0-9]*"
- , img: '.prw > a > img'
- , next: '.prw > a:last-child'
- , numpages: 'select.cbo_wpm_pag'
- , curpage: 'select.cbo_wpm_pag'
- , nextchap: function(prev) {
- var chapSel = getEl('select.cbo_wpm_chp');
- var nextChap = chapSel.options[chapSel.selectedIndex + (prev ? 1 : -1)];
- if(nextChap) {
- return 'http://mngacow.com/' + window.location.pathname.slice(1, window.location.pathname.slice(1).indexOf('/') + 2) + nextChap.value;
- }
- }
- , prevchap: function() {
- return this.nextchap(true);
- }
- }
- , { // MangaHere
- match: "^http://www.mangahere.co/manga/.*/.*"
- , img: '#viewer img'
- , next: '#viewer a'
- , numpages: 'select.wid60'
- , curpage: 'select.wid60'
- , nextchap: '.reader_tip > p:nth-last-child(2) > a'
- , prevchap: '.reader_tip > p:nth-last-child(1) > a'
- }
- , { // MangaDeer
- match: "^http://mangadeer\.com/manga/.*"
- , img: '.img-link > img'
- , next: '.page > span:last-child > a'
- , numpages: '#sel_page_1'
- , curpage: '#sel_page_1'
- , nextchap: function(prev) {
- var ddl = getEl('#sel_book_1');
- var index = ddl.selectedIndex + (prev ? -1 : 1);
- if(index >= ddl.options.length) return;
- var mangaName = window.location.href.slice(window.location.href.indexOf('manga/') + 6);
- mangaName = mangaName.slice(0, mangaName.indexOf('/'));
- return 'http://mangadeer.com/manga/' + mangaName + ddl.options[index].value + '/1';
- }
- , prevchap: function() {
- return this.nextchap(true);
- }
- }
- , { // GEH/EXH
- match: "http://(g.e-hentai|exhentai).org/s/.*/.*"
- , img: '.sni > a > img, #img'
- , next: '.sni > a, #i3 a'
- }
- , { // Fakku
- match: "^http://www.fakku.net/.*/.*/read"
- , img: '.current-page'
- , next: '.current-page'
- , numpages: '.drop'
- , curpage: '.drop'
- , pages: function(url, num, cb, ex) {
- var firstNum = url.lastIndexOf('/')
- , lastDot = url.lastIndexOf('.');
- var c = url.charAt(firstNum);
- while(c && !/[0-9]/.test(c)) {
- c = url.charAt(++firstNum);
- }
- var curPage = parseInt(url.slice(firstNum, lastDot), 10);
- var url = url.slice(0, firstNum) + ('00' + (curPage + 1)).slice(-3) + url.slice(lastDot);
- cb(url, url);
- }
- }
- ];
-
- var log = function(msg, type) {
- type = type || 'log';
- if(type === 'exit') {
- throw scriptName + ' exit: ' + msg;
- } else {
- console[type](scriptName + ' ' + type + ': ', msg);
- }
- };
-
- var getEl = function(q, c) {
- if(!q) return;
- return (c || document).querySelector(q);
- };
-
- var storeGet = function(key) {
- if(typeof GM_getValue === "undefined") {
- var value = localStorage.getItem(key);
- if(value === "true" || value === "false") {
- return (value === "true") ? true : false;
- }
- return value;
- }
- return GM_getValue(key);
- };
-
- var storeSet = function(key, value) {
- if(typeof GM_setValue === "undefined") {
- return localStorage.setItem(key, value);
- }
- return GM_setValue(key, value);
- };
-
- var storeDel = function(key) {
- if(typeof GM_deleteValue === "undefined") {
- return localStorage.removeItem(key);
- }
- return GM_deleteValue(key);
- };
-
- var extractInfo = function(selector, mod, context) {
- selector = this[selector];
- if(typeof selector === 'function') {
- return selector.call(this);
- }
- var elem = getEl(selector, context)
- , option;
- mod = mod || {};
- if(elem) {
- switch (elem.nodeName.toLowerCase()) {
- case 'img':
- return elem.getAttribute('src');
- case 'a':
- return elem.getAttribute('href');
- case 'ul':
- return elem.children.length;
- case 'select':
- switch(mod.type) {
- case 'index':
- return elem.options.selectedIndex;
- case 'value':
- option = elem.options[elem.options.selectedIndex + (mod.val || 0)] || {};
- return option.value;
- default:
- return elem.options.length;
- }
- break;
- default:
- return elem.textContent;
- }
- }
- };
-
- var toStyleStr = function(obj) {
- var stack = []
- , key;
- for(key in obj) {
- if(obj.hasOwnProperty(key)) {
- stack.push(key + ':' + obj[key]);
- }
- }
- return stack.join(';');
- };
-
- var throttle = function(callback, limit) {
- var wait = false;
- return function() {
- if(!wait) {
- callback();
- wait = true;
- setTimeout(function() {
- wait = false;
- }, limit);
- }
- };
- };
-
- var createButton = function(text, action, styleStr) {
- var button = document.createElement('button');
- button.textContent = text;
- button.onclick = action;
- button.setAttribute('style', styleStr || '');
- return button;
- };
-
- var getViewer = function(prevChapter, nextChapter) {
- var viewerCss = toStyleStr({
- 'background-color': 'black'
- , 'text-align': 'center'
- , 'font': '.9em sans-serif'
- })
- , imagesCss = toStyleStr({
- 'margin': '5px 0'
- })
- , navCss = toStyleStr({
- 'text-decoration': 'none'
- , 'color': 'black'
- , 'background': 'linear-gradient(white, #ccc)'
- , 'padding': '3px 10px'
- , 'border': '1px solid #ccc'
- , 'border-radius': '5px'
- })
- ;
- // clear all styles and scripts
- var title = document.title;
- document.head.innerHTML = '';
- document.title = title;
- // and navigation
- var nav = (prevChapter ? '<a href="' + prevChapter + '" style="' + navCss + '" class="ml-chap-nav">Prev Chapter</a> ' : '') +
- (storeGet('mAutoload') ? '' : '<a href="" style="' + navCss + '">Exit</a> ') +
- (nextChapter ? '<a href="' + nextChapter + '" style="' + navCss + '" class="ml-chap-nav">Next Chapter</a>' : '');
- document.body.innerHTML = nav + '<div id="images" style="' + imagesCss + '"></div>' + nav;
- // set the viewer css
- document.body.setAttribute('style', viewerCss);
- // set up listeners for chapter navigation
- document.addEventListener('click', function(evt) {
- if(evt.target.className.indexOf('ml-chap-nav') !== -1) {
- log('next chapter will autoload');
- storeSet('autoload', 'yes');
- }
- }, false);
- return getEl('#images');
- };
-
- var imageCss = toStyleStr({
- 'max-width': '100%'
- , 'display': 'block'
- , 'margin': '3px auto'
- });
-
- var addImage = function(src, loc, callback) {
- var image = new Image();
- image.onerror = function() {
- log('failed to load ' + src);
- image.remove();
- };
- image.onload = callback;
- image.src = src;
- image.setAttribute('style', imageCss);
- loc.appendChild(image);
- };
-
- var loadManga = function(imp) {
- var ex = extractInfo.bind(imp)
- , imgUrl = ex('img')
- , nextUrl = ex('next')
- , numPages = ex('numpages')
- , curPage = ex('curpage', {type:'index'}) + 1 || 1
- , nextChapter = ex('nextchap', {type:'value', val: (imp.invchap && -1) || 1})
- , prevChapter = ex('prevchap', {type:'value', val: (imp.invchap && 1) || -1})
- , xhr = new XMLHttpRequest()
- , d = document.implementation.createHTMLDocument()
- , addAndLoad = function(img, next) {
- addImage(img, loc);
- loadNextPage(next);
- }
- , getPageInfo = function() {
- var page = d.body;
- d.body.innerHTML = xhr.response;
- try {
- // find image and link to next page
- addAndLoad(ex('img', null, page), ex('next', null, page));
- } catch(e) {
- log('error getting details from next page, assuming end of chapter.');
- }
- }
- , loadNextPage = function(url) {
- if(mLoadLess && count % 10 === 0) {
- if(resumeUrl) {
- resumeUrl = null;
- } else {
- resumeUrl = url;
- log('waiting for user to scroll further before loading more images, loaded ' + count + ' pages so far, next url is ' + resumeUrl);
- return;
- }
- }
- if(++curPage > numPages) {
- log('reached "numPages" ' + numPages + ', assuming end of chapter');
- return;
- }
- if(lastUrl === url) {
- log('last url is the same as current, assuming end of chapter');
- return;
- }
- lastUrl = url;
- if(imp.pages) {
- imp.pages(url, curPage, addAndLoad, ex);
- } else {
- xhr.open('get', url);
- xhr.onload = getPageInfo;
- xhr.onerror = function() {
- log('failed to load page, aborting', 'error');
- };
- xhr.send();
- }
- count += 1;
- }
- , count = 1
- , lastUrl, loc, resumeUrl
- ;
-
- if(!imgUrl || !nextUrl) {
- log('failed to retrieve ' + (!imgUrl ? 'image url' : 'next page url'), 'exit');
- }
-
- loc = getViewer(prevChapter, nextChapter);
-
- if(mLoadLess) {
- window.onscroll = throttle(function(e) {
- if(!resumeUrl) return; // exit early if we don't have a position to resume at
- var scrollBottom = document.body.scrollHeight - ((document.body.scrollTop || document.documentElement.scrollTop) + window.innerHeight);
- if(scrollBottom < 4500) {
- log('user scroll nearing end, loading more images starting from ' + resumeUrl);
- loadNextPage(resumeUrl);
- }
- }, 100);
- }
-
- addImage(imgUrl, loc);
- loadNextPage(nextUrl);
-
- };
-
- var pageUrl = window.location.href
- , btnLoadCss = toStyleStr({
- 'position': 'fixed'
- , 'bottom': 0
- , 'right': 0
- , 'padding': '5px'
- , 'margin': '0 10px 10px 0'
- , 'z-index': '1000'
- })
- , btnLoad
- ;
-
- // used when switching chapters
- var autoload = storeGet('autoload');
- // manually set by user in menu
- var mAutoload = storeGet('mAutoload') || false;
- // should we load less pages at a time?
- var mLoadLess = storeGet('mLoadLess') || false;
-
- // clear autoload
- storeDel('autoload');
-
- // register menu commands
- if(typeof GM_registerMenuCommand === 'function') {
- GM_registerMenuCommand('ML: ' + (mAutoload ? 'Disable' : 'Enable') + ' manga autoload', function() {
- storeSet('mAutoload', !mAutoload);
- window.location.reload();
- });
- GM_registerMenuCommand('ML: Load ' + (mLoadLess ? 'full chapter in one go' : '10 pages at a time'), function() {
- storeSet('mLoadLess', !mLoadLess);
- window.location.reload();
- });
- }
-
- log('starting...');
-
- var success = implementations.some(function(imp) {
- if(imp.match && (new RegExp(imp.match, 'i')).test(pageUrl)) {
- if(BM_MODE || mAutoload || autoload) {
- setTimeout(loadManga.bind(null, imp),imp.wait || 0);
- return true;
- }
- // append button to dom that will trigger the page load
- btnLoad = createButton('Load Manga', function(evt) {
- loadManga(imp);
- this.remove();
- }, btnLoadCss);
- document.body.appendChild(btnLoad);
- return true;
- }
- });
-
- if(!success) {
- log('no implementation for ' + pageUrl, 'error');
- }