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, loads quickly and works on mobile devices through bookmarklet

As of 2015-10-02. See the latest version.

  1. // ==UserScript==
  2. // @name Manga Loader
  3. // @namespace http://www.fuzetsu.com/MangaLoader
  4. // @version 1.8.11
  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, loads quickly and works on mobile devices through bookmarklet
  6. // @copyright 2014+, fuzetsu
  7. // @noframes
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_deleteValue
  11. // @grant GM_registerMenuCommand
  12. // @match *://bato.to/read/*
  13. // @match http://mangafox.me/manga/*/*/*
  14. // @match http://readms.com/r/*/*/*/*
  15. // @match http://mangastream.com/read/*/*/*/*
  16. // @match http://www.mangareader.net/*/*
  17. // @match http://www.mangahere.co/manga/*/*
  18. // @match http://www.mangapanda.com/*/*
  19. // @match http://mangapark.me/manga/*/*/*/*
  20. // @match http://mangacow.co/*/*
  21. // @match http://centraldemangas.org/online/*/*
  22. // @match http://*.com.br/leitura/online/capitulo/*
  23. // @match http://www.mangatown.com/manga/*/*
  24. // @match http://manga-joy.com/*/*
  25. // @match http://*.dm5.com/m*
  26. // @match http://*.senmanga.com/*/*/*
  27. // @match http://www.japscan.com/lecture-en-ligne/*
  28. // @match http://www.pecintakomik.com/manga/*/*
  29. // @match http://dynasty-scans.com/chapters/*
  30. // @match http://mangawall.com/manga/*/*
  31. // @match http://manga.animea.net/*
  32. // @match http://kissmanga.com/Manga/*/*
  33. // @match http://view.thespectrum.net/series/*
  34. // @match http://manhua.dmzj.com/*/*
  35. // @match http://hqbr.com.br/hqs/*/capitulo/*/leitor/0
  36. // @match http://www.dmzj.com/view/*/*
  37. // @match http://mangaindo.id/*/*
  38. // @match http://mangadoom.co/*/*
  39. // @match http://www.mangago.me/read-manga/*/*/*/*
  40. // @match http://mangalator.ch/show.php?gallery=*
  41. // @match http://eatmanga.com/Manga-Scan/*/*
  42. // @match http://www.mangacat.me/*/*/*
  43. // @match http://www.mangakaka.com/*/*
  44. // @match http://www.readmanga.today/*/*
  45. // @match *://mangatraders.org/read-online/*/*
  46. // @match http://www.mangainn.me/manga/chapter/*
  47. // @match http://*.kukudm.com/comiclist/*/*
  48. // @match http://www.mangamap.com/*/*
  49. // @match http://www.mangachapter.me/*/*/*.html
  50. // @match http://kawaii.ca/reader/*
  51. // @match http://lonemanga.com/manga/*/*
  52. // -- FOOLSLIDE START
  53. // @match http://manga.redhawkscans.com/reader/read/*
  54. // @match http://reader.s2smanga.com/read/*
  55. // @match http://casanovascans.com/read/*
  56. // @match http://reader.vortex-scans.com/read/*
  57. // @match http://reader.roseliascans.com/read/*
  58. // @match http://mangatopia.net/slide/read/*
  59. // @match http://www.twistedhelscans.com/read/*
  60. // @match http://reader.sensescans.com/reader/read/*
  61. // @match http://reader.kireicake.com/read/*
  62. // @match http://substitutescans.com/reader/read/*
  63. // @match http://mangaichiscans.mokkori.fr/fs/read/*
  64. // @match http://reader.shoujosense.com/read/*
  65. // @match http://www.friendshipscans.com/slide/read/*
  66. // @match http://manga.famatg.com/read/*
  67. // @match http://www.demonicscans.com/FoOlSlide/read/*
  68. // @match http://reader.psscans.info/read/*
  69. // @match http://onetimescans.com/foolslide/read/*
  70. // @match http://necron99scans.com/reader/read/*
  71. // @match http://manga.inpowerz.com/read/*
  72. // @match http://reader.evilflowers.com/read/*
  73. // @match http://reader.cafeconirst.com/read/*
  74. // @match http://kobato.hologfx.com/reader/read/*
  75. // -- FOOLSLIDE END
  76. // ==/UserScript==
  77.  
  78. // should be set to true externally if auto loading is wanted (e.g. bookmarklet)
  79. var BM_MODE;
  80.  
  81. // short reference to unsafeWindow (or window if unsafeWindow is unavailable e.g. bookmarklet)
  82. var W = (typeof unsafeWindow === 'undefined') ? window : unsafeWindow;
  83.  
  84. var scriptName = 'Manga Loader';
  85. var pageTitle = document.title;
  86.  
  87. var IMAGES = {
  88. refresh_large: '//rgeorgeeb-tilebuttongenerator.googlecode.com/hg-history/c35993682c2d50149976fd7a1f302f8c01a88716/asset-studio/src/res/clipart/icons/refresh.svg'
  89. };
  90.  
  91. // reusable functions to insert in implementations
  92. var reuse = {
  93. encodeChinese: function(xhr) {
  94. xhr.overrideMimeType('text/html;charset=gbk');
  95. }
  96. };
  97.  
  98. /**
  99. Sample Implementation:
  100. {
  101. name: 'something' // name of the implementation
  102. , match: "http://domain.com/.*" // the url to react to for manga loading
  103. , img: '#image' // css selector to get the page's manga image
  104. , next: '#next_page' // css selector to get the link to the next page
  105. , numpages: '#page_select' // css selector to get the number of pages. elements like (select, span, etc)
  106. , curpage: '#page_select' // css selector to get the current page. usually the same as numPages if it's a select element
  107. , nextchap: '#next_chap' // css selector to get the link to the next chapter
  108. , prevchap: '#prev_chap' // same as above except for previous
  109. , 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
  110. , pages: function(next_url, current_page_number, callback, extract_function) {
  111. // gets called requesting a certain page number (current_page_number)
  112. // to continue loading execute callback with img to append as first parameter and next url as second parameter
  113. // only really needs to be used on sites that have really unusual ways of loading images or depend on javascript
  114. }
  115.  
  116. Any of the CSS selectors can be functions instead that return the desired value.
  117. }
  118. */
  119.  
  120. var implementations = [{
  121. name: 'batoto',
  122. match: "^https?://bato.to/read/.*",
  123. img: function(ctx) {
  124. var img = getEl('#comic_page', ctx);
  125. if(img) {
  126. return img.src;
  127. } else {
  128. var imgs = getEls('#content > div:nth-child(8) > img', ctx).map(function(page) {
  129. return page.src;
  130. });
  131. if(imgs.length > 0) {
  132. this.next = function() { return imgs[0]; };
  133. this.numpages = function() { return imgs.length; };
  134. this.pages = function(url, num, cb, ex) {
  135. cb(imgs[num - 1], num);
  136. };
  137. return imgs[0];
  138. }
  139. }
  140. },
  141. next: '#full_image + div > a',
  142. numpages: '#page_select',
  143. curpage: '#page_select',
  144. nextchap: 'select[name=chapter_select]',
  145. prevchap: 'select[name=chapter_select]',
  146. invchap: true
  147. }, {
  148. name: 'manga-panda',
  149. match: "^http://www.mangapanda.com/.*/[0-9]*",
  150. img: '#img',
  151. next: '.next a',
  152. numpages: '#pageMenu',
  153. curpage: '#pageMenu',
  154. nextchap: 'td.c5 + td a',
  155. prevchap: 'table.c6 tr:last-child td:last-child a'
  156. }, {
  157. name: 'mangafox',
  158. match: "^http://mangafox.me/manga/[^/]*/[^/]*/[^/]*",
  159. img: '#image',
  160. next: 'a.next_page',
  161. numpages: function() {
  162. return extractInfo('select.m') - 1;
  163. },
  164. curpage: 'select.m',
  165. nextchap: '#chnav p + p a',
  166. prevchap: '#chnav a'
  167. }, {
  168. name: 'manga-stream',
  169. match: "^http://(readms|mangastream).com/(r|read)/[^/]*/[^/]*/[^/]*",
  170. img: '#manga-page',
  171. next: '.next a',
  172. numpages: function() {
  173. var lastPage = getEl('.subnav-wrapper .controls .btn-group:last-child ul li:last-child');
  174. return parseInt(lastPage.textContent.match(/[0-9]/g).join(''), 10);
  175. },
  176. nextchap: function(prev) {
  177. var found;
  178. var chapters = [].slice.call(document.querySelectorAll('.controls > div:first-child > .dropdown-menu > li a'));
  179. chapters.pop();
  180. for (var i = 0; i < chapters.length; i++) {
  181. if (window.location.href.indexOf(chapters[i].href) !== -1) {
  182. found = chapters[i + (prev ? 1 : -1)];
  183. if (found) return found.href;
  184. }
  185. }
  186. },
  187. prevchap: function() {
  188. return this.nextchap(true);
  189. }
  190. }, {
  191. name: 'manga-reader',
  192. match: "^http://www.mangareader.net/.*/.*",
  193. img: '#img',
  194. next: '.next a',
  195. numpages: '#pageMenu',
  196. curpage: '#pageMenu',
  197. nextchap: '#chapterMenu',
  198. prevchap: '#chapterMenu',
  199. wait: function() {
  200. return getEl('#chapterMenu option');
  201. }
  202. }, {
  203. name: 'manga-town',
  204. match: "^http://www.mangatown.com/manga/[^/]+/[^/]+",
  205. img: '#image',
  206. next: '#viewer a',
  207. numpages: '.page_select select',
  208. curpage: '.page_select select',
  209. nextchap: '#top_chapter_list',
  210. prevchap: '#top_chapter_list',
  211. wait: 1000
  212. }, {
  213. name: 'manga-cow, manga-doom, manga-indo',
  214. match: "^http://(mangacow|mangadoom|mangaindo)\\.(co|id)/[^/]+/[0-9.]+",
  215. img: '.prw a > img',
  216. next: '.prw a',
  217. numpages: 'select.cbo_wpm_pag',
  218. curpage: 'select.cbo_wpm_pag',
  219. nextchap: function(prev) {
  220. var next = extractInfo('select.cbo_wpm_chp', {type: 'value', val: (prev ? 1 : -1)});
  221. if(next) return window.location.href.replace(/\/[0-9.]+\/?([0-9]+\/?)?[^/]*$/, '/' + next);
  222. },
  223. prevchap: function() {
  224. return this.nextchap(true);
  225. }
  226. }, {
  227. name: 'manga-here',
  228. match: "^http://www.mangahere.co/manga/[^/]+/[^/]+",
  229. img: '#viewer img',
  230. next: '#viewer a',
  231. numpages: 'select.wid60',
  232. curpage: 'select.wid60',
  233. nextchap: function(prev) {
  234. var chapter = W.chapter_list[W.current_chapter_index + (prev ? -1 : 1)];
  235. return chapter && chapter[1];
  236. },
  237. prevchap: function() {
  238. return this.nextchap(true);
  239. },
  240. wait: function() {
  241. return areDefined(W.current_chapter_index, W.chapter_list);
  242. }
  243. }, {
  244. name: 'manga-park',
  245. match: "^http://mangapark\\.me/manga/[^/]+/[^/]+/[^/]+",
  246. img: '.img-link > img',
  247. next: '.page > span:last-child > a',
  248. numpages: '#sel_page_1',
  249. curpage: '#sel_page_1',
  250. nextchap: function(prev) {
  251. var next = extractInfo('#sel_book_1', {type: 'value', val: (prev ? -1 : 1)});
  252. if(next) return window.location.href.replace(/\/s[0-9.]+\/c[0-9.]+(\/[^\/]+)?$/, next + '/1');
  253. },
  254. prevchap: function() {
  255. return this.nextchap(true);
  256. }
  257. }, {
  258. name: 'central-de-mangas',
  259. match: "^http://(centraldemangas\\.org|[^\\.]+\\.com\\.br/leitura)/online/[^/]*/[0-9]*",
  260. img: '#manga-page',
  261. next: '#manga-page',
  262. numpages: '#manga_pages',
  263. curpage: '#manga_pages',
  264. nextchap: function(prev) {
  265. var next = extractInfo('#manga_caps', {type: 'value', val: (prev ? -1 : 1)});
  266. if(next) return window.location.href.replace(/[^\/]+$/, next);
  267. },
  268. prevchap: function() {
  269. return this.nextchap(true);
  270. },
  271. pages: function(url, num, cb, ex) {
  272. url = url.slice(0, url.lastIndexOf('-') + 1) + ("0" + num).slice(-2) + url.slice(url.lastIndexOf('.'));
  273. cb(url, url);
  274. }
  275. }, {
  276. name: 'manga-joy',
  277. match: "^http://manga-joy.com/[^/]*/[0-9]*",
  278. img: '.prw img',
  279. next: '.nxt',
  280. numpages: '.wpm_nav_rdr li:nth-child(3) > select',
  281. curpage: '.wpm_nav_rdr li:nth-child(3) > select',
  282. nextchap: function(prev) {
  283. var next = extractInfo('.wpm_nav_rdr li:nth-child(2) > select', {type: 'value', val: prev ? 1 : -1});
  284. if(next) return window.location.href.replace(/\/[0-9.]+\/?([0-9]+(\/.*)?)?$/, '/' + next);
  285. },
  286. prevchap: function() {
  287. return this.nextchap(true);
  288. }
  289. }, {
  290. name: 'dm5',
  291. match: "^http://[^\\.]*\\.dm5\\.com/m[0-9]*",
  292. img: '#cp_image',
  293. next: '#cp_image',
  294. numpages: '#pagelist',
  295. curpage: '#pagelist',
  296. pages: function(url, num, cb, ex) {
  297. var cid = window.location.href.match(/m[0-9]*/g)[2].slice(1),
  298. xhr = new XMLHttpRequest();
  299. xhr.open('get', 'chapterfun.ashx?cid=' + cid + '&page=' + num);
  300. xhr.onload = function() {
  301. var images = eval(xhr.responseText);
  302. cb(images[0], images[0]);
  303. };
  304. xhr.send();
  305. },
  306. nextchap: '.innr8>span:nth-child(even)>.redzia'
  307. }, {
  308. name: 'senmanga-raw',
  309. match: "^http://raw\\.senmanga\\.com/[^/]*/[^/]*/[0-9]*",
  310. img: function() {
  311. return W.new_image;
  312. },
  313. next: function() {
  314. return this.img();
  315. },
  316. numpages: 'select[name=page]',
  317. curpage: 'select[name=page]',
  318. nextchap: function(prev) {
  319. var next = extractInfo('select[name=chapter]', {type: 'value', val: (prev ? 1 : -1)});
  320. if(next) return window.location.href.replace(/\/[^\/]*\/[0-9]+\/?$/, '') + '/' + next + '/1';
  321. },
  322. prevchap: function() {
  323. return this.nextchap(true);
  324. },
  325. pages: function(url, num, cb, ex) {
  326. cb(W.new_image.replace(/page=[0-9]+/, 'page=' + num), num);
  327. }
  328. }, {
  329. name: 'japscan',
  330. match: "^http://www\\.japscan\\.com/lecture-en-ligne/[^/]*/[0-9]*",
  331. img: '#imgscan',
  332. next: '#next_link',
  333. numpages: '#pages',
  334. curpage: '#pages',
  335. nextchap: '#next_chapter',
  336. prevchap: '#back_chapter'
  337. }, { // pecintakomik
  338. match: "^http://www\\.pecintakomik\\.com/manga/[^/]*/[^/]*",
  339. img: '.picture',
  340. next: '.pager a:nth-child(3)',
  341. numpages: 'select[name=page]',
  342. curpage: 'select[name=page]',
  343. nextchap: function(prev) {
  344. var next = extractInfo('select[name=chapter]', {type: 'value', val: (prev ? 1 : -1)});
  345. if(next) return window.location.href.replace(/\/([^\/]+)\/[0-9]+\/?$/, '/$1/' + next);
  346. },
  347. prevchap: function() {
  348. return this.nextchap(true);
  349. }
  350. }, {
  351. name: 'dynasty-scans',
  352. match: "^http://dynasty-scans.com/chapters/.*",
  353. img: '#image > img',
  354. next: '#image > img',
  355. numpages: function() {
  356. return W.pages.length;
  357. },
  358. curpage: function() {
  359. return parseInt(getEl('#image > div.pages-list > a.page.active').textContent);
  360. },
  361. nextchap: '#next_link',
  362. prevchap: '#prev_link',
  363. pages: function(url, num, cb, ex) {
  364. url = W.pages[num - 1].image;
  365. cb(url, url);
  366. }
  367. }, {
  368. name: 'manga-kaka',
  369. match: "^http://www\\.(mangakaka|mangamap)\\.com/[^/]+/[0-9]+",
  370. img: 'img.manga-page',
  371. next: '.nav_pag > li:nth-child(1) > a',
  372. numpages: 'select.cbo_wpm_pag',
  373. curpage: 'select.cbo_wpm_pag',
  374. nextchap: function(prev) {
  375. var chapter = extractInfo('select.cbo_wpm_chp', { type: 'value', val: (prev ? 1 : -1) });
  376. if(chapter) return window.location.href.replace(/\/[0-9\.]+\/?([0-9]+\/?)?$/, '/' + chapter);
  377. },
  378. prevchap: function() {
  379. return this.nextchap(true);
  380. }
  381. }, {
  382. name: 'manga-wall',
  383. _page: null,
  384. match: "^http://mangawall\\.com/manga/[^/]*/[0-9]*",
  385. img: 'img.scan',
  386. next: function() {
  387. if(this._page === null) this._page = W.page;
  388. return W.series_url + '/' + W.chapter + '/' + (this._page += 1);
  389. },
  390. numpages: '.pageselect',
  391. curpage: '.pageselect',
  392. nextchap: function(prev) {
  393. return W.series_url + '/' + (parseInt(W.chapter.slice(1)) + (prev ? -1 : 1)) + '/1';
  394. },
  395. prevchap: function() {
  396. return this.nextchap(true);
  397. }
  398. }, {
  399. name: 'anime-a',
  400. _page: null,
  401. match: "^http://manga\\.animea.net/.+chapter-[0-9]+(-page-[0-9]+)?.html",
  402. img: '#scanmr',
  403. next: function() {
  404. if(this._page === null) this._page = W.page;
  405. return W.series_url + W.chapter + '-page-' + (this._page += 1) + '.html';
  406. },
  407. numpages: '.pageselect',
  408. curpage: '.pageselect',
  409. nextchap: function(prev) {
  410. return W.series_url + 'chapter-' + (parseInt(W.chapter.match(/[0-9]+/)[0]) + (prev ? -1 : 1)) + '-page-1.html';
  411. },
  412. prevchap: function() {
  413. return this.nextchap(true);
  414. }
  415. }, {
  416. name: 'kiss-manga',
  417. match: "^http://kissmanga\\.com/Manga/[^/]+/.+",
  418. img: '#divImage img',
  419. next: '#divImage img',
  420. numpages: function() {
  421. return W.lstImages.length;
  422. },
  423. curpage: function() {
  424. if(getEls('#divImage img').length > 1) {
  425. return 1;
  426. } else {
  427. return W.currImage + 1;
  428. }
  429. },
  430. nextchap: '#selectChapter, .selectChapter',
  431. prevchap: '#selectChapter, .selectChapter',
  432. pages: function(url, num, cb, ex) {
  433. cb(W.lstImages[num - 1], num);
  434. }
  435. }, {
  436. name: 'the-spectrum-scans',
  437. match: "^http://view\\.thespectrum\\.net/series/[^\\.]+\\.html\\?ch=[^&]+&page=[0-9]+",
  438. img: '#mainimage',
  439. next: '#mainimage',
  440. numpages: '.selectpage',
  441. curpage: '.selectpage',
  442. nextchap: function(prev) {
  443. var next = extractInfo('.selectchapter', {type: 'value', val: prev ? -1 : 1});
  444. if(next) return window.location.href.replace(/ch=.+/, 'ch=' + next + '&page=1');
  445. },
  446. prevchap: function() {
  447. return this.nextchap(true);
  448. },
  449. pages: function(url, num, cb, ex) {
  450. var numRegex = /([0-9]+)\.([a-z]+)$/;
  451. var curNum = url.match(numRegex);
  452. curNum = parseInt(curNum[1]) + 1;
  453. url = url.replace(numRegex, ('00' + curNum).slice(-3) + '.$2');
  454. cb(url, url);
  455. }
  456. }, {
  457. name: 'manhua-dmzj',
  458. match: "http://manhua.dmzj.com/[^/]*/[0-9]+(-[0-9]+)?\\.shtml",
  459. img: '.pic_link',
  460. next: '.pic_link',
  461. numpages: function() {
  462. return W.arr_pages.length;
  463. },
  464. curpage: function() {
  465. return W.COMIC_PAGE.getPageNumber();
  466. },
  467. nextchap: '#next_chapter',
  468. prevchap: '#prev_chapter',
  469. pages: function(url, num, cb, ex) {
  470. cb(W.img_prefix + W.arr_pages[num - 1], num);
  471. }
  472. }, {
  473. name: 'hqbr',
  474. match: "http://hqbr.com.br/hqs/[^/]+/capitulo/[0-9]+/leitor/0",
  475. img: '#hq-page',
  476. next: '#hq-page',
  477. numpages: function() {
  478. return W.pages.length;
  479. },
  480. curpage: function() {
  481. return W.paginaAtual + 1;
  482. },
  483. nextchap: function(prev) {
  484. var chapters = getEls('#chapter-dropdown a'),
  485. current = parseInt(W.capituloIndex),
  486. chapter = chapters[current + (prev ? -1 : 1)];
  487. return chapter && chapter.href;
  488. },
  489. prevchap: function() {
  490. return this.nextchap(true);
  491. },
  492. pages: function(url, num, cb, ex) {
  493. cb(W.pages[num - 1], num);
  494. }
  495. }, {
  496. name: 'dmzj',
  497. match: "http://www.dmzj.com/view/[^/]+/.+\\.html",
  498. img: '#pic',
  499. next: '#pic',
  500. numpages: '.select_jump',
  501. curpage: '.select_jump',
  502. nextchap: '.next > a',
  503. prevchap: '.pre > a',
  504. pages: function(url, num, cb, ex) {
  505. cb('http://images.dmzj.com/' + W.pic[num - 1], num);
  506. },
  507. wait: 1000
  508. }, {
  509. name: 'mangago',
  510. match: "http://www.mangago.me/read-manga/[^/]+/[^/]+/[^/]+",
  511. img: '#page1',
  512. next: '#pic_container',
  513. numpages: '#dropdown-menu-page',
  514. curpage: function() {
  515. return parseInt(getEls('#page-mainer a.btn.dropdown-toggle')[1].textContent.match(/[0-9]+/)[0]);
  516. },
  517. nextchap: function(prev) {
  518. var chapters = getEls('ul.dropdown-menu.chapter a'),
  519. curName = getEls('#page-mainer a.btn.dropdown-toggle')[0].textContent,
  520. curIdx;
  521. chapters.some(function(chap, idx) {
  522. if(chap.textContent.indexOf(curName) === 0) {
  523. curIdx = idx;
  524. return true;
  525. }
  526. });
  527. var chapter = chapters[curIdx + (prev ? 1 : -1)];
  528. return chapter && chapter.href;
  529. },
  530. prevchap: function() {
  531. return this.nextchap(true);
  532. }
  533. }, {
  534. name: 'mangalator',
  535. match: "http://mangalator.ch/show.php\\?gallery=[0-9]+",
  536. img: '.image img',
  537. next: '#next',
  538. numpages: 'select[name=image]',
  539. curpage: 'select[name=image]',
  540. nextchap: function(prev) {
  541. var next = extractInfo('select[name=gallery]', {type: 'value', val: (prev ? 1 : -1)});
  542. if(next) return location.href.replace(/\?gallery=[0-9]+/, '?gallery=' + next);
  543. },
  544. prevchap: function() {
  545. return this.nextchap(true);
  546. }
  547. }, {
  548. name: 'eatmanga',
  549. match: "http://eatmanga.com/Manga-Scan/[^/]+/.+",
  550. img: '#eatmanga_image, #eatmanga_image_big',
  551. next: '#page_next',
  552. numpages: '#pages',
  553. curpage: '#pages',
  554. nextchap: '#bottom_chapter_list',
  555. prevchap: '#bottom_chapter_list',
  556. invchap: true
  557. }, {
  558. name: 'manga-cat',
  559. match: "http://www.mangacat.me/[^/]+/[^/]+/[^\\.]+.html",
  560. img: '.img',
  561. next: '.img-link',
  562. numpages: '#page',
  563. curpage: '#page',
  564. nextchap: '.info a:nth-child(4)',
  565. prevchap: '.info a:nth-child(2)'
  566. }, {
  567. name: 'readmanga.today',
  568. match: "http://www\\.readmanga\\.today/[^/]+/.+",
  569. img: '.page_chapter img',
  570. next: '.list-switcher-2 > li:nth-child(3) > a, .list-switcher-2 > li:nth-child(2) > a',
  571. numpages: '.list-switcher-2 select[name=category_type]',
  572. curpage: '.list-switcher-2 select[name=category_type]',
  573. nextchap: '.jump-menu[name=chapter_list]',
  574. prevchap: '.jump-menu[name=chapter_list]',
  575. invchap: true
  576. }, {
  577. name: 'foolslide',
  578. 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/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/.+)",
  579. img: function() {
  580. return W.pages[W.current_page].url;
  581. },
  582. next: function() {
  583. return 'N/A';
  584. },
  585. numpages: function() {
  586. return W.pages.length;
  587. },
  588. curpage: function() {
  589. return W.current_page + 1;
  590. },
  591. nextchap: function(prev) {
  592. var desired;
  593. var dropdown = getEls('ul.dropdown')[1] || getEls('ul.uk-nav')[1];
  594. if(!dropdown) return;
  595. getEls('a', dropdown).forEach(function(chap, idx, arr) {
  596. if(location.href.indexOf(chap.href) === 0) desired = arr[idx + (prev ? 1 : -1)];
  597. });
  598. return desired && desired.href;
  599. },
  600. prevchap: function() {
  601. return this.nextchap(true);
  602. },
  603. pages: function(url, num, cb, ex) {
  604. cb(W.pages[num - 1].url, num);
  605. },
  606. wait: function() {
  607. return W.pages;
  608. }
  609. }, {
  610. name: 'mangatraders',
  611. match: "^https?://mangatraders\\.org/read-online/[^/]+/.+",
  612. img: function(ctx) {
  613. var imgs = getEls('img', ctx);
  614. var found;
  615. imgs.some(function(img) {
  616. if(img.src.indexOf('http://img') > -1) {
  617. found = img.src;
  618. return true;
  619. }
  620. });
  621. return found;
  622. },
  623. next: 'body > div.container.mainPageContainer > ol > li:nth-child(4) > a',
  624. numpages: '#changePageSelect',
  625. curpage: '#changePageSelect',
  626. nextchap: function(prev) {
  627. var next = extractInfo('#changeChapterSelect', {type:'value', val: (prev ? -1 : 1)});
  628. if(next) {
  629. var chapter = next.split(';')[0];
  630. return location.href.replace(/chapter-[0-9]+/, 'chapter-' + chapter).replace(/page-[0-9]+/, 'page-1');
  631. }
  632. },
  633. prevchap: function() {
  634. return this.nextchap(true);
  635. }
  636. }, {
  637. name: 'mangainn',
  638. match: "^http://www.mangainn.me/manga/chapter/.+",
  639. img: '#imgPage',
  640. next: function() {
  641. if(!this._count) this._count = extractInfo(this.curpage, {type: 'value'});
  642. var url = location.href;
  643. if(!/page_[0-9]+/.test(url)) url += '/page_1';
  644. return url.replace(/page_[0-9]+/, 'page_' + (++this._count));
  645. },
  646. numpages: '#cmbpages',
  647. curpage: '#cmbpages',
  648. nextchap: function(prev) {
  649. var next = extractInfo('#chapters', {type:'value', val: (prev ? -1 : 1)});
  650. if(next) return location.href.replace(/\/chapter\/.+$/, '/chapter/' + next + '/page_1');
  651. },
  652. prevchap: function() {
  653. return this.nextchap(true);
  654. }
  655. }, {
  656. name: 'kukudm',
  657. match: "http://(www|comic|comic2|comic3).kukudm.com/comiclist/[0-9]+/[0-9]+/[0-9]+.htm",
  658. img: function(ctx) {
  659. var script = getEl('td > script[language=javascript]', ctx);
  660. if(script) {
  661. return 'http://n.kukudm.com/' + script.textContent.match(/\+"([^']+)/)[1];
  662. }
  663. },
  664. next: function(ctx) {
  665. var links = getEls('td > a', ctx);
  666. return links[links.length - 1].getAttribute('href');
  667. },
  668. numpages: function(cur) {
  669. return parseInt(document.body.textContent.match(/共([0-9]+)页/)[1]);
  670. },
  671. curpage: function() {
  672. return parseInt(document.body.textContent.match(/第([0-9]+)页/)[1]);
  673. },
  674. beforexhr: reuse.encodeChinese
  675. }, {
  676. name: 'mangachapter',
  677. match: "http://www\\.mangachapter\\.me/[^/]+/[^/]+/[^/]+.html",
  678. img: '#mangaImg, #viewer > table > tbody > tr > td:nth-child(1) > a:nth-child(2) > img',
  679. next: '.page-select + a.button-page',
  680. numpages: '.page-select select',
  681. curpage: '.page-select select',
  682. invchap: true,
  683. nextchap: '#top_chapter_list',
  684. prevchap: '#top_chapter_list',
  685. wait: '#top_chapter_list'
  686. }, {
  687. name: 'kawaii',
  688. match: "http://kawaii.ca/reader/.+",
  689. img: '.picture',
  690. next: 'select[name=page] + a',
  691. numpages: 'select[name=page]',
  692. curpage: 'select[name=page]',
  693. nextchap: function(prev) {
  694. var next = extractInfo('select[name=chapter]', {type:'value', val: (prev ? -1 : 1)});
  695. if(next) return location.href.replace(/\/reader\/([^/]+)(\/.+)?$/, '/reader/$1/' + next);
  696. },
  697. prevchap: function() {
  698. return this.nextchap(true);
  699. }
  700. }, {
  701. name: 'lonemanga',
  702. match: "http://lonemanga.com/manga/[^/]+/[^/]+",
  703. img: '#imageWrapper img',
  704. next: '#imageWrapper a',
  705. numpages: '.viewerPage',
  706. curpage: '.viewerPage',
  707. nextchap: function(prev) {
  708. var next = extractInfo('.viewerChapter', {type:'value', val: (prev ? 1 : -1)});
  709. if(next) return location.href.replace(/\/manga\/([^/]+)\/.+$/, '/manga/$1/' + next);
  710. },
  711. prevchap: function() {
  712. return this.nextchap(true);
  713. }
  714. }];
  715.  
  716. var log = function(msg, type) {
  717. type = type || 'log';
  718. if (type === 'exit') {
  719. throw scriptName + ' exit: ' + msg;
  720. } else {
  721. console[type]('%c' + scriptName + ' ' + type + ':', 'font-weight:bold;color:green;', msg);
  722. }
  723. };
  724.  
  725. var getEl = function(q, c) {
  726. if (!q) return;
  727. return (c || document).querySelector(q);
  728. };
  729.  
  730. var getEls = function(q, c) {
  731. return [].slice.call((c || document).querySelectorAll(q));
  732. };
  733.  
  734. var storeGet = function(key) {
  735. if (typeof GM_getValue === "undefined") {
  736. var value = localStorage.getItem(key);
  737. if (value === "true" || value === "false") {
  738. return (value === "true") ? true : false;
  739. }
  740. return value;
  741. }
  742. return GM_getValue(key);
  743. };
  744.  
  745. var storeSet = function(key, value) {
  746. if (typeof GM_setValue === "undefined") {
  747. return localStorage.setItem(key, value);
  748. }
  749. return GM_setValue(key, value);
  750. };
  751.  
  752. var storeDel = function(key) {
  753. if (typeof GM_deleteValue === "undefined") {
  754. return localStorage.removeItem(key);
  755. }
  756. return GM_deleteValue(key);
  757. };
  758.  
  759. var areDefined = function() {
  760. return [].every.call(arguments, function(arg) {
  761. return arg !== undefined && arg !== null;
  762. });
  763. };
  764.  
  765. var updateObj = function(orig, ext) {
  766. var key;
  767. for (key in ext) {
  768. if (orig.hasOwnProperty(key) && ext.hasOwnProperty(key)) {
  769. orig[key] = ext[key];
  770. }
  771. }
  772. return orig;
  773. };
  774.  
  775. var extractInfo = function(selector, mod, context) {
  776. selector = this[selector] || selector;
  777. if (typeof selector === 'function') {
  778. return selector.call(this, context);
  779. }
  780. var elem = getEl(selector, context),
  781. option;
  782. mod = mod || {};
  783. if (elem) {
  784. switch (elem.nodeName.toLowerCase()) {
  785. case 'img':
  786. return (mod.altProp && elem.getAttribute(mod.altProp)) || elem.src || elem.getAttribute('src');
  787. case 'a':
  788. return elem.href || elem.getAttribute('href');
  789. case 'ul':
  790. return elem.children.length;
  791. case 'select':
  792. switch (mod.type) {
  793. case 'index':
  794. return elem.options.selectedIndex + 1;
  795. case 'value':
  796. option = elem.options[elem.options.selectedIndex + (mod.val || 0)] || {};
  797. return option.value;
  798. default:
  799. return elem.options.length;
  800. }
  801. break;
  802. default:
  803. switch (mod.type) {
  804. case 'index':
  805. return parseInt(elem.textContent);
  806. default:
  807. return elem.textContent;
  808. }
  809. }
  810. }
  811. };
  812.  
  813. var addStyle = function(id, replace) {
  814. if(!this.MLStyles) this.MLStyles = {};
  815. if(!this.MLStyles[id]) {
  816. this.MLStyles[id] = document.createElement('style');
  817. this.MLStyles[id].dataset.name = 'ml-style-' + id;
  818. document.head.appendChild(this.MLStyles[id]);
  819. }
  820. var style = this.MLStyles[id];
  821. var css = [].slice.call(arguments, 2).join('\n');
  822. if(replace) {
  823. style.textContent = css;
  824. } else {
  825. style.textContent += css;
  826. }
  827. };
  828.  
  829. var toStyleStr = function(obj, selector) {
  830. var stack = [],
  831. key;
  832. for (key in obj) {
  833. if (obj.hasOwnProperty(key)) {
  834. stack.push(key + ':' + obj[key]);
  835. }
  836. }
  837. if (selector) {
  838. return selector + '{' + stack.join(';') + '}';
  839. } else {
  840. return stack.join(';');
  841. }
  842. };
  843.  
  844. var throttle = function(callback, limit) {
  845. var wait = false;
  846. return function() {
  847. if (!wait) {
  848. callback();
  849. wait = true;
  850. setTimeout(function() {
  851. wait = false;
  852. }, limit);
  853. }
  854. };
  855. };
  856.  
  857. var createButton = function(text, action, styleStr) {
  858. var button = document.createElement('button');
  859. button.textContent = text;
  860. button.onclick = action;
  861. button.setAttribute('style', styleStr || '');
  862. return button;
  863. };
  864.  
  865. var getViewer = function(prevChapter, nextChapter) {
  866. var viewerCss = toStyleStr({
  867. 'background-color': 'black',
  868. 'font': '0.813em courier',
  869. 'text-align': 'center',
  870. }, 'body'),
  871. imagesCss = toStyleStr({
  872. 'margin-top': '10px',
  873. 'margin-bottom': '10px'
  874. }, '.ml-images'),
  875. imageCss = toStyleStr({
  876. 'max-width': '100%',
  877. 'display': 'block',
  878. 'margin': '3px auto'
  879. }, '.ml-images img'),
  880. counterCss = toStyleStr({
  881. 'background-color': '#222',
  882. 'color': 'white',
  883. 'border-radius': '10px',
  884. 'width': '30px',
  885. 'margin-left': 'auto',
  886. 'margin-right': 'auto',
  887. 'margin-top': '-12px',
  888. 'padding-left': '5px',
  889. 'padding-right': '5px',
  890. 'border': '1px solid white',
  891. 'z-index': '100',
  892. 'position': 'relative'
  893. }, '.ml-counter'),
  894. navCss = toStyleStr({
  895. 'text-decoration': 'none',
  896. 'color': 'white',
  897. 'background-color': '#444',
  898. 'padding': '3px 10px',
  899. 'border-radius': '5px',
  900. 'transition': '250ms'
  901. }, '.ml-chap-nav a'),
  902. navHoverCss = toStyleStr({
  903. 'background-color': '#555'
  904. }, '.ml-chap-nav a:hover'),
  905. boxCss = toStyleStr({
  906. 'position': 'fixed',
  907. 'background-color': '#222',
  908. 'color': 'white',
  909. 'padding': '7px',
  910. 'border-top-left-radius': '5px',
  911. 'cursor': 'default'
  912. }, '.ml-box'),
  913. statsCss = toStyleStr({
  914. 'bottom': '0',
  915. 'right': '0',
  916. 'opacity': '0.4',
  917. 'transition': '250ms'
  918. }, '.ml-stats'),
  919. statsCollapseCss = toStyleStr({
  920. 'color': 'orange',
  921. 'cursor': 'pointer'
  922. }, '.ml-stats-collapse'),
  923. statsHoverCss = toStyleStr({
  924. 'opacity': '1'
  925. }, '.ml-stats:hover'),
  926. floatingMsgCss = toStyleStr({
  927. 'bottom': '50px',
  928. 'right': '0',
  929. 'border-bottom-left-radius': '5px',
  930. 'text-align': 'left',
  931. 'font': 'inherit',
  932. 'max-width': '95%',
  933. 'white-space': 'pre-wrap'
  934. }, '.ml-floating-msg'),
  935. buttonCss = toStyleStr({
  936. 'cursor': 'pointer'
  937. }, '.ml-button'),
  938. keySettingCss = toStyleStr({
  939. 'width': '35px'
  940. }, '.ml-setting-key input'),
  941. autoloadSettingCss = toStyleStr({
  942. 'vertical-align': 'middle'
  943. }, '.ml-setting-autoload');
  944. // clear all styles and scripts
  945. var title = document.title;
  946. document.head.innerHTML = '<meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">';
  947. document.title = title;
  948. // navigation
  949. var nav = '<div class="ml-chap-nav">' + (prevChapter ? '<a class="ml-chap-prev" href="' + prevChapter + '">Prev Chapter</a> ' : '') +
  950. '<a class="ml-exit" href="" data-exit="true">Exit</a> ' +
  951. (nextChapter ? '<a class="ml-chap-next" href="' + nextChapter + '">Next Chapter</a>' : '') + '</div>';
  952. // message area
  953. var floatingMsg = '<pre class="ml-box ml-floating-msg"></pre>';
  954. // stats
  955. var stats = '<div class="ml-box ml-stats"><span title="hide stats" class="ml-stats-collapse">&gt;&gt;</span><span class="ml-stats-content"><span class="ml-stats-pages"></span> <i class="fa fa-info ml-button ml-info-button" title="See userscript information and help"></i> <i class="fa fa-cog ml-button ml-settings-button"></i> <i class="fa fa-refresh ml-button ml-manual-reload" title="Manually refresh next clicked image."></i></span></div>';
  956. // combine ui elements
  957. document.body.innerHTML = nav + '<div class="ml-images"></div>' + nav + floatingMsg + stats;
  958. // add main styles
  959. addStyle('main', true, viewerCss, imagesCss, imageCss, counterCss, navCss, navHoverCss, statsCss, statsCollapseCss, statsHoverCss, boxCss, floatingMsgCss, buttonCss, keySettingCss, autoloadSettingCss);
  960. // add user styles
  961. var userCss = (storeGet('ml-setting-css') || '');
  962. addStyle('user', true, userCss);
  963. // set up return UI object
  964. var UI = {
  965. images: getEl('.ml-images'),
  966. statsContent: getEl('.ml-stats-content'),
  967. statsPages: getEl('.ml-stats-pages'),
  968. statsCollapse: getEl('.ml-stats-collapse'),
  969. btnManualReload: getEl('.ml-manual-reload'),
  970. btnInfo: getEl('.ml-info-button'),
  971. floatingMsg: getEl('.ml-floating-msg'),
  972. btnNextChap: getEl('.ml-chap-next'),
  973. btnPrevChap: getEl('.ml-chap-prev'),
  974. btnExit: getEl('.ml-exit'),
  975. btnSettings: getEl('.ml-settings-button'),
  976. isTyping: false,
  977. ignore: false
  978. };
  979. // message func
  980. var messageId = null;
  981. var showFloatingMsg = function(msg, timeout, html) {
  982. clearTimeout(messageId);
  983. log(msg);
  984. if(html) {
  985. UI.floatingMsg.innerHTML = msg;
  986. } else {
  987. UI.floatingMsg.textContent = msg;
  988. }
  989. UI.floatingMsg.style.display = msg ? '' : 'none';
  990. if(timeout) {
  991. messageId = setTimeout(function() {
  992. showFloatingMsg('');
  993. }, timeout);
  994. }
  995. };
  996. // configure initial state
  997. UI.floatingMsg.style.display = 'none';
  998. // set up listeners
  999. document.addEventListener('click', function(evt) {
  1000. if (evt.target.nodeName === 'A') {
  1001. if(evt.target.className.indexOf('ml-chap') !== -1) {
  1002. log('next chapter will autoload');
  1003. storeSet('autoload', 'yes');
  1004. } else if(evt.target.className.indexOf('ml-exit') !== -1) {
  1005. log('exiting chapter, stop autoload');
  1006. storeSet('autoload', 'no');
  1007. }
  1008. }
  1009. });
  1010. UI.btnManualReload.addEventListener('click', function(evt) {
  1011. var imgClick = function(e) {
  1012. var target = e.target;
  1013. UI.images.removeEventListener('click', imgClick, false);
  1014. UI.images.style.cursor = '';
  1015. if(target.nodeName === 'IMG' && target.parentNode.className === 'ml-images') {
  1016. showFloatingMsg('');
  1017. if(!target.title) {
  1018. showFloatingMsg('Reloading "' + target.src + '"', 3000);
  1019. if(target.complete) target.onload = null;
  1020. target.src = target.src + (target.src.indexOf('?') !== -1 ? '&' : '?') + new Date().getTime();
  1021. }
  1022. } else {
  1023. showFloatingMsg('Cancelled manual reload...', 3000);
  1024. }
  1025. };
  1026. showFloatingMsg('Left click the image you would like to reload.\nClick on the page margin to cancel.');
  1027. UI.images.style.cursor = 'pointer';
  1028. UI.images.addEventListener('click', imgClick, false);
  1029. });
  1030. UI.statsCollapse.addEventListener('click', function(evt) {
  1031. var test = UI.statsCollapse.textContent === '>>';
  1032. UI.statsContent.style.display = test ? 'none' : '';
  1033. UI.statsCollapse.textContent = test ? '<<' : '>>';
  1034. });
  1035. UI.floatingMsg.addEventListener('focus', function(evt) {
  1036. var target = evt.target;
  1037. if(target.dataset.ignore) UI.ignore = true;
  1038. if((target.nodeName === 'INPUT' && target.type === 'text') || target.nodeName === 'TEXTAREA') UI.isTyping = true;
  1039. }, true);
  1040. UI.floatingMsg.addEventListener('blur', function(evt) {
  1041. var target = evt.target;
  1042. if(target.dataset.ignore) UI.ignore = false;
  1043. if((target.nodeName === 'INPUT' && target.type === 'text') || target.nodeName === 'TEXTAREA') UI.isTyping = false;
  1044. }, true);
  1045. UI.btnInfo.addEventListener('click', function(evt) {
  1046. if(UI.floatingMsg.textContent) {
  1047. showFloatingMsg('');
  1048. } else {
  1049. showFloatingMsg('Information:\n' +
  1050. 'IMPORTANT: The script has been updated to exclude NSFW sites\n' +
  1051. 'in order to gain access to that functionality you\'ll have to install the following addon script.\n' +
  1052. 'https://sleazyfork.org/en/scripts/12657-manga-loader-nsfw\n\n' +
  1053. 'New feature! You can now define custom CSS in the new settings panel (accessible through the gear icon at the bottom left).\n' +
  1054. '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' +
  1055. '\n' +
  1056. 'Keybindings:\n' +
  1057. 'Z - previous chapter\n' +
  1058. 'X - exit\n' +
  1059. 'C - next chapter\n' +
  1060. 'W - scroll up\n' +
  1061. 'S - scroll down\n' +
  1062. 'Click the info button again to close this message.');
  1063. }
  1064. });
  1065. UI.btnSettings.addEventListener('click', function() {
  1066. if(UI.floatingMsg.textContent) {
  1067. showFloatingMsg('');
  1068. } else {
  1069. // start grid and first column
  1070. var settings = '<table><tr><td>';
  1071. // Custom CSS
  1072. settings += 'CSS (custom css for Manga Loader):<br>' +
  1073. '<textarea style="width: 300px; height: 300px;" type="text" class="ml-setting-css">' + (storeGet('ml-setting-css') || '') + '</textarea><br><br>';
  1074. // start new column
  1075. settings += '</td><td>';
  1076. // Keybindings
  1077. var keyTableHtml = Object.keys(UI.keys).map(function(action) {
  1078. return '<tr><td>' + action + '</td><td><input data-ignore="true" data-key="' + action + '" type="text" value="' + UI.keys[action] + '"></td></tr>';
  1079. }).join('');
  1080. settings += 'Keybindings:<br><table class="ml-setting-key">' + keyTableHtml + '</table><br>';
  1081. // Autoload
  1082. settings += 'Auto-load: <input class="ml-setting-autoload" type="checkbox" ' + (storeGet('mAutoload') && 'checked' || '') + '><br><br>';
  1083. // Load all or just N pages
  1084. settings += "# of pages to load:<br>" +
  1085. 'Type "all" to load all<br>default is 10<br>' +
  1086. '<input class="ml-setting-loadnum" size="3" type="text" value="' + (storeGet('mLoadNum') || 10) + '" /><br><br>';
  1087. // close grid and column
  1088. settings += '</td></tr></table>';
  1089. // Save button
  1090. settings += '<button class="ml-setting-save">Save</button> <span class="ml-setting-save-flash"></span>';
  1091. showFloatingMsg(settings, null, true);
  1092. // handle keybinding detection
  1093. getEl('.ml-setting-key').onkeydown = function(e) {
  1094. var target = e.target;
  1095. if(target.nodeName.toUpperCase() === 'INPUT') {
  1096. e.preventDefault();
  1097. e.stopPropagation();
  1098. target.value = e.keyCode || e.which;
  1099. }
  1100. };
  1101. // handle save button
  1102. getEl('.ml-setting-save', UI.floatingMsg).onclick = function() {
  1103. // persist css
  1104. var css = getEl('.ml-setting-css', UI.floatingMsg).value.trim();
  1105. addStyle('user', true, css);
  1106. storeSet('ml-setting-css', css);
  1107. // keybindings
  1108. getEls('.ml-setting-key input').forEach(function(input) {
  1109. UI.keys[input.dataset.key] = parseInt(input.value);
  1110. });
  1111. storeSet('ml-setting-key', UI.keys);
  1112. // autoload
  1113. storeSet('mAutoload', getEl('.ml-setting-autoload').checked);
  1114. // loadnum
  1115. var loadnum = getEl('.ml-setting-loadnum').value;
  1116. mLoadNum = getEl('.ml-setting-loadnum').value = loadnum.toLowerCase() === 'all' ? 'all' : (parseInt(loadnum) || 10);
  1117. storeSet('mLoadNum', mLoadNum);
  1118. // flash notify
  1119. var flash = getEl('.ml-setting-save-flash');
  1120. flash.textContent = 'Saved!';
  1121. setTimeout(function() { flash.textContent = ''; }, 1000);
  1122. };
  1123. }
  1124. });
  1125. // keybindings
  1126. UI.keys = {
  1127. PREV_CHAP: 90, EXIT: 88, NEXT_CHAP: 67,
  1128. SCROLL_UP: 87, SCROLL_DOWN: 83
  1129. };
  1130. UI.scrollAmt = 50;
  1131. // override the defaults with the user defined ones
  1132. updateObj(UI.keys, storeGet('ml-setting-key') || {});
  1133. UI._keys = {};
  1134. Object.keys(UI.keys).forEach(function(action) {
  1135. UI._keys[UI.keys[action]] = action;
  1136. });
  1137. window.addEventListener('keydown', function(evt) {
  1138. // ignore keybindings when text input is focused
  1139. if(UI.isTyping) {
  1140. if(!UI.ignore) evt.stopPropagation();
  1141. return;
  1142. }
  1143. // perform action
  1144. switch(evt.keyCode) {
  1145. case UI.keys.PREV_CHAP:
  1146. if(UI.btnPrevChap) {
  1147. UI.btnPrevChap.click();
  1148. }
  1149. break;
  1150. case UI.keys.EXIT:
  1151. UI.btnExit.click();
  1152. break;
  1153. case UI.keys.NEXT_CHAP:
  1154. if(UI.btnNextChap) {
  1155. UI.btnNextChap.click();
  1156. }
  1157. break;
  1158. case UI.keys.SCROLL_UP:
  1159. window.scrollBy(0, -UI.scrollAmt);
  1160. break;
  1161. case UI.keys.SCROLL_DOWN:
  1162. window.scrollBy(0, UI.scrollAmt);
  1163. break;
  1164. default:
  1165. evt.stopPropagation();
  1166. break;
  1167. }
  1168. }, true);
  1169. return UI;
  1170. };
  1171.  
  1172. var getCounter = function(imgNum) {
  1173. var counter = document.createElement('div');
  1174. counter.classList.add('ml-counter');
  1175. counter.textContent = imgNum;
  1176. return counter;
  1177. };
  1178.  
  1179. var addImage = function(src, loc, imgNum, callback) {
  1180. var image = new Image(),
  1181. counter = getCounter(imgNum);
  1182. image.onerror = function() {
  1183. log('failed to load ' + src);
  1184. image.onload = null;
  1185. image.style.backgroundColor = 'white';
  1186. image.style.cursor = 'pointer';
  1187. image.title = 'Reload?';
  1188. image.src = IMAGES.refresh_large;
  1189. image.onclick = function() {
  1190. image.onload = callback;
  1191. image.title = '';
  1192. image.style.cursor = '';
  1193. image.src = src;
  1194. };
  1195. };
  1196. image.onload = callback;
  1197. image.src = src;
  1198. loc.appendChild(image);
  1199. loc.appendChild(counter);
  1200. };
  1201.  
  1202. var loadManga = function(imp) {
  1203. var ex = extractInfo.bind(imp),
  1204. imgUrl = ex('img', imp.imgmod),
  1205. nextUrl = ex('next'),
  1206. numPages = ex('numpages'),
  1207. curPage = ex('curpage', {
  1208. type: 'index'
  1209. }) || 1,
  1210. nextChapter = ex('nextchap', {
  1211. type: 'value',
  1212. val: (imp.invchap && -1) || 1
  1213. }),
  1214. prevChapter = ex('prevchap', {
  1215. type: 'value',
  1216. val: (imp.invchap && 1) || -1
  1217. }),
  1218. xhr = new XMLHttpRequest(),
  1219. d = document.implementation.createHTMLDocument(),
  1220. addAndLoad = function(img, next) {
  1221. if(!img) throw new Error('failed to retrieve img for page ' + curPage);
  1222. updateStats();
  1223. addImage(img, UI.images, curPage, function() {
  1224. pagesLoaded += 1;
  1225. updateStats();
  1226. });
  1227. if(!next) throw new Error('failed to retrieve next url for page ' + curPage);
  1228. loadNextPage(next);
  1229. },
  1230. updateStats = function() {
  1231. UI.statsPages.textContent = ' ' + pagesLoaded + '/' + curPage + ' loaded' + (numPages ? ', ' + numPages + ' total' : '');
  1232. },
  1233. getPageInfo = function() {
  1234. var page = d.body;
  1235. d.body.innerHTML = xhr.response;
  1236. try {
  1237. // find image and link to next page
  1238. addAndLoad(ex('img', imp.imgmod, page), ex('next', null, page));
  1239. } catch (e) {
  1240. log(e);
  1241. log('error getting details from next page, assuming end of chapter.');
  1242. }
  1243. },
  1244. loadNextPage = function(url) {
  1245. if (mLoadNum !== 'all' && count % mLoadNum === 0) {
  1246. if (resumeUrl) {
  1247. resumeUrl = null;
  1248. } else {
  1249. resumeUrl = url;
  1250. log('waiting for user to scroll further before loading more images, loaded ' + count + ' pages so far, next url is ' + resumeUrl);
  1251. return;
  1252. }
  1253. }
  1254. if (curPage + 1 > numPages) {
  1255. log('reached "numPages" ' + numPages + ', assuming end of chapter');
  1256. return;
  1257. }
  1258. if (lastUrl === url) {
  1259. log('last url (' + lastUrl + ') is the same as current (' + url + '), assuming end of chapter');
  1260. return;
  1261. }
  1262. curPage += 1;
  1263. count += 1;
  1264. lastUrl = url;
  1265. if (imp.pages) {
  1266. imp.pages(url, curPage, addAndLoad, ex, getPageInfo);
  1267. } else {
  1268. var colonIdx = url.indexOf(':');
  1269. if(colonIdx > -1) url = url.slice(colonIdx + 1);
  1270. url = location.protocol + url;
  1271. xhr.open('get', url);
  1272. imp.beforexhr && imp.beforexhr(xhr);
  1273. xhr.onload = getPageInfo;
  1274. xhr.onerror = function() {
  1275. log('failed to load page, aborting', 'error');
  1276. };
  1277. xhr.send();
  1278. }
  1279. },
  1280. count = 1,
  1281. pagesLoaded = curPage - 1,
  1282. lastUrl, UI, resumeUrl;
  1283.  
  1284. if (!imgUrl || !nextUrl) {
  1285. log('failed to retrieve ' + (!imgUrl ? 'image url' : 'next page url'), 'exit');
  1286. }
  1287. // do some checks on the chapter urls
  1288. nextChapter = (nextChapter && nextChapter.trim() === location.href + '#' ? null : nextChapter);
  1289. prevChapter = (prevChapter && prevChapter.trim() === location.href + '#' ? null : prevChapter);
  1290.  
  1291. UI = getViewer(prevChapter, nextChapter);
  1292.  
  1293. UI.statsPages.textContent = ' 0/1 loaded, ' + numPages + ' total';
  1294.  
  1295. if (mLoadNum !== 'all') {
  1296. window.addEventListener('scroll', throttle(function(e) {
  1297. if (!resumeUrl) return; // exit early if we don't have a position to resume at
  1298. if(!UI.imageHeight) {
  1299. UI.imageHeight = getEl('.ml-images img').clientHeight;
  1300. }
  1301. var scrollBottom = document.body.scrollHeight - ((document.body.scrollTop || document.documentElement.scrollTop) + window.innerHeight);
  1302. if (scrollBottom < UI.imageHeight * 2) {
  1303. log('user scroll nearing end, loading more images starting from ' + resumeUrl);
  1304. loadNextPage(resumeUrl);
  1305. }
  1306. }, 100));
  1307. }
  1308.  
  1309. addAndLoad(imgUrl, nextUrl);
  1310.  
  1311. };
  1312.  
  1313. W.MLoaderLoadImps = function(imps) {
  1314. var success = imps.some(function(imp) {
  1315. var intervalId;
  1316. var waitAndLoad = function() {
  1317. if (typeof imp.wait === 'function') {
  1318. log('Waiting for load condition to be fulfilled');
  1319. intervalId = setInterval(function() {
  1320. if (imp.wait()) {
  1321. log('Condition fulfilled, loading');
  1322. clearInterval(intervalId);
  1323. loadManga(imp);
  1324. }
  1325. }, 200);
  1326. } else if(typeof imp.wait === 'string') {
  1327. log('Waiting for load condition to be fulfilled');
  1328. intervalId = setInterval(function() {
  1329. if (getEl(imp.wait)) {
  1330. log('Condition fulfilled, loading');
  1331. clearInterval(intervalId);
  1332. loadManga(imp);
  1333. }
  1334. }, 200);
  1335. } else {
  1336. setTimeout(loadManga.bind(null, imp), imp.wait || 0);
  1337. }
  1338. };
  1339. if (imp.match && (new RegExp(imp.match, 'i')).test(pageUrl)) {
  1340. currentImpName = imp.name;
  1341. if (autoload !== 'no' && (BM_MODE || mAutoload || autoload)) {
  1342. log('autoloading...');
  1343. waitAndLoad();
  1344. return true;
  1345. }
  1346. // append button to dom that will trigger the page load
  1347. btnLoad = createButton('Load Manga', function(evt) {
  1348. waitAndLoad();
  1349. this.remove();
  1350. }, btnLoadCss);
  1351. document.body.appendChild(btnLoad);
  1352. return true;
  1353. }
  1354. });
  1355.  
  1356. if (!success) {
  1357. log('no implementation for ' + pageUrl, 'error');
  1358. }
  1359. };
  1360.  
  1361. var pageUrl = window.location.href,
  1362. btnLoadCss = toStyleStr({
  1363. 'position': 'fixed',
  1364. 'bottom': 0,
  1365. 'right': 0,
  1366. 'padding': '5px',
  1367. 'margin': '0 10px 10px 0',
  1368. 'z-index': '99999'
  1369. }),
  1370. currentImpName, btnLoad;
  1371.  
  1372. // used when switching chapters
  1373. var autoload = storeGet('autoload');
  1374. // manually set by user in menu
  1375. var mAutoload = storeGet('mAutoload') || false;
  1376. // should we load less pages at a time?
  1377. var mLoadNum = storeGet('mLoadNum') || 10;
  1378.  
  1379. // clear autoload
  1380. storeDel('autoload');
  1381.  
  1382. log('starting...');
  1383.  
  1384. W.MLoaderLoadImps(implementations);