您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
e-hentai & exhentai image url retriever
// ==UserScript== // @name e-hentai retriever // @namespace https://github.com/s25g5d4/e-hentai-retriever // @description e-hentai & exhentai image url retriever // @include /^https?://e-hentai.org/s/.*/ // @include /^https?://exhentai.org/s/.*/ // @version 4.4.1 // @author s25g5d4 // @homepageURL https://github.com/s25g5d4/e-hentai-retriever // @grant GM_xmlhttpRequest // @grant unsafeWindow // ==/UserScript== (function () { 'use strict'; function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = "#i3 a {\n display: inline-block;\n position: relative;\n}\n\n#i3.force-img-full-height a img {\n\twidth:auto !important;\n\theight:100vh !important;\n}\n\n.close,\n.swap,\n.page-number {\n\tposition: absolute;\n\twidth: 32px;\n\theight: 32px;\n\tmargin: 8px;\n\tz-index: 999;\n opacity: 0;\n transition: opacity 0.25s;\n\tbackground-color: rgba(255, 255, 255, 0.3);\n}\n\n.close {\n\ttop: 0;\n\tright: 0;\n\tbackground-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAmElEQVRYhc2USQ6AMAwD+/9PhyuRAs3igVrqCTR2m2UtL1u8Hj3sdkjz0MOCQ5o7j+iDOsTWgwyRZhMhykxliDZLEWLMmABkr9gByfuoAsQmKQPGd8mbAW7eDYHoV/NsCFxH3/6I+h8xAZ/tgMo/mDkWogOUhZiAxiEUt2gzlHUss4hOTjPJWd6y8UXSDaFWqQyUUo1Iy3lcN6p2mB7qGCwAAAAASUVORK5CYII=);\n}\n\n.swap {\n\ttop: 0;\n\tleft: 0;\n\tbackground-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAdklEQVRYhe3TwQ0AIQhE0enA7b9JS2BPezExERb9HpiEI/KiIlUqZ2I3AFCECUaYYMQI+IXokwMjhQNCCPwGmqTHWal/IJKrtsDVlA3Y17Bwnns4/lb4TzX5161lDo9UJ4eHAZmIMGCGOB4cMCKw4IAPUalszwvTfbCPlftI4QAAAABJRU5ErkJggg==);\n}\n\n.page-number {\n\tbottom: 0;\n\tright: 0;\n\tfont-size: 16px;\n\tline-height: 32px;\n\tcolor: black;\n}\n\n.close:hover,\n.swap:hover,\n.page-number:hover {\n opacity: 1;\n}\n\n.hidden {\n\tdisplay: none !important;\n}\n\n.show-hidden {\n\tfont-size: larger;\n\tmargin-left: 5px;\n}\n"; styleInject(css_248z); class Queue { constructor(limit, timeout = 0, delay = 0) { this.limit = limit; this.timeout = timeout; this.delay = delay; this.slot = []; this.q = []; } queue(executor, name) { const job = new Promise((resolve, reject) => { this.q.push({ name, run: executor, resolve, reject, isTimeout: false, timeoutId: undefined }); }); // console.log(`queue: job ${name} queued`); this.dequeue(); return job; } dequeue() { const { q, slot, limit, timeout, delay } = this; if (slot.length < limit && q.length >= 1) { const job = q.shift(); slot.push(job); // console.log(`queue: job ${job.name} started`); if (timeout) { job.timeoutId = window.setTimeout(() => this.jobTimeout(job), timeout); } const onFulfilled = (data) => { if (job.isTimeout) { return; } this.removeJob(job); window.setTimeout(() => this.dequeue(), delay); // force dequeue() run after current dequeue() if (job.timeoutId) { window.clearTimeout(job.timeoutId); } // console.log(`queue: job ${job.name} resolved`); job.resolve(data); }; const onRejected = (reason) => { if (job.isTimeout) { return; } this.removeJob(job); setTimeout(() => this.dequeue(), delay); if (job.timeoutId) { window.clearTimeout(job.timeoutId); } // console.log(`queue: job ${job.name} rejected`); job.reject(reason); }; job.run(onFulfilled, onRejected); } } jobTimeout(job) { this.removeJob(job); // console.log(`queue: job ${job.name} timeout`); job.reject(new Error(`queue: job ${job.name} timeout`)); job = null; } removeJob(job) { let index = this.slot.indexOf(job); if (index >= 0) { this.slot.splice(index, 1); return; } index = this.q.indexOf(job); if (index >= 0) { this.q.splice(index, 1); } } } // Origin from window.fetch polyfill // https://github.com/github/fetch // License https://github.com/github/fetch/blob/master/LICENSE const COFetch = (input, init = {}) => { let request; if (Request.prototype.isPrototypeOf(input) && !init) { request = input; } else { request = new Request(input, init); } const headers = Object.assign({}, request.headers); let anonymous = true; if (request.credentials === 'include') { anonymous = false; } const onload = (resolve, reject, gmxhr) => { const init = { url: gmxhr.finalUrl || request.url, status: gmxhr.status, statusText: gmxhr.statusText, headers: undefined }; try { const rawHeaders = gmxhr.responseHeaders.trim().replace(/\r\n(\s+)/g, '$1').split('\r\n').map(e => e.split(/:/)); const header = new Headers(); rawHeaders.forEach(e => { header.append(e[0].trim(), e[1].trim()); }); init.headers = header; const res = new Response(gmxhr.response, init); resolve(res); } catch (e) { reject(e); } }; const onerror = (resolve, reject, gmxhr) => { reject(new TypeError('Network request failed')); }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ anonymous, method: request.method, url: request.url, headers: headers, responseType: 'blob', data: init.body, onload: onload.bind(null, resolve, reject), onerror: onerror.bind(null, resolve, reject) }); }); }; var domain; // This constructor is used to store event handlers. Instantiating this is // faster than explicitly calling `Object.create(null)` to get a "clean" empty // object (tested with v8 v4.9). function EventHandlers() {} EventHandlers.prototype = Object.create(null); function EventEmitter() { EventEmitter.init.call(this); } // nodejs oddity // require('events') === require('events').EventEmitter EventEmitter.EventEmitter = EventEmitter; EventEmitter.usingDomains = false; EventEmitter.prototype.domain = undefined; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; EventEmitter.init = function() { this.domain = null; if (EventEmitter.usingDomains) { // if there is an active domain, then attach to it. if (domain.active ) ; } if (!this._events || this._events === Object.getPrototypeOf(this)._events) { this._events = new EventHandlers(); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || isNaN(n)) throw new TypeError('"n" argument must be a positive number'); this._maxListeners = n; return this; }; function $getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return $getMaxListeners(this); }; // These standalone emit* functions are used to optimize calling of event // handlers for fast cases because emit() itself often has a variable number of // arguments and can be deoptimized because of that. These functions always have // the same number of arguments and thus do not get deoptimized, so the code // inside them can execute faster. function emitNone(handler, isFn, self) { if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } function emitOne(handler, isFn, self, arg1) { if (isFn) handler.call(self, arg1); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1); } } function emitTwo(handler, isFn, self, arg1, arg2) { if (isFn) handler.call(self, arg1, arg2); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2); } } function emitThree(handler, isFn, self, arg1, arg2, arg3) { if (isFn) handler.call(self, arg1, arg2, arg3); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2, arg3); } } function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } EventEmitter.prototype.emit = function emit(type) { var er, handler, len, args, i, events, domain; var doError = (type === 'error'); events = this._events; if (events) doError = (doError && events.error == null); else if (!doError) return false; domain = this.domain; // If there is no 'error' event listener then throw. if (doError) { er = arguments[1]; if (domain) { if (!er) er = new Error('Uncaught, unspecified "error" event'); er.domainEmitter = this; er.domain = domain; er.domainThrown = false; domain.emit('error', er); } else if (er instanceof Error) { throw er; // Unhandled 'error' event } else { // At least give some kind of context to the user var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); err.context = er; throw err; } return false; } handler = events[type]; if (!handler) return false; var isFn = typeof handler === 'function'; len = arguments.length; switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: emitOne(handler, isFn, this, arguments[1]); break; case 3: emitTwo(handler, isFn, this, arguments[1], arguments[2]); break; case 4: emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); break; // slower default: args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; emitMany(handler, isFn, this, args); } return true; }; function _addListener(target, type, listener, prepend) { var m; var events; var existing; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = target._events; if (!events) { events = target._events = new EventHandlers(); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } existing = events[type]; } if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { // If we've already got an array, just append. if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } // Check for listener leak if (!existing.warned) { m = $getMaxListeners(target); if (m && m > 0 && existing.length > m) { existing.warned = true; var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + type + ' listeners added. ' + 'Use emitter.setMaxListeners() to increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; emitWarning(w); } } } return target; } function emitWarning(e) { typeof console.warn === 'function' ? console.warn(e) : console.log(e); } EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); }; function _onceWrap(target, type, listener) { var fired = false; function g() { target.removeListener(type, g); if (!fired) { fired = true; listener.apply(target, arguments); } } g.listener = listener; return g; } EventEmitter.prototype.once = function once(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.prependListener(type, _onceWrap(this, type, listener)); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = this._events; if (!events) return this; list = events[type]; if (!list) return this; if (list === listener || (list.listener && list.listener === listener)) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else { delete events[type]; if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { position = -1; for (i = list.length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { originalListener = list[i].listener; position = i; break; } } if (position < 0) return this; if (list.length === 1) { list[0] = undefined; if (--this._eventsCount === 0) { this._events = new EventHandlers(); return this; } else { delete events[type]; } } else { spliceOne(list, position); } if (events.removeListener) this.emit('removeListener', type, originalListener || listener); } return this; }; // Alias for removeListener added in NodeJS 10.0 // https://nodejs.org/api/events.html#events_emitter_off_eventname_listener EventEmitter.prototype.off = function(type, listener){ return this.removeListener(type, listener); }; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events; events = this._events; if (!events) return this; // not listening for removeListener, no need to emit if (!events.removeListener) { if (arguments.length === 0) { this._events = new EventHandlers(); this._eventsCount = 0; } else if (events[type]) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else delete events[type]; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { var keys = Object.keys(events); for (var i = 0, key; i < keys.length; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = new EventHandlers(); this._eventsCount = 0; return this; } listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners) { // LIFO order do { this.removeListener(type, listeners[listeners.length - 1]); } while (listeners[0]); } return this; }; EventEmitter.prototype.listeners = function listeners(type) { var evlistener; var ret; var events = this._events; if (!events) ret = []; else { evlistener = events[type]; if (!evlistener) ret = []; else if (typeof evlistener === 'function') ret = [evlistener.listener || evlistener]; else ret = unwrapListeners(evlistener); } return ret; }; EventEmitter.listenerCount = function(emitter, type) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(type); } else { return listenerCount.call(emitter, type); } }; EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; if (events) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener) { return evlistener.length; } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; }; // About 1.5x faster than the two-arg version of Array#splice(). function spliceOne(list, index) { for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); } function arrayClone(arr, i) { var copy = new Array(i); while (i--) copy[i] = arr[i]; return copy; } function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { ret[i] = arr[i].listener || arr[i]; } return ret; } class EhRetriever extends EventEmitter { constructor(url, html) { super(); const testEXHentaiUrl = /^https?:\/\/(?:e-|ex)hentai\.org\//; if (typeof url !== 'string') { throw new TypeError('invalid `url`, expected a string'); } if (!testEXHentaiUrl.test(url)) { throw new TypeError(`invalid url: ${url}`); } this.url = url; this.html = html; this.gallery = { gid: '', token: '' }; this.referer = url; this.showkey = ''; this.ehentaiHost = testEXHentaiUrl.exec(url)[0].slice(0, -1); this.q = new Queue(3, 3000, 1000); this.pages = this.init(); this.pages.then(() => this.emit('ready')); } async init() { if (!this.html) { this.html = await this.fetch(this.url).then(res => res.text()); } const galleryURL = this.html.match(/hentai\.org\/g\/(\d+)\/([a-z0-9]+)/i); const showkey = this.html.match(/showkey="([^"]+)"/i); if (galleryURL) { this.gallery.gid = galleryURL[1]; this.gallery.token = galleryURL[2]; } else { throw new Error("Can't get gallery URL"); } if (showkey) { this.showkey = showkey[1]; } else { throw new Error("Can't get showkey"); } return await this.getAllPageURL(); } async getAllPageURL() { const { ehentaiHost, gallery: { gid, token } } = this; const firstPage = await this.fetch(`${ehentaiHost}/g/${gid}/${token}`).then(res => res.text()); let pageNum; const pageLinksTable = firstPage.match(/<table[^>]*class="ptt"[^>]*>((?:[^<]*)(?:<(?!\/table>)[^<]*)*)<\/table>/); if (pageLinksTable) { const pageLinks = pageLinksTable[1].match(/g\/[^/]+\/[^/]+\/\?p=\d+/g); if (pageLinks) { pageNum = Math.max.apply(null, pageLinks.map(e => parseInt(/\d+$/.exec(e)[0], 10))); } else { pageNum = 0; } } else { throw new Error('Cant get page numbers'); } const allPages = await Promise.all(Array(pageNum).fill(undefined).map((e, i) => { return this.fetch(`${ehentaiHost}/g/${gid}/${token}/?p=${i + 1}`).then(res => res.text()); })); allPages.unshift(firstPage); return allPages .map(p => this.parsePage(p)) .reduce((p, c) => p.concat(c), []); // 2d array to 1d } parsePage(page) { const gdtMatcher = /<div[^>]*id="gdt"[^>]*>/g; gdtMatcher.exec(page); const gtbMatcher = /<div[^>]*class="gtb"[^>]*>/g; gtbMatcher.lastIndex = gdtMatcher.lastIndex; gtbMatcher.exec(page); const gdtContent = page.substring(gdtMatcher.lastIndex, gtbMatcher.lastIndex); return Array.from(gdtContent.matchAll(/s\/(\w+)\/\d+-(\d+)/g)).map(([, imgkey, page]) => ({ imgkey, page: parseInt(page, 10) })); } fetch(url, options = {}) { if (typeof url !== 'string') { return Promise.reject(new TypeError('invalid `url`, expected a string')); } if (url.search(/^https?:\/\//) < 0) { return Promise.reject(new TypeError(`invalid url: ${url}`)); } const cofetchOptions = { method: 'GET', credentials: 'include', headers: { 'User-Agent': navigator.userAgent, Referer: this.referer } }; for (const key of Object.keys(options)) { if (key === 'headers') { Object.assign(cofetchOptions.headers, options.headers); } else { cofetchOptions[key] = options[key]; } } return this.q.queue((resolve, reject) => { COFetch(url, cofetchOptions).then(resolve).catch(reject); }, `Fetch ${url} ${JSON.stringify(cofetchOptions)}`); } async retrieve(start = 0, stop = -1) { const pages = await this.pages; if (start < 0 || start >= pages.length || isNaN(start)) { throw new RangeError(`invalid start number: ${start}`); } if (stop < 0) { stop = pages.length - 1; } else if (stop < start || stop >= pages.length || isNaN(stop)) { throw new RangeError(`invalid stop number: ${stop}, start: ${start}`); } const retrievePages = pages.slice(start, stop + 1); const loadPage = async (e) => { if (e.imgsrc && e.filename) { return Promise.resolve(e); } const fetchPage = await this.fetch(`${this.ehentaiHost}/api.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, // assign e = {'imgkey': ..., 'page': ...} to object literal {'method': ..., 'gid': ..., 'showkey': ...} // does not modify e body: JSON.stringify(Object.assign({ method: 'showpage', gid: this.gallery.gid, showkey: this.showkey }, e)) }).then(res => res.json()); this.emit('load', { current: e.page - start, total: stop - start + 1 }); return fetchPage; }; const imagePages = await Promise.all(retrievePages.map(loadPage)); imagePages.forEach((e, i) => { retrievePages[i].filename = e.i.match(/>([^:]+):/)[1].trim(); retrievePages[i].imgsrc = e.i3.match(/src="([^"]+)"/)[1]; retrievePages[i].failnl = new Set([e.i6.match(/nl\('([^']+)'/)[1]]); retrievePages[i].style = e.i3.match(/style="([^"]+)"/)[1]; retrievePages[i].url = e.s; }); return retrievePages; } async fail(index) { const pages = await this.pages; const failPage = pages[index - 1]; const failnl = [...failPage.failnl.values()].map(e => `nl=${e}`).join('&'); const res = await this.fetch(`${this.ehentaiHost}/${failPage.url}?${failnl}`).then(res => res.text()); const parsed = res.match(/<img[^>]*id="img"[^>]*src="([^"]+)"[^>]*.*onclick="return nl\('([^']+)'\)/i); if (parsed) { failPage.imgsrc = parsed[1]; failPage.failnl.add(parsed[2]); return failPage; } return null; } } const LoadTimeout = 10000; // helper functions const $ = (selector) => document.querySelector(selector); const $$ = (selector) => Array.from(document.querySelectorAll(selector)); const buttonsFragment = document.createDocumentFragment(); const buttonReverse = document.createElement('button'); const buttonDoubleFrame = document.createElement('button'); const buttonRetrieve = document.createElement('button'); const buttonRange = document.createElement('button'); const buttonFullHeight = document.createElement('button'); buttonsFragment.appendChild(buttonReverse); buttonsFragment.appendChild(buttonDoubleFrame); buttonsFragment.appendChild(buttonFullHeight); buttonsFragment.appendChild(buttonRetrieve); buttonsFragment.appendChild(buttonRange); buttonReverse.textContent = 'Reverse'; buttonDoubleFrame.textContent = 'Double Frame'; buttonRetrieve.textContent = 'Retrieve!'; buttonRange.textContent = 'Set Range'; buttonFullHeight.textContent = 'View Height'; $('#i1').insertBefore(buttonsFragment, $('#i2')); let ehentaiResize; let originalWidth; let ehr; let showHiddenImageLink = false; const reload = (event) => { event.stopPropagation(); event.preventDefault(); const target = event.target; if (target.dataset.locked === 'true') { return; } target.dataset.locked = 'true'; ehr.fail(parseInt(target.dataset.page, 10)).then(imgInfo => { target.src = imgInfo.imgsrc; target.parentElement.href = imgInfo.imgsrc; target.dataset.locked = 'false'; }); }; const showImage = (event) => { event.stopPropagation(); event.preventDefault(); $$('#i3 a').forEach(e => { e.classList.remove('hidden'); }); event.target.remove(); showHiddenImageLink = false; }; const hideImage = (event) => { event.stopPropagation(); event.preventDefault(); event.target.parentElement.classList.add('hidden'); if (!showHiddenImageLink) { const showHiddenImage = document.createElement('a'); showHiddenImage.href = ''; showHiddenImage.textContent = 'show hidden image'; showHiddenImage.classList.add('show-hidden'); showHiddenImage.addEventListener('click', showImage); buttonRetrieve.insertAdjacentElement('afterend', showHiddenImage); showHiddenImageLink = true; } }; const swapImage = (event) => { event.stopPropagation(); event.preventDefault(); const right = event.target.parentElement; const left = right.previousElementSibling; if (left) { left.parentElement.insertBefore(right, left); } }; const scrollNextImage = (event) => { if (event.keyCode !== 9) { return; } event.preventDefault(); const bodyOffset = document.body.getBoundingClientRect().top; const pageY = window.pageYOffset; const imgs = Array.from($$('#i3 a img')); const isLast = !imgs.some((e, i) => { const imgOffset = e.getBoundingClientRect().top - bodyOffset; if (pageY - imgOffset < -1) { window.scrollTo(0, imgOffset); return true; } }); if (isLast) { window.scrollTo(0, imgs[0].getBoundingClientRect().top - bodyOffset); } }; buttonReverse.addEventListener('click', event => { const i3 = $('#i3'); const imgs = $$('#i3 > a[data-page]'); if (buttonReverse.textContent === 'Reverse') { buttonReverse.textContent = 'Original Order'; imgs .sort((a, b) => a.offsetTop - b.offsetTop) .reduce((p, c) => { const l = p.at(-1); if (!l || l.at(-1).offsetTop !== c.offsetTop) { return [...p, [c]]; } l.push(c); return p; }, []) .flatMap(e => e.reverse()) .forEach(e => i3.appendChild(e)); } else { buttonReverse.textContent = 'Reverse'; imgs .sort((a, b) => parseInt(a.dataset.page, 10) - parseInt(b.dataset.page, 10)) .forEach(e => i3.appendChild(e)); } }); buttonDoubleFrame.addEventListener('click', event => { if (!ehentaiResize) { try { ehentaiResize = unsafeWindow.onresize; } catch (e) { console.log(e); } } const imgWidths = $$('#i3 a:not(.hidden) img').map(e => e.getBoundingClientRect().width); const avg = imgWidths.reduce((p, c) => p + c) / imgWidths.length; const filtered = imgWidths.filter(v => (v < avg * 1.5 && v > avg * 0.5)); const filteredMax = Math.max.apply(null, filtered); if (!originalWidth) { originalWidth = parseInt($('#i1').style.width, 10); } if (buttonDoubleFrame.textContent === 'Double Frame') { buttonDoubleFrame.textContent = 'Reset Frame'; try { unsafeWindow.onresize = null; } catch (e) { console.log(e); } $('#i1').style.maxWidth = (filteredMax * 2 + 20) + 'px'; $('#i1').style.width = (filteredMax * 2 + 20) + 'px'; } else { buttonDoubleFrame.textContent = 'Double Frame'; try { unsafeWindow.onresize = ehentaiResize; ehentaiResize(); } catch (e) { console.log(e); $('#i1').style.maxWidth = originalWidth + 'px'; $('#i1').style.width = originalWidth + 'px'; } } }); buttonRetrieve.addEventListener('click', event => { buttonRetrieve.setAttribute('disabled', ''); buttonRange.setAttribute('disabled', ''); buttonRetrieve.textContent = 'Initializing...'; if (!ehr) { ehr = new EhRetriever(location.href, document.body.innerHTML); } ehr.on('ready', () => { buttonRetrieve.textContent = `Ready to retrieve`; }); ehr.on('load', (progress) => { buttonRetrieve.textContent = `Retrieving ${progress.current}/${progress.total}`; }); let retrieve; if ($('#ehrstart')) { const start = parseInt($('#ehrstart').value, 10); const stop = parseInt($('#ehrstop').value, 10); const pageNumMax = parseInt($('div.sn').textContent.match(/\/\s*(\d+)/)[1], 10); if (stop < start || start <= 0 || start > pageNumMax || stop > pageNumMax) { window.alert(`invalid range: ${start} - ${stop}, accepted range: 1 - ${pageNumMax}`); buttonRetrieve.textContent = 'Retrieve!'; buttonRetrieve.removeAttribute('disabled'); return; } retrieve = ehr.retrieve(start - 1, stop - 1); $('#ehrsetrange').remove(); } else { retrieve = ehr.retrieve(); buttonRange.remove(); } retrieve.then(pages => { $('#i3 a').remove(); const template = document.createElement('template'); template.innerHTML = pages .map(e => ` <a href="${e.imgsrc}" data-page="${e.page}"> <img src="${e.imgsrc}" style="${e.style}" data-page="${e.page}" data-locked="false" /> <div class="close"></div> <div class="swap"></div> <div class="page-number">${e.page}</div> </a>`) .join(''); template.content.querySelectorAll('img').forEach(e => { e.addEventListener('error', function onError(event) { e.removeEventListener('error', onError); reload(event); }); let timeout; { timeout = window.setTimeout(() => { console.log(`timeout: page ${e.dataset.page}`); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); e.dispatchEvent(clickEvent); }, LoadTimeout); e.addEventListener('load', function onload() { e.removeEventListener('load', onload); clearTimeout(timeout); }); } }); $('#i3').appendChild(template.content); $('#i3').addEventListener('click', event => { if (event.target.nodeName === 'IMG') { reload(event); } else if (event.target.classList.contains('close')) { hideImage(event); } else if (event.target.classList.contains('swap')) { swapImage(event); } else if (event.target.classList.contains('page-number')) { event.preventDefault(); event.stopPropagation(); } }); buttonRetrieve.textContent = 'Done!'; buttonDoubleFrame.removeAttribute('disabled'); buttonFullHeight.removeAttribute('disabled'); document.onkeydown = null; document.addEventListener('keydown', scrollNextImage); }).catch(e => { console.log(e); }); }); buttonRange.addEventListener('click', event => { // override e-hentai's viewing shortcut document.onkeydown = undefined; const pageNum = $('div.sn').textContent.match(/(\d+)\s*\/\s*(\d+)/).slice(1); buttonRange.insertAdjacentHTML('afterend', `<span id="ehrsetrange"><input type="number" id="ehrstart" value="${pageNum[0]}" min="1" max="${pageNum[1]}"> - <input type="number" id="ehrstop" value="${pageNum[1]}" min="1" max="${pageNum[1]}"></span>`); buttonRange.remove(); }); buttonFullHeight.addEventListener('click', event => { $('#i3').classList.toggle('force-img-full-height'); }); })();