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

От 14.04.2015. Виж последната версия.

  1. // ==UserScript==
  2. // @name Manga Loader
  3. // @namespace http://www.fuzetsu.com/MangaLoader
  4. // @version 1.6.0
  5. // @description Loads manga chapter into one page in a long strip format, supports switching chapters and works for a variety of sites, minimal script with no dependencies, easy to implement new sites
  6. // @copyright 2014+, fuzetsu
  7. // @noframes
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_deleteValue
  11. // @grant GM_registerMenuCommand
  12. // @match http://bato.to/read/*
  13. // @match http://mangafox.me/manga/*/*/*
  14. // @match http://readms.com/r/*/*/*/*
  15. // @match http://mangastream.com/read/*/*/*/*
  16. // @match http://g.e-hentai.org/s/*/*
  17. // @match http://exhentai.org/s/*/*
  18. // @match *://www.fakku.net/*/*/read*
  19. // @match http://www.mangareader.net/*/*
  20. // @match http://www.mangahere.co/manga/*/*
  21. // @match http://www.mangapanda.com/*/*
  22. // @match http://mangadeer.com/manga/*/*/*/*
  23. // @match http://mangacow.co/*/*
  24. // @match http://nowshelf.com/watch/*
  25. // @match http://nhentai.net/g/*/*
  26. // @match http://centraldemangas.net/online/*/*
  27. // @match http://narutomanga.com.br/leitura/online/capitulo/*
  28. // @match http://bleachmanga.com.br/leitura/online/capitulo/*
  29. // @match http://www.mangatown.com/manga/*/*/*
  30. // @match http://manga-joy.com/*/*
  31. // @match http://*.dm5.com/m*
  32. // @match http://raw.senmanga.com/*/*/*
  33. // @match http://www.japscan.com/lecture-en-ligne/*
  34. // @match http://www.pecintakomik.com/manga/*/*
  35. // @match http://dynasty-scans.com/chapters/*
  36. // @match http://www.onemanga.me/*/*
  37. // @match http://www.onemanga2.com/*/*
  38. // @match http://mangawall.com/manga/*/*
  39. // @match http://manga.animea.net/*
  40. // @match http://kissmanga.com/Manga/*/*
  41. // @match http://view.thespectrum.net/series/*
  42. // @match http://manhua.dmzj.com/*/*
  43. // @match http://www.8muses.com/picture/*/category/*
  44. // @match http://hqbr.com.br/hqs/*/capitulo/*/leitor/0
  45. // @match http://www.dmzj.com/view/*/*
  46. // @match http://mangaindo.co/*/*
  47. // @match *://hitomi.la/reader/*
  48. // @match http://www.doujin-moe.us/*
  49. // @match http://mangadoom.co/*/*
  50. // @match http://www.mangago.me/read-manga/*/*/*/*
  51. // @match http://mangalator.ch/show.php?gallery=*
  52. // @match http://eatmanga.com/Manga-Scan/*/*
  53. // ==/UserScript==
  54.  
  55. // set to true for manga load without prompt
  56. var BM_MODE = false;
  57.  
  58. // short reference to unsafeWindow (or window if unsafeWindow is unavailable e.g. bookmarklet)
  59. var W = (typeof unsafeWindow === 'undefined') ? window : unsafeWindow;
  60.  
  61. var scriptName = 'Manga Loader';
  62. var pageTitle = document.title;
  63.  
  64. var IMAGES = {
  65. refresh_large: '//rgeorgeeb-tilebuttongenerator.googlecode.com/hg-history/c35993682c2d50149976fd7a1f302f8c01a88716/asset-studio/src/res/clipart/icons/refresh.svg'
  66. };
  67.  
  68. /**
  69. Sample Implementation:
  70. {
  71. match: "http://domain.com/.*" // the url to react to for manga loading
  72. , img: '#image' // css selector to get the page's manga image
  73. , next: '#next_page' // css selector to get the link to the next page
  74. , numpages: '#page_select' // css selector to get the number of pages. elements like (select, span, etc)
  75. , curpage: '#page_select' // css selector to get the current page. usually the same as numPages if it's a select element
  76. , nextchap: '#next_chap' // css selector to get the link to the next chapter
  77. , prevchap: '#prev_chap' // same as above except for previous
  78. , wait: 3000 // how many ms to wait before auto loading (to wait for elements to load)
  79. , pages: function(next_url, current_page_number, callback, extract_function) {
  80. // gets called requesting a certain page number (current_page_number)
  81. // to continue loading execute callback with img to append as first parameter and next url as second parameter
  82. // only really needs to be used on sites that have really unusual ways of loading images or depend on javascript
  83. }
  84.  
  85. Any of the CSS selectors can be functions instead that return the desired value.
  86. }
  87. */
  88.  
  89. var implementations = [{ // Batoto
  90. match: "^http://bato.to/read/.*",
  91. img: '#comic_page',
  92. next: '#full_image + div > a',
  93. numpages: '#page_select',
  94. curpage: '#page_select',
  95. nextchap: 'select[name=chapter_select]',
  96. prevchap: 'select[name=chapter_select]',
  97. invchap: true
  98. }, { // MangaPanda
  99. match: "^http://www.mangapanda.com/.*/[0-9]*",
  100. img: '#img',
  101. next: '.next a',
  102. numpages: '#pageMenu',
  103. curpage: '#pageMenu',
  104. nextchap: 'td.c5 + td a',
  105. prevchap: 'table.c6 tr:last-child td:last-child a'
  106. }, { // MangaFox
  107. match: "^http://mangafox.me/manga/[^/]*/[^/]*/[^/]*",
  108. img: '#image',
  109. next: 'a.next_page',
  110. numpages: function() {
  111. return extractInfo('select.m') - 1;
  112. },
  113. curpage: 'select.m',
  114. nextchap: '#chnav p + p a',
  115. prevchap: '#chnav a'
  116. }, { // MangaStream
  117. match: "^http://(readms|mangastream).com/(r|read)/[^/]*/[^/]*/[^/]*",
  118. img: '#manga-page',
  119. next: '.next a',
  120. numpages: function() {
  121. var lastPage = getEl('.subnav-wrapper .controls .btn-group:last-child ul li:last-child');
  122. return parseInt(lastPage.textContent.match(/[0-9]/g).join(''), 10);
  123. },
  124. nextchap: function(prev) {
  125. var found;
  126. var chapters = [].slice.call(document.querySelectorAll('.controls > div:first-child > .dropdown-menu > li a'));
  127. chapters.pop();
  128. for (var i = 0; i < chapters.length; i++) {
  129. if (window.location.href.indexOf(chapters[i].href) !== -1) {
  130. found = chapters[i + (prev ? 1 : -1)];
  131. if (found) return found.href;
  132. }
  133. }
  134. },
  135. prevchap: function() {
  136. return this.nextchap(true);
  137. }
  138. }, { // MangaReader
  139. match: "^http://www.mangareader.net/.*/.*",
  140. img: '#img',
  141. next: '.next a',
  142. numpages: '#pageMenu',
  143. curpage: '#pageMenu',
  144. nextchap: 'td.c5 + td a',
  145. prevchap: 'table.c6 tr:last-child td:last-child a'
  146. }, { // MangaTown
  147. match: "^http://www.mangatown.com/manga/[^/]*/v[0-9]*/c[0-9]*",
  148. img: '#image',
  149. next: '#viewer a',
  150. numpages: '.page_select select',
  151. curpage: '.page_select select',
  152. nextchap: '#top_chapter_list',
  153. prevchap: '#top_chapter_list',
  154. wait: 1000
  155. }, { // MangaCow
  156. match: "^http://mangacow\\.co/.*/[0-9]*",
  157. img: '.prw > a > img',
  158. next: '.prw > a:last-child',
  159. numpages: 'select.cbo_wpm_pag',
  160. curpage: 'select.cbo_wpm_pag',
  161. nextchap: function(prev) {
  162. var chapSel = getEl('select.cbo_wpm_chp');
  163. var nextChap = chapSel.options[chapSel.selectedIndex + (prev ? 1 : -1)];
  164. if (nextChap) {
  165. return 'http://mangacow.co/' + window.location.pathname.slice(1, window.location.pathname.slice(1).indexOf('/') + 2) + nextChap.value;
  166. }
  167. },
  168. prevchap: function() {
  169. return this.nextchap(true);
  170. }
  171. }, { // MangaHere
  172. match: "^http://www.mangahere.co/manga/.*/.*",
  173. img: '#viewer img',
  174. next: '#viewer a',
  175. numpages: 'select.wid60',
  176. curpage: 'select.wid60',
  177. nextchap: function(prev) {
  178. var chapter = W.chapter_list[W.current_chapter_index + (prev ? -1 : 1)];
  179. return chapter && chapter[1];
  180. },
  181. prevchap: function() {
  182. return this.nextchap(true);
  183. },
  184. wait: function() {
  185. return areDefined(W.current_chapter_index, W.chapter_list);
  186. }
  187. }, { // MangaDeer
  188. match: "^http://mangadeer\\.com/manga/.*",
  189. img: '.img-link > img',
  190. next: '.page > span:last-child > a',
  191. numpages: '#sel_page_1',
  192. curpage: '#sel_page_1',
  193. nextchap: function(prev) {
  194. var ddl = getEl('#sel_book_1');
  195. var index = ddl.selectedIndex + (prev ? -1 : 1);
  196. if (index >= ddl.options.length) return;
  197. var mangaName = window.location.href.slice(window.location.href.indexOf('manga/') + 6);
  198. mangaName = mangaName.slice(0, mangaName.indexOf('/'));
  199. return 'http://mangadeer.com/manga/' + mangaName + ddl.options[index].value + '/1';
  200. },
  201. prevchap: function() {
  202. return this.nextchap(true);
  203. }
  204. }, { // Central de Mangas
  205. match: "^http://(centraldemangas\\.net|(bleachmanga|narutomanga)\\.com\\.br/leitura)/online/[^/]*/[0-9]*",
  206. img: '#manga-page',
  207. next: '#manga-page',
  208. numpages: '#manga_pages',
  209. curpage: '#manga_pages',
  210. nextchap: function(prev) {
  211. var url = window.location.href,
  212. chapters = getEl('#manga_caps'),
  213. urlPre = url.slice(0, url.lastIndexOf('/') + 1),
  214. newChap = chapters.options[chapters.selectedIndex + (prev ? -1 : 1)];
  215. return newChap ? urlPre + newChap.textContent : null;
  216. },
  217. prevchap: function() {
  218. return this.nextchap(true);
  219. },
  220. pages: function(url, num, cb, ex) {
  221. var url = url.slice(0, url.lastIndexOf('-') + 1) + ("0" + num).slice(-2) + url.slice(url.lastIndexOf('.'));
  222. cb(url, url);
  223. }
  224. }, { // Manga Joy
  225. match: "^http://manga-joy.com/[^/]*/[0-9]*",
  226. img: '.prw img',
  227. next: '.nxt',
  228. numpages: '.wpm_nav_rdr li:nth-child(3) > select',
  229. curpage: '.wpm_nav_rdr li:nth-child(3) > select',
  230. nextchap: function(prev) {
  231. var chapter = extractInfo('.wpm_nav_rdr li:nth-child(2) > select', {
  232. type: 'value',
  233. val: prev ? 1 : -1
  234. });
  235. if (chapter) {
  236. var urlParts = window.location.href.slice(7).split('/');
  237. while (urlParts.length > 2) urlParts.pop();
  238. return 'http://' + urlParts.join('/') + '/' + chapter;
  239. }
  240. },
  241. prevchap: function() {
  242. return this.nextchap(true);
  243. }
  244. }, { // GEH/EXH
  245. match: "^http://(g.e-hentai|exhentai).org/s/.*/.*",
  246. img: '.sni > a > img, #img',
  247. next: '.sni > a, #i3 a',
  248. numpages: 'body > div > div:nth-child(2) > div > span:nth-child(2)',
  249. curpage: 'body > div > div:nth-child(2) > div > span:nth-child(1)'
  250. }, { // Fakku
  251. match: "^http(s)?://www.fakku.net/.*/.*/read",
  252. img: '.current-page',
  253. next: '.current-page',
  254. numpages: '.drop',
  255. curpage: '.drop',
  256. pages: function(url, num, cb, ex) {
  257. var firstNum = url.lastIndexOf('/'),
  258. lastDot = url.lastIndexOf('.');
  259. var c = url.charAt(firstNum);
  260. while (c && !/[0-9]/.test(c)) {
  261. c = url.charAt(++firstNum);
  262. }
  263. var curPage = parseInt(url.slice(firstNum, lastDot), 10);
  264. var url = url.slice(0, firstNum) + ('00' + (curPage + 1)).slice(-3) + url.slice(lastDot);
  265. cb(url, url);
  266. }
  267. }, { // Nowshelf
  268. match: "^http://nowshelf.com/watch/[0-9]*",
  269. img: '#image',
  270. next: '#image',
  271. numpages: function() {
  272. return parseInt(getEl('#page').textContent.slice(3), 10);
  273. },
  274. curpage: function() {
  275. return parseInt(getEl('#page > input').value, 10);
  276. },
  277. pages: function(url, num, cb, ex) {
  278. var url = url.slice(0, -7) + ('00' + num).slice(-3) + url.slice(-4);
  279. cb(url, url);
  280. }
  281. }, { // nhentai
  282. match: "^http://nhentai\\.net\\/g\\/[0-9]*/[0-9]*",
  283. img: '#image-container > a > img',
  284. next: '#image-container > a > img',
  285. numpages: '.num-pages',
  286. curpage: '.current',
  287. pages: function(url, num, cb, ex) {
  288. url = url.replace(/\/[^\/]*$/, '/') + num;
  289. cb(url, url);
  290. }
  291. }, { // dm5
  292. match: "^http://[^\\.]*\\.dm5\\.com/m[0-9]*",
  293. img: '#cp_image',
  294. next: '#cp_image',
  295. numpages: '#pagelist',
  296. curpage: '#pagelist',
  297. pages: function(url, num, cb, ex) {
  298. var cid = window.location.href.match(/m[0-9]*/g)[2].slice(1),
  299. xhr = new XMLHttpRequest();
  300. xhr.open('get', 'chapterfun.ashx?cid=' + cid + '&page=' + num);
  301. xhr.onload = function() {
  302. var images = eval(xhr.responseText);
  303. cb(images[0], images[0]);
  304. };
  305. xhr.send();
  306. }
  307. }, { // Senmanga
  308. match: "^http://raw\\.senmanga\\.com/[^/]*/[^/]*/[0-9]*",
  309. img: '#picture',
  310. next: '#omv > table > tbody > tr:nth-child(2) > td > a',
  311. numpages: 'select[name=page]',
  312. curpage: 'select[name=page]',
  313. nextchap: function(prev) {
  314. var next = extractInfo('select[name=chapter]', {type: 'value', val: (prev ? 1 : -1)});
  315. if(next) return window.location.href.replace(/\/[^\/]*\/[0-9]+\/?$/, '') + '/' + next + '/1';
  316. },
  317. prevchap: function() {
  318. return this.nextchap(true);
  319. },
  320. pages: function(url, num, cb, ex) {
  321. cb(W.new_image.replace(/page=[0-9]+&/, 'page=' + num + '&'), num);
  322. }
  323. }, { // japscan
  324. match: "^http://www\\.japscan\\.com/lecture-en-ligne/[^/]*/[0-9]*",
  325. img: '#imgscan',
  326. next: '#next_link',
  327. numpages: '#pages',
  328. curpage: '#pages',
  329. nextchap: '#next_chapter',
  330. prevchap: '#back_chapter'
  331. }, { // pecintakomik
  332. match: "^http://www\\.pecintakomik\\.com/manga/[^/]*/[^/]*",
  333. img: '.picture',
  334. next: '.pager a:nth-child(3)',
  335. numpages: 'select[name=page]',
  336. curpage: 'select[name=page]',
  337. nextchap: function(prev) {
  338. var chapters = getEl('select[name=chapter]'),
  339. chapter = chapters.options[chapters.selectedIndex + (prev ? 1 : -1)];
  340. if (chapter) {
  341. return window.location.href.replace(/\/([^\/]+)\/[0-9]+\/?$/, '/$1/' + chapter.value);
  342. }
  343. },
  344. prevchap: function() {
  345. return this.nextchap(true);
  346. }
  347. }, { // dynasty-scans
  348. match: "^http://dynasty-scans.com/chapters/.*",
  349. img: '#image > img',
  350. next: '#image > img',
  351. numpages: function() {
  352. return W.pages.length;
  353. },
  354. curpage: function() {
  355. return parseInt(getEl('#image > div.pages-list > a.page.active').textContent);
  356. },
  357. nextchap: '#next_link',
  358. prevchap: '#prev_link',
  359. pages: function(url, num, cb, ex) {
  360. url = W.pages[num - 1].image;
  361. cb(url, url);
  362. }
  363. }, { // OneManga
  364. match: "^http://www\\.onemanga(2)?\\.(me|com)/[^/]*/[0-9]*",
  365. img: 'img.manga-page',
  366. next: '.nav_pag > li:nth-child(1) > a',
  367. numpages: 'select.cbo_wpm_pag',
  368. curpage: 'select.cbo_wpm_pag',
  369. nextchap: function(prev) {
  370. var curChap = parseInt(extractInfo('select.cbo_wpm_chp', {type: 'value'})),
  371. targetChap = curChap + (prev ? -1 : 1);
  372. return window.location.href.replace(/\/[0-9]*(\/[0-9]*\/?)?$/, '/' + targetChap);
  373. },
  374. prevchap: function() {
  375. return this.nextchap(true);
  376. }
  377. }, { // MangaWall
  378. _page: null,
  379. match: "^http://mangawall\\.com/manga/[^/]*/[0-9]*",
  380. img: 'img.scan',
  381. next: function() {
  382. if(this._page === null) this._page = W.page;
  383. return W.series_url + '/' + W.chapter + '/' + (this._page += 1);
  384. },
  385. numpages: '.pageselect',
  386. curpage: '.pageselect',
  387. nextchap: function(prev) {
  388. return W.series_url + '/' + (parseInt(W.chapter.slice(1)) + (prev ? -1 : 1)) + '/1';
  389. },
  390. prevchap: function() {
  391. return this.nextchap(true);
  392. }
  393. }, { // Manga AnimeA
  394. _page: null,
  395. match: "^http://manga\\.animea.net/.*chapter-[0-9]*-page-[0-9]*.html",
  396. img: '#scanmr',
  397. next: function() {
  398. if(this._page === null) this._page = W.page;
  399. return W.series_url + W.chapter + '-page-' + (this._page += 1) + '.html';
  400. },
  401. numpages: '.pageselect',
  402. curpage: '.pageselect',
  403. nextchap: function(prev) {
  404. return W.series_url + 'chapter-' + (parseInt(W.chapter.match(/[0-9]+/)[0]) + (prev ? -1 : 1)) + '-page-1.html';
  405. },
  406. prevchap: function() {
  407. return this.nextchap(true);
  408. }
  409. }, { // Kissmanga
  410. match: "^http://kissmanga\\.com/Manga/[^/]*/.+",
  411. img: '#imgCurrent',
  412. next: '#imgCurrent',
  413. numpages: function() {
  414. return W.lstImages.length;
  415. },
  416. curpage: function() {
  417. return W.currImage + 1;
  418. },
  419. nextchap: '#selectChapter',
  420. prevchap: '#selectChapter',
  421. pages: function(url, num, cb, ex) {
  422. cb(W.lstImages[num - 1], num);
  423. }
  424. }, { // the spectrum scans
  425. match: "^http://view\\.thespectrum\\.net/series/[^\\.]+\\.html\\?ch=[^&]*&page=[0-9]+",
  426. img: '#mainimage',
  427. next: '#mainimage',
  428. numpages: '.selectpage',
  429. curpage: '.selectpage',
  430. nextchap: function(prev) {
  431. var next = extractInfo('.selectchapter', {type: 'value', val: prev ? -1 : 1});
  432. if(next) {
  433. return window.location.href.replace(/ch=.+/, 'ch=' + next + '&page=1');
  434. }
  435. },
  436. prevchap: function() {
  437. return this.nextchap(true);
  438. },
  439. pages: function(url, num, cb, ex) {
  440. url = url.replace(/[^\.\/]+\.([a-z]+)$/, ('00' + num).slice(-3) + '.$1');
  441. cb(url, url);
  442. }
  443. }, { // manhua.dmzj.com
  444. match: "http://manhua.dmzj.com/[^/]*/[0-9]+(-[0-9]+)?\\.shtml",
  445. img: '.pic_link',
  446. next: '.pic_link',
  447. numpages: function() {
  448. return W.arr_pages.length;
  449. },
  450. curpage: function() {
  451. return W.COMIC_PAGE.getPageNumber();
  452. },
  453. nextchap: '#next_chapter',
  454. prevchap: '#prev_chapter',
  455. pages: function(url, num, cb, ex) {
  456. cb(W.img_prefix + W.arr_pages[num - 1], num);
  457. }
  458. }, { // 8muses
  459. match: "http://www.8muses.com/picture/[^/]+/category/.+",
  460. img: '.image',
  461. next: '.imgLiquidFill > a'
  462. }, { // hqbr.com.br
  463. match: "http://hqbr.com.br/hqs/[^/]+/capitulo/[0-9]+/leitor/0",
  464. img: '#hq-page',
  465. next: '#hq-page',
  466. numpages: function() {
  467. return W.pages.length;
  468. },
  469. curpage: function() {
  470. return W.paginaAtual + 1;
  471. },
  472. nextchap: function(prev) {
  473. var chapters = getEls('#chapter-dropdown a'),
  474. current = parseInt(W.capituloIndex),
  475. chapter = chapters[current + (prev ? -1 : 1)];
  476. return chapter && chapter.href;
  477. },
  478. prevchap: function() {
  479. return this.nextchap(true);
  480. },
  481. pages: function(url, num, cb, ex) {
  482. cb(W.pages[num - 1], num);
  483. }
  484. }, { // www.dmzj.com
  485. match: "http://www.dmzj.com/view/[^/]+/.+\\.html",
  486. img: '#pic',
  487. next: '#pic',
  488. numpages: '.select_jump',
  489. curpage: '.select_jump',
  490. nextchap: '.next > a',
  491. prevchap: '.pre > a',
  492. pages: function(url, num, cb, ex) {
  493. cb('http://images.dmzj.com/' + W.pic[num - 1], num);
  494. },
  495. wait: 1000
  496. }, { // mangaindo.co
  497. match: "http://mangaindo.co/[^/]+/[0-9]+",
  498. img: '.prw > a > img',
  499. next: '.prw > a',
  500. numpages: '.cbo_wpm_pag',
  501. curpage: '.cbo_wpm_pag',
  502. nextchap: function(prev) {
  503. var chapter = extractInfo('.cbo_wpm_chp', { type: 'value', val: (prev ? 1 : -1) });
  504. if(chapter) return W.location.origin + '/' + W.location.pathname.slice(1).split('/').shift() + '/' + chapter + '/1';
  505. },
  506. prevchap: function() {
  507. return this.nextchap(true);
  508. }
  509. }, { // hitomi.la
  510. match: "http(s)?://hitomi.la/reader/[0-9]+.html",
  511. img: '#comicImages > img',
  512. next: '#comicImages > img',
  513. numpages: function() {
  514. return W.images.length;
  515. },
  516. curpage: function() {
  517. return parseInt(W.curPanel);
  518. },
  519. pages: function(url, num, cb, ex) {
  520. cb(W.images[num - 1].path, num);
  521. }
  522. }, { // doujin-moe
  523. _pages: null,
  524. match: "http://www.doujin-moe.us/.+",
  525. img: 'img.picture',
  526. next: 'img.picture',
  527. numpages: function() {
  528. if(!this._pages) {
  529. this._pages = getEls('#gallery djm').map(function(file) {
  530. return file.getAttribute('file');
  531. });
  532. }
  533. return this._pages.length;
  534. },
  535. curpage: function() {
  536. return parseInt(getEl('.counter').textContent.match(/^[0-9]+/)[0]);
  537. },
  538. pages: function(url, num, cb, ex) {
  539. cb(this._pages[num - 1], num);
  540. }
  541. }, { // Mangadoom
  542. match: "http://mangadoom.co/[^/]+/[0-9]+",
  543. img: '.prw a:last-child > img',
  544. next: '.prw a:last-child',
  545. numpages: 'select.cbo_wpm_pag',
  546. curpage: 'select.cbo_wpm_pag',
  547. nextchap: function(prev) {
  548. var chapSel = getEl('select.cbo_wpm_chp');
  549. var nextChap = chapSel.options[chapSel.selectedIndex + (prev ? 1 : -1)];
  550. if (nextChap) {
  551. return 'http://mangadoom.co/' + window.location.pathname.slice(1, window.location.pathname.slice(1).indexOf('/') + 2) + nextChap.value;
  552. }
  553. },
  554. prevchap: function() {
  555. return this.nextchap(true);
  556. }
  557. }, { // mangago
  558. match: "http://www.mangago.me/read-manga/[^/]+/[^/]+/[^/]+",
  559. img: '#page1',
  560. next: '#pic_container',
  561. numpages: '#dropdown-menu-page',
  562. curpage: function() {
  563. return parseInt(getEls('#page-mainer a.btn.dropdown-toggle')[1].textContent.match(/[0-9]+/)[0]);
  564. },
  565. nextchap: function(prev) {
  566. var chapters = getEls('ul.dropdown-menu.chapter a'),
  567. curName = getEls('#page-mainer a.btn.dropdown-toggle')[0].textContent,
  568. curIdx;
  569. chapters.some(function(chap, idx) {
  570. if(chap.textContent === curName) {
  571. curIdx = idx;
  572. return true;
  573. }
  574. });
  575. var chapter = chapters[curIdx + (prev ? 1 : -1)];
  576. return chapter && chapter.href;
  577. },
  578. prevchap: function() {
  579. return this.nextchap(true);
  580. }
  581. }, { // mangalator
  582. match: "http://mangalator.ch/show.php\\?gallery=[0-9]+",
  583. img: '.image img',
  584. next: '#next',
  585. numpages: 'select[name=image]',
  586. curpage: 'select[name=image]',
  587. nextchap: function(prev) {
  588. var chapters = getEl('select[name=gallery]');
  589. var chapter = chapters.options[chapters.selectedIndex + (prev ? 1 : -1)];
  590. if(chapter) {
  591. return location.href.replace(/\?gallery=[0-9]+/, '?gallery=' + chapter.value);
  592. }
  593. },
  594. prevchap: function() {
  595. return this.nextchap(true);
  596. }
  597. }, { // eatmanga
  598. match: "http://eatmanga.com/Manga-Scan/[^/]+/.+",
  599. img: '#main_content img',
  600. next: '#page_next',
  601. numpages: '#pages',
  602. curpage: '#pages',
  603. nextchap: '#bottom_chapter_list',
  604. prevchap: '#bottom_chapter_list',
  605. invchap: true
  606. }];
  607.  
  608. var log = function(msg, type) {
  609. type = type || 'log';
  610. if (type === 'exit') {
  611. throw scriptName + ' exit: ' + msg;
  612. } else {
  613. console[type](scriptName + ' ' + type + ': ', msg);
  614. }
  615. };
  616.  
  617. var getEl = function(q, c) {
  618. if (!q) return;
  619. return (c || document).querySelector(q);
  620. };
  621.  
  622. var getEls = function(q, c) {
  623. return [].slice.call((c || document).querySelectorAll(q));
  624. };
  625.  
  626. var storeGet = function(key) {
  627. if (typeof GM_getValue === "undefined") {
  628. var value = localStorage.getItem(key);
  629. if (value === "true" || value === "false") {
  630. return (value === "true") ? true : false;
  631. }
  632. return value;
  633. }
  634. return GM_getValue(key);
  635. };
  636.  
  637. var storeSet = function(key, value) {
  638. if (typeof GM_setValue === "undefined") {
  639. return localStorage.setItem(key, value);
  640. }
  641. return GM_setValue(key, value);
  642. };
  643.  
  644. var storeDel = function(key) {
  645. if (typeof GM_deleteValue === "undefined") {
  646. return localStorage.removeItem(key);
  647. }
  648. return GM_deleteValue(key);
  649. };
  650.  
  651. var areDefined = function() {
  652. return [].every.call(arguments, function(arg) {
  653. return arg !== undefined && arg !== null;
  654. });
  655. };
  656.  
  657. var extractInfo = function(selector, mod, context) {
  658. selector = this[selector] || selector;
  659. if (typeof selector === 'function') {
  660. return selector.call(this);
  661. }
  662. var elem = getEl(selector, context),
  663. option;
  664. mod = mod || {};
  665. if (elem) {
  666. switch (elem.nodeName.toLowerCase()) {
  667. case 'img':
  668. return (mod.altProp && elem.getAttribute(mod.altProp)) || elem.src || elem.getAttribute('src');
  669. case 'a':
  670. return elem.href || elem.getAttribute('href');
  671. case 'ul':
  672. return elem.children.length;
  673. case 'select':
  674. switch (mod.type) {
  675. case 'index':
  676. return elem.options.selectedIndex + 1;
  677. case 'value':
  678. option = elem.options[elem.options.selectedIndex + (mod.val || 0)] || {};
  679. return option.value;
  680. default:
  681. return elem.options.length;
  682. }
  683. break;
  684. default:
  685. switch (mod.type) {
  686. case 'index':
  687. return parseInt(elem.textContent);
  688. default:
  689. return elem.textContent;
  690. }
  691. }
  692. }
  693. };
  694.  
  695. var addStyle = function() {
  696. if (!this.MLStyle) {
  697. this.MLStyle = document.createElement('style');
  698. this.MLStyle.dataset.name = 'ml-style';
  699. document.head.appendChild(this.MLStyle);
  700. }
  701. this.MLStyle.textContent += [].join.call(arguments, '\n');
  702. };
  703.  
  704. var toStyleStr = function(obj, selector) {
  705. var stack = [],
  706. key;
  707. for (key in obj) {
  708. if (obj.hasOwnProperty(key)) {
  709. stack.push(key + ':' + obj[key]);
  710. }
  711. }
  712. if (selector) {
  713. return selector + '{' + stack.join(';') + '}';
  714. } else {
  715. return stack.join(';');
  716. }
  717. };
  718.  
  719. var throttle = function(callback, limit) {
  720. var wait = false;
  721. return function() {
  722. if (!wait) {
  723. callback();
  724. wait = true;
  725. setTimeout(function() {
  726. wait = false;
  727. }, limit);
  728. }
  729. };
  730. };
  731.  
  732. var createButton = function(text, action, styleStr) {
  733. var button = document.createElement('button');
  734. button.textContent = text;
  735. button.onclick = action;
  736. button.setAttribute('style', styleStr || '');
  737. return button;
  738. };
  739.  
  740. var getViewer = function(prevChapter, nextChapter) {
  741. var viewerCss = toStyleStr({
  742. 'background-color': 'black',
  743. 'font': '0.813em courier',
  744. 'text-align': 'center',
  745. }, 'body'),
  746. imagesCss = toStyleStr({
  747. 'margin-top': '10px',
  748. 'margin-bottom': '10px'
  749. }, '.ml-images'),
  750. imageCss = toStyleStr({
  751. 'max-width': '100%',
  752. 'display': 'block',
  753. 'margin': '3px auto'
  754. }, '.ml-images img'),
  755. counterCss = toStyleStr({
  756. 'background-color': '#222',
  757. 'color': 'white',
  758. 'border-radius': '10px',
  759. 'width': '30px',
  760. 'margin-left': 'auto',
  761. 'margin-right': 'auto',
  762. 'margin-top': '-12px',
  763. 'padding-left': '5px',
  764. 'padding-right': '5px',
  765. 'border': '1px solid white',
  766. 'z-index': '100',
  767. 'position': 'relative'
  768. }, '.ml-counter'),
  769. navCss = toStyleStr({
  770. 'text-decoration': 'none',
  771. 'color': 'white',
  772. 'background-color': '#444',
  773. 'padding': '3px 10px',
  774. 'border-radius': '5px',
  775. 'transition': '250ms'
  776. }, '.ml-chap-nav a'),
  777. navHoverCss = toStyleStr({
  778. 'background-color': '#555'
  779. }, '.ml-chap-nav a:hover'),
  780. boxCss = toStyleStr({
  781. 'position': 'fixed',
  782. 'background-color': '#222',
  783. 'color': 'white',
  784. 'padding': '7px',
  785. 'border-top-left-radius': '5px',
  786. 'cursor': 'default'
  787. }, '.ml-box'),
  788. statsCss = toStyleStr({
  789. 'bottom': '0',
  790. 'right': '0',
  791. 'opacity': '0.4',
  792. 'transition': '250ms'
  793. }, '.ml-stats'),
  794. statsCollapseCss = toStyleStr({
  795. 'color': 'orange',
  796. 'cursor': 'pointer'
  797. }, '.ml-stats-collapse'),
  798. statsHoverCss = toStyleStr({
  799. 'opacity': '1'
  800. }, '.ml-stats:hover'),
  801. manualReloadCss = toStyleStr({
  802. 'cursor': 'pointer'
  803. }, '.ml-manual-reload'),
  804. floatingMsgCss = toStyleStr({
  805. 'bottom': '50px',
  806. 'right': '0',
  807. 'border-bottom-left-radius': '5px',
  808. 'text-align': 'left',
  809. 'font': 'inherit',
  810. 'max-width': '95%',
  811. 'white-space': 'pre-wrap'
  812. }, '.ml-floating-msg'),
  813. infoButtonCss = toStyleStr({
  814. 'cursor': 'pointer'
  815. }, '.ml-info-button');
  816. // clear all styles and scripts
  817. var title = document.title;
  818. 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">';
  819. document.title = title;
  820. // navigation
  821. var nav = '<div class="ml-chap-nav">' + (prevChapter ? '<a class="ml-prev-chap" href="' + prevChapter + '">Prev Chapter</a> ' : '') +
  822. (storeGet('mAutoload') ? '' : '<a class="ml-exit" href="" data-ignore="true">Exit</a> ') +
  823. (nextChapter ? '<a class="ml-next-chap" href="' + nextChapter + '">Next Chapter</a>' : '') + '</div>';
  824. // message area
  825. var floatingMsg = '<pre class="ml-box ml-floating-msg"></pre>';
  826. // stats
  827. 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-info-button" title="See userscript information and help"></i> <i class="fa fa-refresh ml-manual-reload" title="Manually refresh next clicked image."></i></span></div>';
  828. // combine ui elements
  829. document.body.innerHTML = nav + '<div class="ml-images"></div>' + nav + floatingMsg + stats;
  830. // add all styles to the page
  831. addStyle(viewerCss, imagesCss, imageCss, counterCss, navCss, navHoverCss, statsCss, statsCollapseCss, statsHoverCss, manualReloadCss, boxCss, floatingMsgCss, infoButtonCss);
  832. // set up return UI object
  833. var UI = {
  834. images: getEl('.ml-images'),
  835. statsContent: getEl('.ml-stats-content'),
  836. statsPages: getEl('.ml-stats-pages'),
  837. statsCollapse: getEl('.ml-stats-collapse'),
  838. btnManualReload: getEl('.ml-manual-reload'),
  839. btnInfo: getEl('.ml-info-button'),
  840. floatingMsg: getEl('.ml-floating-msg'),
  841. btnNextChap: getEl('.ml-next-chap'),
  842. btnPrevChap: getEl('.ml-prev-chap'),
  843. btnExit: getEl('.ml-exit')
  844. };
  845. // message func
  846. var messageId = null;
  847. var showFloatingMsg = function(msg, timeout) {
  848. clearTimeout(messageId);
  849. log(msg);
  850. UI.floatingMsg.textContent = msg;
  851. UI.floatingMsg.style.display = msg ? '' : 'none';
  852. if(timeout) {
  853. messageId = setTimeout(function() {
  854. showFloatingMsg('');
  855. }, timeout);
  856. }
  857. };
  858. // configure initial state
  859. UI.floatingMsg.style.display = 'none';
  860. // set up listeners
  861. document.addEventListener('click', function(evt) {
  862. if (evt.target.nodeName === 'A' && !evt.target.dataset.ignore && evt.target.parentNode.className.indexOf('ml-chap-nav') !== -1) {
  863. log('next chapter will autoload');
  864. storeSet('autoload', 'yes');
  865. }
  866. });
  867. UI.btnManualReload.addEventListener('click', function(evt) {
  868. var imgClick = function(e) {
  869. var target = e.target;
  870. UI.images.removeEventListener('click', imgClick, false);
  871. UI.images.style.cursor = '';
  872. if(target.nodeName === 'IMG' && target.parentNode.className === 'ml-images') {
  873. showFloatingMsg('');
  874. if(!target.title) {
  875. showFloatingMsg('Reloading "' + target.src + '"', 3000);
  876. if(target.complete) target.onload = null;
  877. target.src = target.src + (target.src.indexOf('?') !== -1 ? '&' : '?') + new Date().getTime();
  878. }
  879. } else {
  880. showFloatingMsg('Cancelled manual reload...', 3000);
  881. }
  882. };
  883. showFloatingMsg('Left click the image you would like to reload.\nClick on the page margin to cancel.');
  884. UI.images.style.cursor = 'pointer';
  885. UI.images.addEventListener('click', imgClick, false);
  886. });
  887. UI.statsCollapse.addEventListener('click', function(evt) {
  888. var test = UI.statsCollapse.textContent === '>>';
  889. UI.statsContent.style.display = test ? 'none' : '';
  890. UI.statsCollapse.textContent = test ? '<<' : '>>';
  891. });
  892. UI.btnInfo.addEventListener('click', function(evt) {
  893. if(UI.floatingMsg.textContent) {
  894. showFloatingMsg('');
  895. } else {
  896. showFloatingMsg('Information:\n' +
  897. 'Nothing to say yet, but messages about new features will appear here in the future.\n\n' +
  898. 'Keybindings:\n' +
  899. 'Z - previous chapter\n' +
  900. 'X - exit\n' +
  901. 'C - next chapter\n\n' +
  902. 'Click the info button again to close this message.');
  903. }
  904. });
  905. // keybindings
  906. window.addEventListener('keydown', function(evt) {
  907. switch(evt.keyCode) {
  908. case 90: // z - prev chapter
  909. if(UI.btnPrevChap) {
  910. UI.btnPrevChap.click();
  911. }
  912. break;
  913. case 88: // x - exit
  914. UI.btnExit.click();
  915. break;
  916. case 67: // c - next chapter
  917. if(UI.btnNextChap) {
  918. UI.btnNextChap.click();
  919. }
  920. break;
  921. }
  922. });
  923. return UI;
  924. };
  925.  
  926. var getCounter = function(imgNum) {
  927. var counter = document.createElement('div');
  928. counter.classList.add('ml-counter');
  929. counter.textContent = imgNum;
  930. return counter;
  931. };
  932.  
  933. var addImage = function(src, loc, imgNum, callback) {
  934. if(!src) return;
  935. var image = new Image(),
  936. counter = getCounter(imgNum);
  937. image.onerror = function() {
  938. log('failed to load ' + src);
  939. image.onload = null;
  940. image.style.backgroundColor = 'white';
  941. image.title = 'Reload?';
  942. image.src = IMAGES.refresh_large;
  943. image.onclick = function() {
  944. image.onload = callback;
  945. image.title = '';
  946. image.src = src;
  947. };
  948. };
  949. image.onload = callback;
  950. image.src = src;
  951. loc.appendChild(image);
  952. loc.appendChild(counter);
  953. };
  954.  
  955. var loadManga = function(imp) {
  956. var ex = extractInfo.bind(imp),
  957. imgUrl = ex('img'),
  958. nextUrl = ex('next'),
  959. numPages = ex('numpages'),
  960. curPage = ex('curpage', {
  961. type: 'index'
  962. }) || 1,
  963. nextChapter = ex('nextchap', {
  964. type: 'value',
  965. val: (imp.invchap && -1) || 1
  966. }),
  967. prevChapter = ex('prevchap', {
  968. type: 'value',
  969. val: (imp.invchap && 1) || -1
  970. }),
  971. xhr = new XMLHttpRequest(),
  972. d = document.implementation.createHTMLDocument(),
  973. addAndLoad = function(img, next) {
  974. updateStats();
  975. addImage(img, UI.images, curPage, function() {
  976. pagesLoaded += 1;
  977. updateStats();
  978. });
  979. loadNextPage(next);
  980. },
  981. updateStats = function() {
  982. UI.statsPages.textContent = ' ' + pagesLoaded + '/' + curPage + ' loaded' + (numPages ? ', ' + numPages + ' total' : '');
  983. },
  984. getPageInfo = function() {
  985. var page = d.body;
  986. d.body.innerHTML = xhr.response;
  987. try {
  988. // find image and link to next page
  989. addAndLoad(ex('img', imp.imgmod, page), ex('next', null, page));
  990. } catch (e) {
  991. log('error getting details from next page, assuming end of chapter.');
  992. }
  993. },
  994. loadNextPage = function(url) {
  995. if (mLoadLess && count % loadInterval === 0) {
  996. if (resumeUrl) {
  997. resumeUrl = null;
  998. } else {
  999. resumeUrl = url;
  1000. log('waiting for user to scroll further before loading more images, loaded ' + count + ' pages so far, next url is ' + resumeUrl);
  1001. return;
  1002. }
  1003. }
  1004. if (curPage + 1 > numPages) {
  1005. log('reached "numPages" ' + numPages + ', assuming end of chapter');
  1006. return;
  1007. }
  1008. if (lastUrl === url) {
  1009. log('last url (' + lastUrl + ') is the same as current (' + url + '), assuming end of chapter');
  1010. return;
  1011. }
  1012. curPage += 1;
  1013. count += 1;
  1014. lastUrl = url;
  1015. if (imp.pages) {
  1016. imp.pages(url, curPage, addAndLoad, ex, getPageInfo);
  1017. } else {
  1018. xhr.open('get', url);
  1019. xhr.onload = getPageInfo;
  1020. xhr.onerror = function() {
  1021. log('failed to load page, aborting', 'error');
  1022. };
  1023. xhr.send();
  1024. }
  1025. },
  1026. count = 1,
  1027. pagesLoaded = curPage - 1,
  1028. loadInterval = 10,
  1029. lastUrl, UI, resumeUrl;
  1030.  
  1031. if (!imgUrl || !nextUrl) {
  1032. log('failed to retrieve ' + (!imgUrl ? 'image url' : 'next page url'), 'exit');
  1033. }
  1034.  
  1035. UI = getViewer(prevChapter, nextChapter);
  1036.  
  1037. UI.statsPages.textContent = ' 0/1 loaded, ' + numPages + ' total';
  1038.  
  1039. if (mLoadLess) {
  1040. window.addEventListener('scroll', throttle(function(e) {
  1041. if (!resumeUrl) return; // exit early if we don't have a position to resume at
  1042. if(!UI.imageHeight) {
  1043. UI.imageHeight = getEl('.ml-images img').clientHeight;
  1044. }
  1045. var scrollBottom = document.body.scrollHeight - ((document.body.scrollTop || document.documentElement.scrollTop) + window.innerHeight);
  1046. if (scrollBottom < UI.imageHeight * 2) {
  1047. log('user scroll nearing end, loading more images starting from ' + resumeUrl);
  1048. loadNextPage(resumeUrl);
  1049. }
  1050. }, 100));
  1051. }
  1052.  
  1053. addAndLoad(imgUrl, nextUrl);
  1054.  
  1055. };
  1056.  
  1057. var pageUrl = window.location.href,
  1058. btnLoadCss = toStyleStr({
  1059. 'position': 'fixed',
  1060. 'bottom': 0,
  1061. 'right': 0,
  1062. 'padding': '5px',
  1063. 'margin': '0 10px 10px 0',
  1064. 'z-index': '99999'
  1065. }),
  1066. btnLoad;
  1067.  
  1068. // used when switching chapters
  1069. var autoload = storeGet('autoload');
  1070. // manually set by user in menu
  1071. var mAutoload = storeGet('mAutoload') || false;
  1072. // should we load less pages at a time?
  1073. var mLoadLess = storeGet('mLoadLess') === false ? false : true;
  1074.  
  1075. // clear autoload
  1076. storeDel('autoload');
  1077.  
  1078. // register menu commands
  1079. if (typeof GM_registerMenuCommand === 'function') {
  1080. GM_registerMenuCommand('ML: ' + (mAutoload ? 'Disable' : 'Enable') + ' manga autoload', function() {
  1081. storeSet('mAutoload', !mAutoload);
  1082. window.location.reload();
  1083. });
  1084. GM_registerMenuCommand('ML: Load ' + (mLoadLess ? 'full chapter in one go' : '10 pages at a time'), function() {
  1085. storeSet('mLoadLess', !mLoadLess);
  1086. window.location.reload();
  1087. });
  1088. }
  1089.  
  1090. log('starting...');
  1091.  
  1092. var success = implementations.some(function(imp) {
  1093. var intervalId;
  1094. if (imp.match && (new RegExp(imp.match, 'i')).test(pageUrl)) {
  1095. if (BM_MODE || mAutoload || autoload) {
  1096. log('autoloading...');
  1097. if(typeof imp.wait === 'function') {
  1098. log('Waiting for load condition to be fulfilled');
  1099. intervalId = setInterval(function() {
  1100. if(imp.wait()) {
  1101. log('Condition fulfilled, loading');
  1102. clearInterval(intervalId);
  1103. loadManga(imp);
  1104. }
  1105. }, 200);
  1106. } else {
  1107. setTimeout(loadManga.bind(null, imp), imp.wait || 0);
  1108. }
  1109. return true;
  1110. }
  1111. // append button to dom that will trigger the page load
  1112. btnLoad = createButton('Load Manga', function(evt) {
  1113. loadManga(imp);
  1114. this.remove();
  1115. }, btnLoadCss);
  1116. document.body.appendChild(btnLoad);
  1117. return true;
  1118. }
  1119. });
  1120.  
  1121. if (!success) {
  1122. log('no implementation for ' + pageUrl, 'error');
  1123. }