// ==UserScript== // @name Manga Loader // @namespace http://www.fuzetsu.com/MangaLoader // @version 1.8.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, loads quickly and works on mobile devices through bookmarklet // @copyright 2014+, fuzetsu // @noframes // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @match *://bato.to/read/* // @match http://mangafox.me/manga/*/*/* // @match http://readms.com/r/*/*/*/* // @match http://mangastream.com/read/*/*/*/* // @match http://www.mangareader.net/*/* // @match http://www.mangahere.co/manga/*/* // @match http://www.mangapanda.com/*/* // @match http://mangapark.me/manga/*/*/*/* // @match http://mangacow.co/*/* // @match http://centraldemangas.org/online/*/* // @match http://*.com.br/leitura/online/capitulo/* // @match http://www.mangatown.com/manga/*/* // @match http://manga-joy.com/*/* // @match http://*.dm5.com/m* // @match http://*.senmanga.com/*/*/* // @match http://www.japscan.com/lecture-en-ligne/* // @match http://www.pecintakomik.com/manga/*/* // @match http://dynasty-scans.com/chapters/* // @match http://mangawall.com/manga/*/* // @match http://manga.animea.net/* // @match http://kissmanga.com/Manga/*/* // @match http://view.thespectrum.net/series/* // @match http://manhua.dmzj.com/*/* // @match http://hqbr.com.br/hqs/*/capitulo/*/leitor/0 // @match http://www.dmzj.com/view/*/* // @match http://mangaindo.id/*/* // @match http://mangadoom.co/*/* // @match http://www.mangago.me/read-manga/*/*/*/* // @match http://mangalator.ch/show.php?gallery=* // @match http://eatmanga.com/Manga-Scan/*/* // @match http://www.mangacat.me/*/*/* // @match http://www.mangakaka.com/*/* // @match http://www.readmanga.today/*/* // @match *://mangatraders.org/read-online/*/* // @match http://www.mangainn.me/manga/chapter/* // @match http://*.kukudm.com/comiclist/*/* // @match http://www.mangamap.com/*/* // @match http://www.mangachapter.me/*/*/*.html // @match http://kawaii.ca/reader/* // @match http://lonemanga.com/manga/*/* // -- FOOLSLIDE START // @match http://manga.redhawkscans.com/reader/read/* // @match http://reader.s2smanga.com/read/* // @match http://casanovascans.com/read/* // @match http://reader.vortex-scans.com/read/* // @match http://reader.roseliascans.com/read/* // @match http://mangatopia.net/slide/read/* // @match http://www.twistedhelscans.com/read/* // @match http://reader.sensescans.com/reader/read/* // @match http://reader.kireicake.com/reader/read/* // @match http://substitutescans.com/reader/read/* // @match http://mangaichiscans.mokkori.fr/fs/read/* // @match http://reader.shoujosense.com/read/* // @match http://www.friendshipscans.com/slide/read/* // @match http://manga.famatg.com/read/* // @match http://www.demonicscans.com/FoOlSlide/read/* // @match http://reader.psscans.info/read/* // @match http://onetimescans.com/foolslide/read/* // @match http://necron99scans.com/reader/read/* // @match http://manga.inpowerz.com/read/* // @match http://reader.evilflowers.com/read/* // @match http://reader.cafeconirst.com/read/* // @match http://kobato.hologfx.com/reader/read/* // -- FOOLSLIDE END // @downloadURL none // ==/UserScript== // should be set to true externally if auto loading is wanted (e.g. bookmarklet) var BM_MODE; // short reference to unsafeWindow (or window if unsafeWindow is unavailable e.g. bookmarklet) var W = (typeof unsafeWindow === 'undefined') ? window : unsafeWindow; var scriptName = 'Manga Loader'; var pageTitle = document.title; var IMAGES = { refresh_large: '//rgeorgeeb-tilebuttongenerator.googlecode.com/hg-history/c35993682c2d50149976fd7a1f302f8c01a88716/asset-studio/src/res/clipart/icons/refresh.svg' }; // reusable functions to insert in implementations var reuse = { encodeChinese: function(xhr) { xhr.overrideMimeType('text/html;charset=gbk'); } }; /** Sample Implementation: { name: 'something' // name of the 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), or a css selector to keep trying until it returns an elem , 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 = [{ name: 'batoto', match: "^https?://bato.to/read/.*", img: function(ctx) { var img = getEl('#comic_page', ctx); if(img) { return img.src; } else { var imgs = getEls('#content > div:nth-child(8) > img', ctx).map(function(page) { return page.src; }); if(imgs.length > 0) { this.next = function() { return imgs[0]; }; this.numpages = function() { return imgs.length; }; this.pages = function(url, num, cb, ex) { cb(imgs[num - 1], num); }; return imgs[0]; } } }, next: '#full_image + div > a', numpages: '#page_select', curpage: '#page_select', nextchap: 'select[name=chapter_select]', prevchap: 'select[name=chapter_select]', invchap: true }, { name: 'manga-panda', 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' }, { name: 'mangafox', match: "^http://mangafox.me/manga/[^/]*/[^/]*/[^/]*", img: '#image', next: 'a.next_page', numpages: function() { return extractInfo('select.m') - 1; }, curpage: 'select.m', nextchap: '#chnav p + p a', prevchap: '#chnav a' }, { name: 'manga-stream', match: "^http://(readms|mangastream).com/(r|read)/[^/]*/[^/]*/[^/]*", 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); } }, { name: 'manga-reader', match: "^http://www.mangareader.net/.*/.*", img: '#img', next: '.next a', numpages: '#pageMenu', curpage: '#pageMenu', nextchap: '#chapterMenu', prevchap: '#chapterMenu', wait: function() { return getEl('#chapterMenu option'); } }, { name: 'manga-town', match: "^http://www.mangatown.com/manga/[^/]+/[^/]+", img: '#image', next: '#viewer a', numpages: '.page_select select', curpage: '.page_select select', nextchap: '#top_chapter_list', prevchap: '#top_chapter_list', wait: 1000 }, { name: 'manga-cow, manga-doom, manga-indo', match: "^http://(mangacow|mangadoom|mangaindo)\\.(co|id)/[^/]+/[0-9.]+", img: '.prw a > img', next: '.prw a', numpages: 'select.cbo_wpm_pag', curpage: 'select.cbo_wpm_pag', nextchap: function(prev) { var next = extractInfo('select.cbo_wpm_chp', {type: 'value', val: (prev ? 1 : -1)}); if(next) return window.location.href.replace(/\/[0-9.]+\/?([0-9]+\/?)?[^/]*$/, '/' + next); }, prevchap: function() { return this.nextchap(true); } }, { name: 'manga-here', match: "^http://www.mangahere.co/manga/[^/]+/[^/]+", img: '#viewer img', next: '#viewer a', numpages: 'select.wid60', curpage: 'select.wid60', nextchap: function(prev) { var chapter = W.chapter_list[W.current_chapter_index + (prev ? -1 : 1)]; return chapter && chapter[1]; }, prevchap: function() { return this.nextchap(true); }, wait: function() { return areDefined(W.current_chapter_index, W.chapter_list); } }, { name: 'manga-park', match: "^http://mangapark\\.me/manga/[^/]+/[^/]+/[^/]+", img: '.img-link > img', next: '.page > span:last-child > a', numpages: '#sel_page_1', curpage: '#sel_page_1', nextchap: function(prev) { var next = extractInfo('#sel_book_1', {type: 'value', val: (prev ? -1 : 1)}); if(next) return window.location.href.replace(/\/s[0-9.]+\/c[0-9.]+(\/[^\/]+)?$/, next + '/1'); }, prevchap: function() { return this.nextchap(true); } }, { name: 'central-de-mangas', match: "^http://(centraldemangas\\.org|[^\\.]+\\.com\\.br/leitura)/online/[^/]*/[0-9]*", img: '#manga-page', next: '#manga-page', numpages: '#manga_pages', curpage: '#manga_pages', nextchap: function(prev) { var next = extractInfo('#manga_caps', {type: 'value', val: (prev ? -1 : 1)}); if(next) return window.location.href.replace(/[^\/]+$/, next); }, prevchap: function() { return this.nextchap(true); }, pages: function(url, num, cb, ex) { url = url.slice(0, url.lastIndexOf('-') + 1) + ("0" + num).slice(-2) + url.slice(url.lastIndexOf('.')); cb(url, url); } }, { name: 'manga-joy', match: "^http://manga-joy.com/[^/]*/[0-9]*", img: '.prw img', next: '.nxt', numpages: '.wpm_nav_rdr li:nth-child(3) > select', curpage: '.wpm_nav_rdr li:nth-child(3) > select', nextchap: function(prev) { var next = extractInfo('.wpm_nav_rdr li:nth-child(2) > select', {type: 'value', val: prev ? 1 : -1}); if(next) return window.location.href.replace(/\/[0-9.]+\/?([0-9]+(\/.*)?)?$/, '/' + next); }, prevchap: function() { return this.nextchap(true); } }, { name: 'dm5', match: "^http://[^\\.]*\\.dm5\\.com/m[0-9]*", img: '#cp_image', next: '#cp_image', numpages: '#pagelist', curpage: '#pagelist', pages: function(url, num, cb, ex) { var cid = window.location.href.match(/m[0-9]*/g)[2].slice(1), xhr = new XMLHttpRequest(); xhr.open('get', 'chapterfun.ashx?cid=' + cid + '&page=' + num); xhr.onload = function() { var images = eval(xhr.responseText); cb(images[0], images[0]); }; xhr.send(); } }, { name: 'senmanga-raw', match: "^http://raw\\.senmanga\\.com/[^/]*/[^/]*/[0-9]*", img: function() { return W.new_image; }, next: function() { return this.img(); }, numpages: 'select[name=page]', curpage: 'select[name=page]', nextchap: function(prev) { var next = extractInfo('select[name=chapter]', {type: 'value', val: (prev ? 1 : -1)}); if(next) return window.location.href.replace(/\/[^\/]*\/[0-9]+\/?$/, '') + '/' + next + '/1'; }, prevchap: function() { return this.nextchap(true); }, pages: function(url, num, cb, ex) { cb(W.new_image.replace(/page=[0-9]+/, 'page=' + num), num); } }, { name: 'japscan', match: "^http://www\\.japscan\\.com/lecture-en-ligne/[^/]*/[0-9]*", img: '#imgscan', next: '#next_link', numpages: '#pages', curpage: '#pages', nextchap: '#next_chapter', prevchap: '#back_chapter' }, { // pecintakomik match: "^http://www\\.pecintakomik\\.com/manga/[^/]*/[^/]*", img: '.picture', next: '.pager a:nth-child(3)', numpages: 'select[name=page]', curpage: 'select[name=page]', nextchap: function(prev) { var next = extractInfo('select[name=chapter]', {type: 'value', val: (prev ? 1 : -1)}); if(next) return window.location.href.replace(/\/([^\/]+)\/[0-9]+\/?$/, '/$1/' + next); }, prevchap: function() { return this.nextchap(true); } }, { name: 'dynasty-scans', match: "^http://dynasty-scans.com/chapters/.*", img: '#image > img', next: '#image > img', numpages: function() { return W.pages.length; }, curpage: function() { return parseInt(getEl('#image > div.pages-list > a.page.active').textContent); }, nextchap: '#next_link', prevchap: '#prev_link', pages: function(url, num, cb, ex) { url = W.pages[num - 1].image; cb(url, url); } }, { name: 'manga-kaka', match: "^http://www\\.(mangakaka|mangamap)\\.com/[^/]+/[0-9]+", img: 'img.manga-page', next: '.nav_pag > li:nth-child(1) > a', numpages: 'select.cbo_wpm_pag', curpage: 'select.cbo_wpm_pag', nextchap: function(prev) { var chapter = extractInfo('select.cbo_wpm_chp', { type: 'value', val: (prev ? 1 : -1) }); if(chapter) return window.location.href.replace(/\/[0-9\.]+\/?([0-9]+\/?)?$/, '/' + chapter); }, prevchap: function() { return this.nextchap(true); } }, { name: 'manga-wall', _page: null, match: "^http://mangawall\\.com/manga/[^/]*/[0-9]*", img: 'img.scan', next: function() { if(this._page === null) this._page = W.page; return W.series_url + '/' + W.chapter + '/' + (this._page += 1); }, numpages: '.pageselect', curpage: '.pageselect', nextchap: function(prev) { return W.series_url + '/' + (parseInt(W.chapter.slice(1)) + (prev ? -1 : 1)) + '/1'; }, prevchap: function() { return this.nextchap(true); } }, { name: 'anime-a', _page: null, match: "^http://manga\\.animea.net/.+chapter-[0-9]+(-page-[0-9]+)?.html", img: '#scanmr', next: function() { if(this._page === null) this._page = W.page; return W.series_url + W.chapter + '-page-' + (this._page += 1) + '.html'; }, numpages: '.pageselect', curpage: '.pageselect', nextchap: function(prev) { return W.series_url + 'chapter-' + (parseInt(W.chapter.match(/[0-9]+/)[0]) + (prev ? -1 : 1)) + '-page-1.html'; }, prevchap: function() { return this.nextchap(true); } }, { name: 'kiss-manga', match: "^http://kissmanga\\.com/Manga/[^/]+/.+", img: '#divImage img', next: '#divImage img', numpages: function() { return W.lstImages.length; }, curpage: function() { if(getEls('#divImage img').length > 1) { return 1; } else { return W.currImage + 1; } }, nextchap: '#selectChapter, .selectChapter', prevchap: '#selectChapter, .selectChapter', pages: function(url, num, cb, ex) { cb(W.lstImages[num - 1], num); } }, { name: 'the-spectrum-scans', match: "^http://view\\.thespectrum\\.net/series/[^\\.]+\\.html\\?ch=[^&]+&page=[0-9]+", img: '#mainimage', next: '#mainimage', numpages: '.selectpage', curpage: '.selectpage', nextchap: function(prev) { var next = extractInfo('.selectchapter', {type: 'value', val: prev ? -1 : 1}); if(next) return window.location.href.replace(/ch=.+/, 'ch=' + next + '&page=1'); }, prevchap: function() { return this.nextchap(true); }, pages: function(url, num, cb, ex) { var numRegex = /([0-9]+)\.([a-z]+)$/; var curNum = url.match(numRegex); curNum = parseInt(curNum[1]) + 1; url = url.replace(numRegex, ('00' + curNum).slice(-3) + '.$2'); cb(url, url); } }, { name: 'manhua-dmzj', match: "http://manhua.dmzj.com/[^/]*/[0-9]+(-[0-9]+)?\\.shtml", img: '.pic_link', next: '.pic_link', numpages: function() { return W.arr_pages.length; }, curpage: function() { return W.COMIC_PAGE.getPageNumber(); }, nextchap: '#next_chapter', prevchap: '#prev_chapter', pages: function(url, num, cb, ex) { cb(W.img_prefix + W.arr_pages[num - 1], num); } }, { name: 'hqbr', match: "http://hqbr.com.br/hqs/[^/]+/capitulo/[0-9]+/leitor/0", img: '#hq-page', next: '#hq-page', numpages: function() { return W.pages.length; }, curpage: function() { return W.paginaAtual + 1; }, nextchap: function(prev) { var chapters = getEls('#chapter-dropdown a'), current = parseInt(W.capituloIndex), chapter = chapters[current + (prev ? -1 : 1)]; return chapter && chapter.href; }, prevchap: function() { return this.nextchap(true); }, pages: function(url, num, cb, ex) { cb(W.pages[num - 1], num); } }, { name: 'dmzj', match: "http://www.dmzj.com/view/[^/]+/.+\\.html", img: '#pic', next: '#pic', numpages: '.select_jump', curpage: '.select_jump', nextchap: '.next > a', prevchap: '.pre > a', pages: function(url, num, cb, ex) { cb('http://images.dmzj.com/' + W.pic[num - 1], num); }, wait: 1000 }, { name: 'mangago', match: "http://www.mangago.me/read-manga/[^/]+/[^/]+/[^/]+", img: '#page1', next: '#pic_container', numpages: '#dropdown-menu-page', curpage: function() { return parseInt(getEls('#page-mainer a.btn.dropdown-toggle')[1].textContent.match(/[0-9]+/)[0]); }, nextchap: function(prev) { var chapters = getEls('ul.dropdown-menu.chapter a'), curName = getEls('#page-mainer a.btn.dropdown-toggle')[0].textContent, curIdx; chapters.some(function(chap, idx) { if(chap.textContent.indexOf(curName) === 0) { curIdx = idx; return true; } }); var chapter = chapters[curIdx + (prev ? 1 : -1)]; return chapter && chapter.href; }, prevchap: function() { return this.nextchap(true); } }, { name: 'mangalator', match: "http://mangalator.ch/show.php\\?gallery=[0-9]+", img: '.image img', next: '#next', numpages: 'select[name=image]', curpage: 'select[name=image]', nextchap: function(prev) { var next = extractInfo('select[name=gallery]', {type: 'value', val: (prev ? 1 : -1)}); if(next) return location.href.replace(/\?gallery=[0-9]+/, '?gallery=' + next); }, prevchap: function() { return this.nextchap(true); } }, { name: 'eatmanga', match: "http://eatmanga.com/Manga-Scan/[^/]+/.+", img: '#main_content img', next: '#page_next', numpages: '#pages', curpage: '#pages', nextchap: '#bottom_chapter_list', prevchap: '#bottom_chapter_list', invchap: true }, { name: 'manga-cat', match: "http://www.mangacat.me/[^/]+/[^/]+/[^\\.]+.html", img: '.img', next: '.img-link', numpages: '#page', curpage: '#page', nextchap: '.info a:nth-child(4)', prevchap: '.info a:nth-child(2)' }, { name: 'readmanga.today', match: "http://www\\.readmanga\\.today/[^/]+/.+", img: '.page_chapter img', next: '.list-switcher-2 > li:nth-child(3) > a, .list-switcher-2 > li:nth-child(2) > a', numpages: '.list-switcher-2 select[name=category_type]', curpage: '.list-switcher-2 select[name=category_type]', nextchap: '.jump-menu[name=chapter_list]', prevchap: '.jump-menu[name=chapter_list]', invchap: true }, { name: 'foolslide', match: "^(http://manga.redhawkscans.com/reader/read/.+|http://reader.s2smanga.com/read/.+|http://casanovascans.com/read/.+|http://reader.vortex-scans.com/read/.+|http://reader.roseliascans.com/read/.+|http://mangatopia.net/slide/read/.+|http://www.twistedhelscans.com/read/.+|http://reader.sensescans.com/reader/read/.+|http://reader.kireicake.com/reader/read/.+|http://substitutescans.com/reader/read/.+|http://mangaichiscans.mokkori.fr/fs/read/.+|http://reader.shoujosense.com/read/.+|http://www.friendshipscans.com/slide/read/.+|http://manga.famatg.com/read/.+|http://www.demonicscans.com/FoOlSlide/read/.+|http://necron99scans.com/reader/read/.+|http://www.demonicscans.com/FoOlSlide/read/.+|http://reader.psscans.info/read/.+|http://onetimescans.com/foolslide/read/.+|http://necron99scans.com/reader/read/.+|http://manga.inpowerz.com/read/.+|http://reader.evilflowers.com/read/.+|http://reader.cafeconirst.com/read/.+|http://kobato.hologfx.com/reader/read/.+)", img: function() { return W.pages[W.current_page].url; }, next: function() { return 'N/A'; }, numpages: function() { return W.pages.length; }, curpage: function() { return W.current_page + 1; }, nextchap: function(prev) { var desired; var dropdown = getEls('ul.dropdown')[1] || getEls('ul.uk-nav')[1]; if(!dropdown) return; getEls('a', dropdown).forEach(function(chap, idx, arr) { if(location.href.indexOf(chap.href) === 0) desired = arr[idx + (prev ? 1 : -1)]; }); return desired && desired.href; }, prevchap: function() { return this.nextchap(true); }, pages: function(url, num, cb, ex) { cb(W.pages[num - 1].url, num); }, wait: function() { return W.pages; } }, { name: 'mangatraders', match: "^https?://mangatraders\\.org/read-online/[^/]+/.+", img: function(ctx) { var imgs = getEls('img', ctx); var found; imgs.some(function(img) { if(img.src.indexOf('http://img') > -1) { found = img.src; return true; } }); return found; }, next: 'body > div.container.mainPageContainer > ol > li:nth-child(4) > a', numpages: '#changePageSelect', curpage: '#changePageSelect', nextchap: function(prev) { var next = extractInfo('#changeChapterSelect', {type:'value', val: (prev ? -1 : 1)}); if(next) { var chapter = next.split(';')[0]; return location.href.replace(/chapter-[0-9]+/, 'chapter-' + chapter).replace(/page-[0-9]+/, 'page-1'); } }, prevchap: function() { return this.nextchap(true); } }, { name: 'mangainn', match: "^http://www.mangainn.me/manga/chapter/.+", img: '#imgPage', next: function() { if(!this._count) this._count = extractInfo(this.curpage, {type: 'value'}); var url = location.href; if(!/page_[0-9]+/.test(url)) url += '/page_1'; return url.replace(/page_[0-9]+/, 'page_' + (++this._count)); }, numpages: '#cmbpages', curpage: '#cmbpages', nextchap: function(prev) { var next = extractInfo('#chapters', {type:'value', val: (prev ? -1 : 1)}); if(next) return location.href.replace(/\/chapter\/.+$/, '/chapter/' + next + '/page_1'); }, prevchap: function() { return this.nextchap(true); } }, { name: 'kukudm', match: "http://(www|comic|comic2|comic3).kukudm.com/comiclist/[0-9]+/[0-9]+/[0-9]+.htm", img: function(ctx) { var script = getEl('td > script[language=javascript]', ctx); if(script) { return 'http://n.kukudm.com/' + script.textContent.match(/\+"([^']+)/)[1]; } }, next: function(ctx) { var links = getEls('td > a', ctx); return links[links.length - 1].getAttribute('href'); }, numpages: function(cur) { return parseInt(document.body.textContent.match(/共([0-9]+)页/)[1]); }, curpage: function() { return parseInt(document.body.textContent.match(/第([0-9]+)页/)[1]); }, beforexhr: reuse.encodeChinese }, { name: 'mangachapter', match: "http://www\\.mangachapter\\.me/[^/]+/[^/]+/[^/]+.html", img: '#mangaImg, #viewer > table > tbody > tr > td:nth-child(1) > a:nth-child(2) > img', next: '.page-select + a.button-page', numpages: '.page-select select', curpage: '.page-select select', invchap: true, nextchap: '#top_chapter_list', prevchap: '#top_chapter_list', wait: '#top_chapter_list' }, { name: 'kawaii', match: "http://kawaii.ca/reader/.+", img: '.picture', next: 'select[name=page] + a', numpages: 'select[name=page]', curpage: 'select[name=page]', nextchap: function(prev) { var next = extractInfo('select[name=chapter]', {type:'value', val: (prev ? -1 : 1)}); if(next) return location.href.replace(/\/reader\/([^/]+)(\/.+)?$/, '/reader/$1/' + next); }, prevchap: function() { return this.nextchap(true); } }, { name: 'lonemanga', match: "http://lonemanga.com/manga/[^/]+/[^/]+", img: '#imageWrapper img', next: '#imageWrapper a', numpages: '.viewerPage', curpage: '.viewerPage', nextchap: function(prev) { var next = extractInfo('.viewerChapter', {type:'value', val: (prev ? 1 : -1)}); if(next) return location.href.replace(/\/manga\/([^/]+)\/.+$/, '/manga/$1/' + next); }, prevchap: function() { return this.nextchap(true); } }]; var log = function(msg, type) { type = type || 'log'; if (type === 'exit') { throw scriptName + ' exit: ' + msg; } else { console[type]('%c' + scriptName + ' ' + type + ':', 'font-weight:bold;color:green;', msg); } }; var getEl = function(q, c) { if (!q) return; return (c || document).querySelector(q); }; var getEls = function(q, c) { return [].slice.call((c || document).querySelectorAll(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 areDefined = function() { return [].every.call(arguments, function(arg) { return arg !== undefined && arg !== null; }); }; var updateObj = function(orig, ext) { var key; for (key in ext) { if (orig.hasOwnProperty(key) && ext.hasOwnProperty(key)) { orig[key] = ext[key]; } } return orig; }; var extractInfo = function(selector, mod, context) { selector = this[selector] || selector; if (typeof selector === 'function') { return selector.call(this, context); } var elem = getEl(selector, context), option; mod = mod || {}; if (elem) { switch (elem.nodeName.toLowerCase()) { case 'img': return (mod.altProp && elem.getAttribute(mod.altProp)) || elem.src || elem.getAttribute('src'); case 'a': return elem.href || elem.getAttribute('href'); case 'ul': return elem.children.length; case 'select': switch (mod.type) { case 'index': return elem.options.selectedIndex + 1; case 'value': option = elem.options[elem.options.selectedIndex + (mod.val || 0)] || {}; return option.value; default: return elem.options.length; } break; default: switch (mod.type) { case 'index': return parseInt(elem.textContent); default: return elem.textContent; } } } }; var addStyle = function(id, replace) { if(!this.MLStyles) this.MLStyles = {}; if(!this.MLStyles[id]) { this.MLStyles[id] = document.createElement('style'); this.MLStyles[id].dataset.name = 'ml-style-' + id; document.head.appendChild(this.MLStyles[id]); } var style = this.MLStyles[id]; var css = [].slice.call(arguments, 2).join('\n'); if(replace) { style.textContent = css; } else { style.textContent += css; } }; var toStyleStr = function(obj, selector) { var stack = [], key; for (key in obj) { if (obj.hasOwnProperty(key)) { stack.push(key + ':' + obj[key]); } } if (selector) { return selector + '{' + stack.join(';') + '}'; } else { 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', 'font': '0.813em courier', 'text-align': 'center', }, 'body'), imagesCss = toStyleStr({ 'margin-top': '10px', 'margin-bottom': '10px' }, '.ml-images'), imageCss = toStyleStr({ 'max-width': '100%', 'display': 'block', 'margin': '3px auto' }, '.ml-images img'), counterCss = toStyleStr({ 'background-color': '#222', 'color': 'white', 'border-radius': '10px', 'width': '30px', 'margin-left': 'auto', 'margin-right': 'auto', 'margin-top': '-12px', 'padding-left': '5px', 'padding-right': '5px', 'border': '1px solid white', 'z-index': '100', 'position': 'relative' }, '.ml-counter'), navCss = toStyleStr({ 'text-decoration': 'none', 'color': 'white', 'background-color': '#444', 'padding': '3px 10px', 'border-radius': '5px', 'transition': '250ms' }, '.ml-chap-nav a'), navHoverCss = toStyleStr({ 'background-color': '#555' }, '.ml-chap-nav a:hover'), boxCss = toStyleStr({ 'position': 'fixed', 'background-color': '#222', 'color': 'white', 'padding': '7px', 'border-top-left-radius': '5px', 'cursor': 'default' }, '.ml-box'), statsCss = toStyleStr({ 'bottom': '0', 'right': '0', 'opacity': '0.4', 'transition': '250ms' }, '.ml-stats'), statsCollapseCss = toStyleStr({ 'color': 'orange', 'cursor': 'pointer' }, '.ml-stats-collapse'), statsHoverCss = toStyleStr({ 'opacity': '1' }, '.ml-stats:hover'), floatingMsgCss = toStyleStr({ 'bottom': '50px', 'right': '0', 'border-bottom-left-radius': '5px', 'text-align': 'left', 'font': 'inherit', 'max-width': '95%', 'white-space': 'pre-wrap' }, '.ml-floating-msg'), buttonCss = toStyleStr({ 'cursor': 'pointer' }, '.ml-button'), keySettingCss = toStyleStr({ 'width': '35px' }, '.ml-setting-key input'), autoloadSettingCss = toStyleStr({ 'vertical-align': 'middle' }, '.ml-setting-autoload'); // clear all styles and scripts var title = document.title; document.head.innerHTML = ''; document.title = title; // navigation var nav = '
' + (prevChapter ? 'Prev Chapter ' : '') + 'Exit ' + (nextChapter ? 'Next Chapter' : '') + '
'; // message area var floatingMsg = '
';
  // stats
  var stats = '
