Manga Loader

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

Ajankohdalta 6.6.2014. Katso uusin versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name       Manga Loader
// @namespace  http://www.fuzetsu.com/MangaLoader
// @version    1.0.8
// @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
// @match http://www.batoto.net/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)

  Any of the CSS selectors can be functions instead that return the desired value.
}
*/

var implementations = [
    { // Batoto
        match: "http://www.batoto.net/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);
      }
    }
  , { // 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'
      , 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: '#top_chapter_list'
      , prevchap: '#top_chapter_list'
      , wait: 2000
    }
  , { // 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 (doesn' work...)
        match: "^http://www.fakku.net/.*/.*/read#page=[0-9]*"
      , img: '.current-page'
      , next: '#image a'
      , numpages: '.drop'
      , curpage: '.drop'
    }
];

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") {
    return localStorage.getItem(key);
  }
  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 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)
    , url = ex('img')
    , nextUrl = ex('next')
    , numPages = ex('numpages')
    , curPage = ex('curpage', {type:'index'}) || 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()
    , getPageInfo = function() {
      var page = d.body;
      d.body.innerHTML = xhr.response;
      try {
        // find image and append
        addImage(ex('img', null, page), loc);
        // find next link and load it
        loadNextPage(ex('next', null, page));
      } catch(e) {
        log('error getting details from next page, assuming end of chapter.');
      }
    }
    , loadNextPage = function(url) {
      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;
      xhr.open('get', url);
      xhr.onload = getPageInfo;
      xhr.onerror = function() {
        log('failed to load page, aborting', 'error');
      };
      xhr.send();
    }
    , lastUrl, loc
  ;

  if(!url || !nextUrl) {
    log('failed to retrieve ' + (!url ? 'image url' : 'next page url'), 'exit');
  }

  loc = getViewer(prevChapter, nextChapter);

  addImage(url, 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;

// register menu command
typeof GM_registerMenuCommand === 'function' && GM_registerMenuCommand((mAutoload ? 'Disable' : 'Enable') + ' manga autoload', function() {
    storeSet('mAutoload', !mAutoload);
    window.location.reload();
});
    
// clear autoload
storeDel('autoload');

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');
}