Greasy Fork is available in English.

Manga Loader

Loads manga chapter into one page in a long strip format, supports switching chapters and works for a variety of sites, minimal script with no dependencies, easy to implement new sites, loads quickly and works on mobile devices through bookmarklet

目前為 2015-11-02 提交的版本,檢視 最新版本

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