>>
'; // combine ui elements document.body.innerHTML = nav + '
' + nav + floatingMsg + stats; // add main styles addStyle('main', true, viewerCss, imagesCss, imageCss, counterCss, navCss, navHoverCss, statsCss, statsCollapseCss, statsHoverCss, boxCss, floatingMsgCss, buttonCss, keySettingCss, autoloadSettingCss); // add user styles var userCss = (storeGet('ml-setting-css') || ''); addStyle('user', true, userCss); // set up return UI object var UI = { images: getEl('.ml-images'), statsContent: getEl('.ml-stats-content'), statsPages: getEl('.ml-stats-pages'), statsCollapse: getEl('.ml-stats-collapse'), btnManualReload: getEl('.ml-manual-reload'), btnInfo: getEl('.ml-info-button'), floatingMsg: getEl('.ml-floating-msg'), btnNextChap: getEl('.ml-chap-next'), btnPrevChap: getEl('.ml-chap-prev'), btnExit: getEl('.ml-exit'), btnSettings: getEl('.ml-settings-button'), isTyping: false, ignore: false }; // message func var messageId = null; var showFloatingMsg = function(msg, timeout, html) { clearTimeout(messageId); log(msg); if(html) { UI.floatingMsg.innerHTML = msg; } else { UI.floatingMsg.textContent = msg; } UI.floatingMsg.style.display = msg ? '' : 'none'; if(timeout) { messageId = setTimeout(function() { showFloatingMsg(''); }, timeout); } }; // configure initial state UI.floatingMsg.style.display = 'none'; // set up listeners document.addEventListener('click', function(evt) { if (evt.target.nodeName === 'A') { if(evt.target.className.indexOf('ml-chap') !== -1) { log('next chapter will autoload'); storeSet('autoload', 'yes'); } else if(evt.target.className.indexOf('ml-exit') !== -1) { log('exiting chapter, stop autoload'); storeSet('autoload', 'no'); } } }); UI.btnManualReload.addEventListener('click', function(evt) { var imgClick = function(e) { var target = e.target; UI.images.removeEventListener('click', imgClick, false); UI.images.style.cursor = ''; if(target.nodeName === 'IMG' && target.parentNode.className === 'ml-images') { showFloatingMsg(''); if(!target.title) { showFloatingMsg('Reloading "' + target.src + '"', 3000); if(target.complete) target.onload = null; target.src = target.src + (target.src.indexOf('?') !== -1 ? '&' : '?') + new Date().getTime(); } } else { showFloatingMsg('Cancelled manual reload...', 3000); } }; showFloatingMsg('Left click the image you would like to reload.\nClick on the page margin to cancel.'); UI.images.style.cursor = 'pointer'; UI.images.addEventListener('click', imgClick, false); }); UI.statsCollapse.addEventListener('click', function(evt) { var test = UI.statsCollapse.textContent === '>>'; UI.statsContent.style.display = test ? 'none' : ''; UI.statsCollapse.textContent = test ? '<<' : '>>'; }); UI.floatingMsg.addEventListener('focus', function(evt) { var target = evt.target; if(target.dataset.ignore) UI.ignore = true; if((target.nodeName === 'INPUT' && target.type === 'text') || target.nodeName === 'TEXTAREA') UI.isTyping = true; }, true); UI.floatingMsg.addEventListener('blur', function(evt) { var target = evt.target; if(target.dataset.ignore) UI.ignore = false; if((target.nodeName === 'INPUT' && target.type === 'text') || target.nodeName === 'TEXTAREA') UI.isTyping = false; }, true); UI.btnInfo.addEventListener('click', function(evt) { if(UI.floatingMsg.textContent) { showFloatingMsg(''); } else { showFloatingMsg('Information:\n' + 'IMPORTANT: The script has been updated to exclude NSFW sites\n' + 'in order to gain access to that functionality you\'ll have to install the following addon script.\n' + 'https://sleazyfork.org/en/scripts/12657-manga-loader-nsfw\n\n' + 'New feature! You can now define custom CSS in the new settings panel (accessible through the gear icon at the bottom left).\n' + 'The CSS will be saved and reapplied each time the script loads. You can change the background color of the page,\nthe width of the images and pretty much anything else.\n' + '\n' + 'Keybindings:\n' + 'Z - previous chapter\n' + 'X - exit\n' + 'C - next chapter\n' + 'W - scroll up\n' + 'S - scroll down\n' + 'Click the info button again to close this message.'); } }); UI.btnSettings.addEventListener('click', function() { if(UI.floatingMsg.textContent) { showFloatingMsg(''); } else { // start grid and first column var settings = ''; }).join(''); settings += 'Keybindings:
'; // Custom CSS settings += 'CSS (custom css for Manga Loader):
' + '

'; // start new column settings += '
'; // Keybindings var keyTableHtml = Object.keys(UI.keys).map(function(action) { return '
' + action + '
' + keyTableHtml + '

'; // Autoload settings += 'Auto-load:

'; // Load all or just N pages settings += "# of pages to load:
" + 'Type "all" to load all
default is 10
' + '

'; // close grid and column settings += ''; // Save button settings += ' '; showFloatingMsg(settings, null, true); // handle keybinding detection getEl('.ml-setting-key').onkeydown = function(e) { var target = e.target; if(target.nodeName.toUpperCase() === 'INPUT') { e.preventDefault(); e.stopPropagation(); target.value = e.keyCode || e.which; } }; // handle save button getEl('.ml-setting-save', UI.floatingMsg).onclick = function() { // persist css var css = getEl('.ml-setting-css', UI.floatingMsg).value.trim(); addStyle('user', true, css); storeSet('ml-setting-css', css); // keybindings getEls('.ml-setting-key input').forEach(function(input) { UI.keys[input.dataset.key] = parseInt(input.value); }); storeSet('ml-setting-key', UI.keys); // autoload storeSet('mAutoload', getEl('.ml-setting-autoload').checked); // loadnum var loadnum = getEl('.ml-setting-loadnum').value; mLoadNum = getEl('.ml-setting-loadnum').value = loadnum.toLowerCase() === 'all' ? 'all' : (parseInt(loadnum) || 10); storeSet('mLoadNum', mLoadNum); // flash notify var flash = getEl('.ml-setting-save-flash'); flash.textContent = 'Saved!'; setTimeout(function() { flash.textContent = ''; }, 1000); }; } }); // keybindings UI.keys = { PREV_CHAP: 90, EXIT: 88, NEXT_CHAP: 67, SCROLL_UP: 87, SCROLL_DOWN: 83 }; UI.scrollAmt = 50; // override the defaults with the user defined ones updateObj(UI.keys, storeGet('ml-setting-key') || {}); UI._keys = {}; Object.keys(UI.keys).forEach(function(action) { UI._keys[UI.keys[action]] = action; }); window.addEventListener('keydown', function(evt) { // ignore keybindings when text input is focused if(UI.isTyping) { if(!UI.ignore) evt.stopPropagation(); return; } // perform action switch(evt.keyCode) { case UI.keys.PREV_CHAP: if(UI.btnPrevChap) { UI.btnPrevChap.click(); } break; case UI.keys.EXIT: UI.btnExit.click(); break; case UI.keys.NEXT_CHAP: if(UI.btnNextChap) { UI.btnNextChap.click(); } break; case UI.keys.SCROLL_UP: window.scrollBy(0, -UI.scrollAmt); break; case UI.keys.SCROLL_DOWN: window.scrollBy(0, UI.scrollAmt); break; default: evt.stopPropagation(); break; } }, true); return UI; }; var getCounter = function(imgNum) { var counter = document.createElement('div'); counter.classList.add('ml-counter'); counter.textContent = imgNum; return counter; }; var addImage = function(src, loc, imgNum, callback) { var image = new Image(), counter = getCounter(imgNum); image.onerror = function() { log('failed to load ' + src); image.onload = null; image.style.backgroundColor = 'white'; image.style.cursor = 'pointer'; image.title = 'Reload?'; image.src = IMAGES.refresh_large; image.onclick = function() { image.onload = callback; image.title = ''; image.style.cursor = ''; image.src = src; }; }; image.onload = callback; image.src = src; loc.appendChild(image); loc.appendChild(counter); }; var loadManga = function(imp) { var ex = extractInfo.bind(imp), imgUrl = ex('img', imp.imgmod), 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(), addAndLoad = function(img, next) { if(!img) throw new Error('failed to retrieve img for page ' + curPage); updateStats(); addImage(img, UI.images, curPage, function() { pagesLoaded += 1; updateStats(); }); if(!next) throw new Error('failed to retrieve next url for page ' + curPage); loadNextPage(next); }, updateStats = function() { UI.statsPages.textContent = ' ' + pagesLoaded + '/' + curPage + ' loaded' + (numPages ? ', ' + numPages + ' total' : ''); }, getPageInfo = function() { var page = d.body; d.body.innerHTML = xhr.response; try { // find image and link to next page addAndLoad(ex('img', imp.imgmod, page), ex('next', null, page)); } catch (e) { log(e); log('error getting details from next page, assuming end of chapter.'); } }, loadNextPage = function(url) { if (mLoadNum !== 'all' && count % mLoadNum === 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 + 1 > numPages) { log('reached "numPages" ' + numPages + ', assuming end of chapter'); return; } if (lastUrl === url) { log('last url (' + lastUrl + ') is the same as current (' + url + '), assuming end of chapter'); return; } curPage += 1; count += 1; lastUrl = url; if (imp.pages) { imp.pages(url, curPage, addAndLoad, ex, getPageInfo); } else { var colonIdx = url.indexOf(':'); if(colonIdx > -1) url = url.slice(colonIdx + 1); url = location.protocol + url; xhr.open('get', url); imp.beforexhr && imp.beforexhr(xhr); xhr.onload = getPageInfo; xhr.onerror = function() { log('failed to load page, aborting', 'error'); }; xhr.send(); } }, count = 1, pagesLoaded = curPage - 1, lastUrl, UI, resumeUrl; if (!imgUrl || !nextUrl) { log('failed to retrieve ' + (!imgUrl ? 'image url' : 'next page url'), 'exit'); } // do some checks on the chapter urls nextChapter = (nextChapter && nextChapter.trim() === location.href + '#' ? null : nextChapter); prevChapter = (prevChapter && prevChapter.trim() === location.href + '#' ? null : prevChapter); UI = getViewer(prevChapter, nextChapter); UI.statsPages.textContent = ' 0/1 loaded, ' + numPages + ' total'; if (mLoadNum !== 'all') { window.addEventListener('scroll', throttle(function(e) { if (!resumeUrl) return; // exit early if we don't have a position to resume at if(!UI.imageHeight) { UI.imageHeight = getEl('.ml-images img').clientHeight; } var scrollBottom = document.body.scrollHeight - ((document.body.scrollTop || document.documentElement.scrollTop) + window.innerHeight); if (scrollBottom < UI.imageHeight * 2) { log('user scroll nearing end, loading more images starting from ' + resumeUrl); loadNextPage(resumeUrl); } }, 100)); } addAndLoad(imgUrl, nextUrl); }; W.MLoaderLoadImps = function(imps) { var success = imps.some(function(imp) { var intervalId; var waitAndLoad = function() { if (typeof imp.wait === 'function') { log('Waiting for load condition to be fulfilled'); intervalId = setInterval(function() { if (imp.wait()) { log('Condition fulfilled, loading'); clearInterval(intervalId); loadManga(imp); } }, 200); } else if(typeof imp.wait === 'string') { log('Waiting for load condition to be fulfilled'); intervalId = setInterval(function() { if (getEl(imp.wait)) { log('Condition fulfilled, loading'); clearInterval(intervalId); loadManga(imp); } }, 200); } else { setTimeout(loadManga.bind(null, imp), imp.wait || 0); } }; if (imp.match && (new RegExp(imp.match, 'i')).test(pageUrl)) { currentImpName = imp.name; if (autoload !== 'no' && (BM_MODE || mAutoload || autoload)) { log('autoloading...'); waitAndLoad(); return true; } // append button to dom that will trigger the page load btnLoad = createButton('Load Manga', function(evt) { waitAndLoad(); this.remove(); }, btnLoadCss); document.body.appendChild(btnLoad); return true; } }); if (!success) { log('no implementation for ' + pageUrl, 'error'); } }; var pageUrl = window.location.href, btnLoadCss = toStyleStr({ 'position': 'fixed', 'bottom': 0, 'right': 0, 'padding': '5px', 'margin': '0 10px 10px 0', 'z-index': '99999' }), currentImpName, 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 mLoadNum = storeGet('mLoadNum') || 10; // clear autoload storeDel('autoload'); log('starting...'); W.MLoaderLoadImps(implementations);