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

2015-08-20 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

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