Greasy Fork is available in English.

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

נכון ליום 07-10-2014. ראה הגרסה האחרונה.

  1. // ==UserScript==
  2. // @name Manga Loader
  3. // @namespace http://www.fuzetsu.com/MangaLoader
  4. // @version 1.3.0
  5. // @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
  6. // @copyright 2014+, fuzetsu
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @grant GM_deleteValue
  10. // @grant GM_registerMenuCommand
  11. // @match http://bato.to/read/*
  12. // @match http://mangafox.me/manga/*/*/*
  13. // @match http://readms.com/r/*/*/*/*
  14. // @match http://g.e-hentai.org/s/*/*
  15. // @match http://exhentai.org/s/*/*
  16. // @match http://www.fakku.net/*/*/read*
  17. // @match http://www.mangareader.net/*/*
  18. // @match http://www.mangahere.co/manga/*/*
  19. // @match http://www.mangapanda.com/*/*
  20. // @match http://mangadeer.com/manga/*/*/*/*
  21. // @match http://mngacow.com/*/*
  22. // ==/UserScript==
  23.  
  24. // set to true for manga load without prompt
  25. var BM_MODE = false;
  26.  
  27. var scriptName = 'Manga Loader';
  28.  
  29. /**
  30. Sample Implementation:
  31. {
  32. match: "http://domain.com/.*" // the url to react to for manga loading
  33. , img: '#image' // css selector to get the page's manga image
  34. , next: '#next_page' // css selector to get the link to the next page
  35. , numpages: '#page_select' // css selector to get the number of pages. elements like (select, span, etc)
  36. , curpage: '#page_select' // css selector to get the current page. usually the same as numPages if it's a select element
  37. , nextchap: '#next_chap' // css selector to get the link to the next chapter
  38. , prevchap: '#prev_chap' // same as above except for previous
  39. , wait: 3000 // how many ms to wait before auto loading (to wait for elements to load)
  40. , pages: function(next_url, current_page_number, callback, extract_function) {
  41. // gets called requesting a certain page number (current_page_number)
  42. // to continue loading execute callback with img to append as first parameter and next url as second parameter
  43. // only really needs to be used on sites that have really unusual ways of loading images or depend on javascript
  44. }
  45.  
  46. Any of the CSS selectors can be functions instead that return the desired value.
  47. }
  48. */
  49.  
  50. var implementations = [
  51. { // Batoto
  52. match: "http://bato.to/read/.*"
  53. , img: '#comic_page'
  54. , next: '#full_image + div > a'
  55. , numpages: '#page_select'
  56. , curpage: '#page_select'
  57. , nextchap: 'select[name=chapter_select]'
  58. , prevchap: 'select[name=chapter_select]'
  59. , invchap: true
  60. }
  61. , { // MangaPanda
  62. match: "http://www.mangapanda.com/.*/[0-9]*"
  63. , img: '#img'
  64. , next: '.next a'
  65. , numpages: '#pageMenu'
  66. , curpage: '#pageMenu'
  67. , nextchap: 'td.c5 + td a'
  68. , prevchap: 'table.c6 tr:last-child td:last-child a'
  69. }
  70. , { // MangaFox
  71. match: "http://mangafox.me/manga/.*/.*/.*"
  72. , img: '#image'
  73. , next: '.next_page'
  74. , numpages: 'select.m'
  75. , curpage: 'select.m'
  76. , nextchap: '#chnav p + p a'
  77. , prevchap: '#chnav a'
  78. }
  79. , { // MangaStream
  80. match: "http://readms.com/r/.*/.*/.*"
  81. , img: '#manga-page'
  82. , next: '.next a'
  83. , numpages: function() {
  84. var lastPage = getEl('.subnav-wrapper .controls .btn-group:last-child ul li:last-child');
  85. return parseInt(lastPage.textContent.match(/[0-9]/g).join(''), 10);
  86. }
  87. , nextchap: function(prev) {
  88. var found;
  89. var chapters = [].slice.call(document.querySelectorAll('.controls > div:first-child > .dropdown-menu > li a'));
  90. chapters.pop();
  91. for(var i = 0; i < chapters.length; i++) {
  92. if(window.location.href.indexOf(chapters[i].href) !== -1) {
  93. found = chapters[i + (prev ? 1 : -1)];
  94. if(found) return found.href;
  95. }
  96. }
  97. }
  98. , prevchap: function() {
  99. return this.nextchap(true);
  100. }
  101. }
  102. , { // MangaReader
  103. match: "http://www.mangareader.net/.*/.*"
  104. , img:'#img'
  105. , next: '.next a'
  106. , numpages: '#pageMenu'
  107. , curpage: '#pageMenu'
  108. , nextchap: 'td.c5 + td a'
  109. , prevchap: 'table.c6 tr:last-child td:last-child a'
  110. }
  111. , { // MangaCow
  112. match: "^http://mngacow\.com/.*/[0-9]*"
  113. , img: '.prw > a > img'
  114. , next: '.prw > a:last-child'
  115. , numpages: 'select.cbo_wpm_pag'
  116. , curpage: 'select.cbo_wpm_pag'
  117. , nextchap: function(prev) {
  118. var chapSel = getEl('select.cbo_wpm_chp');
  119. var nextChap = chapSel.options[chapSel.selectedIndex + (prev ? 1 : -1)];
  120. if(nextChap) {
  121. return 'http://mngacow.com/' + window.location.pathname.slice(1, window.location.pathname.slice(1).indexOf('/') + 2) + nextChap.value;
  122. }
  123. }
  124. , prevchap: function() {
  125. return this.nextchap(true);
  126. }
  127. }
  128. , { // MangaHere
  129. match: "^http://www.mangahere.co/manga/.*/.*"
  130. , img: '#viewer img'
  131. , next: '#viewer a'
  132. , numpages: 'select.wid60'
  133. , curpage: 'select.wid60'
  134. , nextchap: '.reader_tip > p:nth-last-child(2) > a'
  135. , prevchap: '.reader_tip > p:nth-last-child(1) > a'
  136. }
  137. , { // MangaDeer
  138. match: "^http://mangadeer\.com/manga/.*"
  139. , img: '.img-link > img'
  140. , next: '.page > span:last-child > a'
  141. , numpages: '#sel_page_1'
  142. , curpage: '#sel_page_1'
  143. , nextchap: function(prev) {
  144. var ddl = getEl('#sel_book_1');
  145. var index = ddl.selectedIndex + (prev ? -1 : 1);
  146. if(index >= ddl.options.length) return;
  147. var mangaName = window.location.href.slice(window.location.href.indexOf('manga/') + 6);
  148. mangaName = mangaName.slice(0, mangaName.indexOf('/'));
  149. return 'http://mangadeer.com/manga/' + mangaName + ddl.options[index].value + '/1';
  150. }
  151. , prevchap: function() {
  152. return this.nextchap(true);
  153. }
  154. }
  155. , { // GEH/EXH
  156. match: "http://(g.e-hentai|exhentai).org/s/.*/.*"
  157. , img: '.sni > a > img, #img'
  158. , next: '.sni > a, #i3 a'
  159. }
  160. , { // Fakku
  161. match: "^http://www.fakku.net/.*/.*/read"
  162. , img: '.current-page'
  163. , next: '.current-page'
  164. , numpages: '.drop'
  165. , curpage: '.drop'
  166. , pages: function(url, num, cb, ex) {
  167. var firstNum = url.lastIndexOf('/')
  168. , lastDot = url.lastIndexOf('.');
  169. var c = url.charAt(firstNum);
  170. while(c && !/[0-9]/.test(c)) {
  171. c = url.charAt(++firstNum);
  172. }
  173. var curPage = parseInt(url.slice(firstNum, lastDot), 10);
  174. var url = url.slice(0, firstNum) + ('00' + (curPage + 1)).slice(-3) + url.slice(lastDot);
  175. cb(url, url);
  176. }
  177. }
  178. ];
  179.  
  180. var log = function(msg, type) {
  181. type = type || 'log';
  182. if(type === 'exit') {
  183. throw scriptName + ' exit: ' + msg;
  184. } else {
  185. console[type](scriptName + ' ' + type + ': ', msg);
  186. }
  187. };
  188.  
  189. var getEl = function(q, c) {
  190. if(!q) return;
  191. return (c || document).querySelector(q);
  192. };
  193.  
  194. var storeGet = function(key) {
  195. if(typeof GM_getValue === "undefined") {
  196. var value = localStorage.getItem(key);
  197. if(value === "true" || value === "false") {
  198. return (value === "true") ? true : false;
  199. }
  200. return value;
  201. }
  202. return GM_getValue(key);
  203. };
  204.  
  205. var storeSet = function(key, value) {
  206. if(typeof GM_setValue === "undefined") {
  207. return localStorage.setItem(key, value);
  208. }
  209. return GM_setValue(key, value);
  210. };
  211.  
  212. var storeDel = function(key) {
  213. if(typeof GM_deleteValue === "undefined") {
  214. return localStorage.removeItem(key);
  215. }
  216. return GM_deleteValue(key);
  217. };
  218.  
  219. var extractInfo = function(selector, mod, context) {
  220. selector = this[selector];
  221. if(typeof selector === 'function') {
  222. return selector.call(this);
  223. }
  224. var elem = getEl(selector, context)
  225. , option;
  226. mod = mod || {};
  227. if(elem) {
  228. switch (elem.nodeName.toLowerCase()) {
  229. case 'img':
  230. return elem.getAttribute('src');
  231. case 'a':
  232. return elem.getAttribute('href');
  233. case 'ul':
  234. return elem.children.length;
  235. case 'select':
  236. switch(mod.type) {
  237. case 'index':
  238. return elem.options.selectedIndex;
  239. case 'value':
  240. option = elem.options[elem.options.selectedIndex + (mod.val || 0)] || {};
  241. return option.value;
  242. default:
  243. return elem.options.length;
  244. }
  245. break;
  246. default:
  247. return elem.textContent;
  248. }
  249. }
  250. };
  251.  
  252. var toStyleStr = function(obj) {
  253. var stack = []
  254. , key;
  255. for(key in obj) {
  256. if(obj.hasOwnProperty(key)) {
  257. stack.push(key + ':' + obj[key]);
  258. }
  259. }
  260. return stack.join(';');
  261. };
  262.  
  263. var throttle = function(callback, limit) {
  264. var wait = false;
  265. return function() {
  266. if(!wait) {
  267. callback();
  268. wait = true;
  269. setTimeout(function() {
  270. wait = false;
  271. }, limit);
  272. }
  273. };
  274. };
  275.  
  276. var createButton = function(text, action, styleStr) {
  277. var button = document.createElement('button');
  278. button.textContent = text;
  279. button.onclick = action;
  280. button.setAttribute('style', styleStr || '');
  281. return button;
  282. };
  283.  
  284. var getViewer = function(prevChapter, nextChapter) {
  285. var viewerCss = toStyleStr({
  286. 'background-color': 'black'
  287. , 'text-align': 'center'
  288. , 'font': '.9em sans-serif'
  289. })
  290. , imagesCss = toStyleStr({
  291. 'margin': '5px 0'
  292. })
  293. , navCss = toStyleStr({
  294. 'text-decoration': 'none'
  295. , 'color': 'black'
  296. , 'background': 'linear-gradient(white, #ccc)'
  297. , 'padding': '3px 10px'
  298. , 'border': '1px solid #ccc'
  299. , 'border-radius': '5px'
  300. })
  301. ;
  302. // clear all styles and scripts
  303. var title = document.title;
  304. document.head.innerHTML = '';
  305. document.title = title;
  306. // and navigation
  307. var nav = (prevChapter ? '<a href="' + prevChapter + '" style="' + navCss + '" class="ml-chap-nav">Prev Chapter</a> ' : '') +
  308. (storeGet('mAutoload') ? '' : '<a href="" style="' + navCss + '">Exit</a> ') +
  309. (nextChapter ? '<a href="' + nextChapter + '" style="' + navCss + '" class="ml-chap-nav">Next Chapter</a>' : '');
  310. document.body.innerHTML = nav + '<div id="images" style="' + imagesCss + '"></div>' + nav;
  311. // set the viewer css
  312. document.body.setAttribute('style', viewerCss);
  313. // set up listeners for chapter navigation
  314. document.addEventListener('click', function(evt) {
  315. if(evt.target.className.indexOf('ml-chap-nav') !== -1) {
  316. log('next chapter will autoload');
  317. storeSet('autoload', 'yes');
  318. }
  319. }, false);
  320. return getEl('#images');
  321. };
  322.  
  323. var imageCss = toStyleStr({
  324. 'max-width': '100%'
  325. , 'display': 'block'
  326. , 'margin': '3px auto'
  327. });
  328.  
  329. var addImage = function(src, loc, callback) {
  330. var image = new Image();
  331. image.onerror = function() {
  332. log('failed to load ' + src);
  333. image.remove();
  334. };
  335. image.onload = callback;
  336. image.src = src;
  337. image.setAttribute('style', imageCss);
  338. loc.appendChild(image);
  339. };
  340.  
  341. var loadManga = function(imp) {
  342. var ex = extractInfo.bind(imp)
  343. , imgUrl = ex('img')
  344. , nextUrl = ex('next')
  345. , numPages = ex('numpages')
  346. , curPage = ex('curpage', {type:'index'}) + 1 || 1
  347. , nextChapter = ex('nextchap', {type:'value', val: (imp.invchap && -1) || 1})
  348. , prevChapter = ex('prevchap', {type:'value', val: (imp.invchap && 1) || -1})
  349. , xhr = new XMLHttpRequest()
  350. , d = document.implementation.createHTMLDocument()
  351. , addAndLoad = function(img, next) {
  352. addImage(img, loc);
  353. loadNextPage(next);
  354. }
  355. , getPageInfo = function() {
  356. var page = d.body;
  357. d.body.innerHTML = xhr.response;
  358. try {
  359. // find image and link to next page
  360. addAndLoad(ex('img', null, page), ex('next', null, page));
  361. } catch(e) {
  362. log('error getting details from next page, assuming end of chapter.');
  363. }
  364. }
  365. , loadNextPage = function(url) {
  366. if(mLoadLess && count % 10 === 0) {
  367. if(resumeUrl) {
  368. resumeUrl = null;
  369. } else {
  370. resumeUrl = url;
  371. log('waiting for user to scroll further before loading more images, loaded ' + count + ' pages so far, next url is ' + resumeUrl);
  372. return;
  373. }
  374. }
  375. if(++curPage > numPages) {
  376. log('reached "numPages" ' + numPages + ', assuming end of chapter');
  377. return;
  378. }
  379. if(lastUrl === url) {
  380. log('last url is the same as current, assuming end of chapter');
  381. return;
  382. }
  383. lastUrl = url;
  384. if(imp.pages) {
  385. imp.pages(url, curPage, addAndLoad, ex);
  386. } else {
  387. xhr.open('get', url);
  388. xhr.onload = getPageInfo;
  389. xhr.onerror = function() {
  390. log('failed to load page, aborting', 'error');
  391. };
  392. xhr.send();
  393. }
  394. count += 1;
  395. }
  396. , count = 1
  397. , lastUrl, loc, resumeUrl
  398. ;
  399.  
  400. if(!imgUrl || !nextUrl) {
  401. log('failed to retrieve ' + (!imgUrl ? 'image url' : 'next page url'), 'exit');
  402. }
  403.  
  404. loc = getViewer(prevChapter, nextChapter);
  405. if(mLoadLess) {
  406. window.onscroll = throttle(function(e) {
  407. if(!resumeUrl) return; // exit early if we don't have a position to resume at
  408. var scrollBottom = document.body.scrollHeight - ((document.body.scrollTop || document.documentElement.scrollTop) + window.innerHeight);
  409. if(scrollBottom < 4500) {
  410. log('user scroll nearing end, loading more images starting from ' + resumeUrl);
  411. loadNextPage(resumeUrl);
  412. }
  413. }, 100);
  414. }
  415.  
  416. addImage(imgUrl, loc);
  417. loadNextPage(nextUrl);
  418.  
  419. };
  420.  
  421. var pageUrl = window.location.href
  422. , btnLoadCss = toStyleStr({
  423. 'position': 'fixed'
  424. , 'bottom': 0
  425. , 'right': 0
  426. , 'padding': '5px'
  427. , 'margin': '0 10px 10px 0'
  428. , 'z-index': '1000'
  429. })
  430. , btnLoad
  431. ;
  432.  
  433. // used when switching chapters
  434. var autoload = storeGet('autoload');
  435. // manually set by user in menu
  436. var mAutoload = storeGet('mAutoload') || false;
  437. // should we load less pages at a time?
  438. var mLoadLess = storeGet('mLoadLess') || false;
  439. // clear autoload
  440. storeDel('autoload');
  441.  
  442. // register menu commands
  443. if(typeof GM_registerMenuCommand === 'function') {
  444. GM_registerMenuCommand('ML: ' + (mAutoload ? 'Disable' : 'Enable') + ' manga autoload', function() {
  445. storeSet('mAutoload', !mAutoload);
  446. window.location.reload();
  447. });
  448. GM_registerMenuCommand('ML: Load ' + (mLoadLess ? 'full chapter in one go' : '10 pages at a time'), function() {
  449. storeSet('mLoadLess', !mLoadLess);
  450. window.location.reload();
  451. });
  452. }
  453.  
  454. log('starting...');
  455.  
  456. var success = implementations.some(function(imp) {
  457. if(imp.match && (new RegExp(imp.match, 'i')).test(pageUrl)) {
  458. if(BM_MODE || mAutoload || autoload) {
  459. setTimeout(loadManga.bind(null, imp),imp.wait || 0);
  460. return true;
  461. }
  462. // append button to dom that will trigger the page load
  463. btnLoad = createButton('Load Manga', function(evt) {
  464. loadManga(imp);
  465. this.remove();
  466. }, btnLoadCss);
  467. document.body.appendChild(btnLoad);
  468. return true;
  469. }
  470. });
  471.  
  472. if(!success) {
  473. log('no implementation for ' + pageUrl, 'error');
  474. }