e-hentai-infinite-scroll

Exhentai infinite scroll scripts.

// ==UserScript==
// @name         e-hentai-infinite-scroll
// @namespace    https://github.com/IronKinoko/userscripts/tree/master/packages/e-hentai-infinite-scroll
// @version      1.3.9
// @description  Exhentai infinite scroll scripts.
// @author       IronKinoko
// @match        https://e-hentai.org/*
// @match        https://exhentai.org/*
// @grant        none
// @require      https://unpkg.com/jquery@3.6.1/dist/jquery.min.js
// ==/UserScript==
(function () {
  'use strict';

  /** Detect free variable `global` from Node.js. */
  var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;

  /** Detect free variable `self`. */
  var freeSelf = typeof self == 'object' && self && self.Object === Object && self;

  /** Used as a reference to the global object. */
  var root = freeGlobal || freeSelf || Function('return this')();

  /** Built-in value references. */
  var Symbol = root.Symbol;

  /** Used for built-in method references. */
  var objectProto$1 = Object.prototype;

  /** Used to check objects for own properties. */
  var hasOwnProperty = objectProto$1.hasOwnProperty;

  /**
   * Used to resolve the
   * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
   * of values.
   */
  var nativeObjectToString$1 = objectProto$1.toString;

  /** Built-in value references. */
  var symToStringTag$1 = Symbol ? Symbol.toStringTag : undefined;

  /**
   * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
   *
   * @private
   * @param {*} value The value to query.
   * @returns {string} Returns the raw `toStringTag`.
   */
  function getRawTag(value) {
    var isOwn = hasOwnProperty.call(value, symToStringTag$1),
        tag = value[symToStringTag$1];

    try {
      value[symToStringTag$1] = undefined;
      var unmasked = true;
    } catch (e) {}

    var result = nativeObjectToString$1.call(value);
    if (unmasked) {
      if (isOwn) {
        value[symToStringTag$1] = tag;
      } else {
        delete value[symToStringTag$1];
      }
    }
    return result;
  }

  /** Used for built-in method references. */
  var objectProto = Object.prototype;

  /**
   * Used to resolve the
   * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
   * of values.
   */
  var nativeObjectToString = objectProto.toString;

  /**
   * Converts `value` to a string using `Object.prototype.toString`.
   *
   * @private
   * @param {*} value The value to convert.
   * @returns {string} Returns the converted string.
   */
  function objectToString(value) {
    return nativeObjectToString.call(value);
  }

  /** `Object#toString` result references. */
  var nullTag = '[object Null]',
      undefinedTag = '[object Undefined]';

  /** Built-in value references. */
  var symToStringTag = Symbol ? Symbol.toStringTag : undefined;

  /**
   * The base implementation of `getTag` without fallbacks for buggy environments.
   *
   * @private
   * @param {*} value The value to query.
   * @returns {string} Returns the `toStringTag`.
   */
  function baseGetTag(value) {
    if (value == null) {
      return value === undefined ? undefinedTag : nullTag;
    }
    return (symToStringTag && symToStringTag in Object(value))
      ? getRawTag(value)
      : objectToString(value);
  }

  /**
   * Checks if `value` is object-like. A value is object-like if it's not `null`
   * and has a `typeof` result of "object".
   *
   * @static
   * @memberOf _
   * @since 4.0.0
   * @category Lang
   * @param {*} value The value to check.
   * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
   * @example
   *
   * _.isObjectLike({});
   * // => true
   *
   * _.isObjectLike([1, 2, 3]);
   * // => true
   *
   * _.isObjectLike(_.noop);
   * // => false
   *
   * _.isObjectLike(null);
   * // => false
   */
  function isObjectLike(value) {
    return value != null && typeof value == 'object';
  }

  /** `Object#toString` result references. */
  var symbolTag = '[object Symbol]';

  /**
   * Checks if `value` is classified as a `Symbol` primitive or object.
   *
   * @static
   * @memberOf _
   * @since 4.0.0
   * @category Lang
   * @param {*} value The value to check.
   * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
   * @example
   *
   * _.isSymbol(Symbol.iterator);
   * // => true
   *
   * _.isSymbol('abc');
   * // => false
   */
  function isSymbol(value) {
    return typeof value == 'symbol' ||
      (isObjectLike(value) && baseGetTag(value) == symbolTag);
  }

  /** Used to match a single whitespace character. */
  var reWhitespace = /\s/;

  /**
   * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace
   * character of `string`.
   *
   * @private
   * @param {string} string The string to inspect.
   * @returns {number} Returns the index of the last non-whitespace character.
   */
  function trimmedEndIndex(string) {
    var index = string.length;

    while (index-- && reWhitespace.test(string.charAt(index))) {}
    return index;
  }

  /** Used to match leading whitespace. */
  var reTrimStart = /^\s+/;

  /**
   * The base implementation of `_.trim`.
   *
   * @private
   * @param {string} string The string to trim.
   * @returns {string} Returns the trimmed string.
   */
  function baseTrim(string) {
    return string
      ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '')
      : string;
  }

  /**
   * Checks if `value` is the
   * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
   * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
   *
   * @static
   * @memberOf _
   * @since 0.1.0
   * @category Lang
   * @param {*} value The value to check.
   * @returns {boolean} Returns `true` if `value` is an object, else `false`.
   * @example
   *
   * _.isObject({});
   * // => true
   *
   * _.isObject([1, 2, 3]);
   * // => true
   *
   * _.isObject(_.noop);
   * // => true
   *
   * _.isObject(null);
   * // => false
   */
  function isObject(value) {
    var type = typeof value;
    return value != null && (type == 'object' || type == 'function');
  }

  /** Used as references for various `Number` constants. */
  var NAN = 0 / 0;

  /** Used to detect bad signed hexadecimal string values. */
  var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;

  /** Used to detect binary string values. */
  var reIsBinary = /^0b[01]+$/i;

  /** Used to detect octal string values. */
  var reIsOctal = /^0o[0-7]+$/i;

  /** Built-in method references without a dependency on `root`. */
  var freeParseInt = parseInt;

  /**
   * Converts `value` to a number.
   *
   * @static
   * @memberOf _
   * @since 4.0.0
   * @category Lang
   * @param {*} value The value to process.
   * @returns {number} Returns the number.
   * @example
   *
   * _.toNumber(3.2);
   * // => 3.2
   *
   * _.toNumber(Number.MIN_VALUE);
   * // => 5e-324
   *
   * _.toNumber(Infinity);
   * // => Infinity
   *
   * _.toNumber('3.2');
   * // => 3.2
   */
  function toNumber(value) {
    if (typeof value == 'number') {
      return value;
    }
    if (isSymbol(value)) {
      return NAN;
    }
    if (isObject(value)) {
      var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
      value = isObject(other) ? (other + '') : other;
    }
    if (typeof value != 'string') {
      return value === 0 ? value : +value;
    }
    value = baseTrim(value);
    var isBinary = reIsBinary.test(value);
    return (isBinary || reIsOctal.test(value))
      ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
      : (reIsBadHex.test(value) ? NAN : +value);
  }

  /**
   * Gets the timestamp of the number of milliseconds that have elapsed since
   * the Unix epoch (1 January 1970 00:00:00 UTC).
   *
   * @static
   * @memberOf _
   * @since 2.4.0
   * @category Date
   * @returns {number} Returns the timestamp.
   * @example
   *
   * _.defer(function(stamp) {
   *   console.log(_.now() - stamp);
   * }, _.now());
   * // => Logs the number of milliseconds it took for the deferred invocation.
   */
  var now = function() {
    return root.Date.now();
  };

  /** Error message constants. */
  var FUNC_ERROR_TEXT = 'Expected a function';

  /* Built-in method references for those with the same name as other `lodash` methods. */
  var nativeMax = Math.max,
      nativeMin = Math.min;

  /**
   * Creates a debounced function that delays invoking `func` until after `wait`
   * milliseconds have elapsed since the last time the debounced function was
   * invoked. The debounced function comes with a `cancel` method to cancel
   * delayed `func` invocations and a `flush` method to immediately invoke them.
   * Provide `options` to indicate whether `func` should be invoked on the
   * leading and/or trailing edge of the `wait` timeout. The `func` is invoked
   * with the last arguments provided to the debounced function. Subsequent
   * calls to the debounced function return the result of the last `func`
   * invocation.
   *
   * **Note:** If `leading` and `trailing` options are `true`, `func` is
   * invoked on the trailing edge of the timeout only if the debounced function
   * is invoked more than once during the `wait` timeout.
   *
   * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
   * until to the next tick, similar to `setTimeout` with a timeout of `0`.
   *
   * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
   * for details over the differences between `_.debounce` and `_.throttle`.
   *
   * @static
   * @memberOf _
   * @since 0.1.0
   * @category Function
   * @param {Function} func The function to debounce.
   * @param {number} [wait=0] The number of milliseconds to delay.
   * @param {Object} [options={}] The options object.
   * @param {boolean} [options.leading=false]
   *  Specify invoking on the leading edge of the timeout.
   * @param {number} [options.maxWait]
   *  The maximum time `func` is allowed to be delayed before it's invoked.
   * @param {boolean} [options.trailing=true]
   *  Specify invoking on the trailing edge of the timeout.
   * @returns {Function} Returns the new debounced function.
   * @example
   *
   * // Avoid costly calculations while the window size is in flux.
   * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
   *
   * // Invoke `sendMail` when clicked, debouncing subsequent calls.
   * jQuery(element).on('click', _.debounce(sendMail, 300, {
   *   'leading': true,
   *   'trailing': false
   * }));
   *
   * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
   * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
   * var source = new EventSource('/stream');
   * jQuery(source).on('message', debounced);
   *
   * // Cancel the trailing debounced invocation.
   * jQuery(window).on('popstate', debounced.cancel);
   */
  function debounce(func, wait, options) {
    var lastArgs,
        lastThis,
        maxWait,
        result,
        timerId,
        lastCallTime,
        lastInvokeTime = 0,
        leading = false,
        maxing = false,
        trailing = true;

    if (typeof func != 'function') {
      throw new TypeError(FUNC_ERROR_TEXT);
    }
    wait = toNumber(wait) || 0;
    if (isObject(options)) {
      leading = !!options.leading;
      maxing = 'maxWait' in options;
      maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
      trailing = 'trailing' in options ? !!options.trailing : trailing;
    }

    function invokeFunc(time) {
      var args = lastArgs,
          thisArg = lastThis;

      lastArgs = lastThis = undefined;
      lastInvokeTime = time;
      result = func.apply(thisArg, args);
      return result;
    }

    function leadingEdge(time) {
      // Reset any `maxWait` timer.
      lastInvokeTime = time;
      // Start the timer for the trailing edge.
      timerId = setTimeout(timerExpired, wait);
      // Invoke the leading edge.
      return leading ? invokeFunc(time) : result;
    }

    function remainingWait(time) {
      var timeSinceLastCall = time - lastCallTime,
          timeSinceLastInvoke = time - lastInvokeTime,
          timeWaiting = wait - timeSinceLastCall;

      return maxing
        ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
        : timeWaiting;
    }

    function shouldInvoke(time) {
      var timeSinceLastCall = time - lastCallTime,
          timeSinceLastInvoke = time - lastInvokeTime;

      // Either this is the first call, activity has stopped and we're at the
      // trailing edge, the system time has gone backwards and we're treating
      // it as the trailing edge, or we've hit the `maxWait` limit.
      return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
        (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
    }

    function timerExpired() {
      var time = now();
      if (shouldInvoke(time)) {
        return trailingEdge(time);
      }
      // Restart the timer.
      timerId = setTimeout(timerExpired, remainingWait(time));
    }

    function trailingEdge(time) {
      timerId = undefined;

      // Only invoke if we have `lastArgs` which means `func` has been
      // debounced at least once.
      if (trailing && lastArgs) {
        return invokeFunc(time);
      }
      lastArgs = lastThis = undefined;
      return result;
    }

    function cancel() {
      if (timerId !== undefined) {
        clearTimeout(timerId);
      }
      lastInvokeTime = 0;
      lastArgs = lastCallTime = lastThis = timerId = undefined;
    }

    function flush() {
      return timerId === undefined ? result : trailingEdge(now());
    }

    function debounced() {
      var time = now(),
          isInvoking = shouldInvoke(time);

      lastArgs = arguments;
      lastThis = this;
      lastCallTime = time;

      if (isInvoking) {
        if (timerId === undefined) {
          return leadingEdge(lastCallTime);
        }
        if (maxing) {
          // Handle invocations in a tight loop.
          clearTimeout(timerId);
          timerId = setTimeout(timerExpired, wait);
          return invokeFunc(lastCallTime);
        }
      }
      if (timerId === undefined) {
        timerId = setTimeout(timerExpired, wait);
      }
      return result;
    }
    debounced.cancel = cancel;
    debounced.flush = flush;
    return debounced;
  }

  var __defProp = Object.defineProperty;
  var __defProps = Object.defineProperties;
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __spreadValues = (a, b) => {
    for (var prop in b || (b = {}))
      if (__hasOwnProp.call(b, prop))
        __defNormalProp(a, prop, b[prop]);
    if (__getOwnPropSymbols)
      for (var prop of __getOwnPropSymbols(b)) {
        if (__propIsEnum.call(b, prop))
          __defNormalProp(a, prop, b[prop]);
      }
    return a;
  };
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
  function set(arg1, arg2) {
    let options = {
      name: "",
      value: "",
      maxAge: 24 * 60 * 60,
      path: "/"
    };
    if (typeof arg1 === "object") {
      Object.assign(options, arg1);
    } else {
      options.name = arg1;
      options.value = arg2;
    }
    options.value = encodeURIComponent(options.value);
    document.cookie = [
      `${options.name}=${options.value}`,
      `max-age=${options.maxAge}`,
      !!options.domain && `domain=${options.domain}`,
      !!options.path && `path=${options.path}`,
      !!options.sameSite && `sameSite=${options.sameSite}`,
      !!options.secure && `secure`
    ].filter(Boolean).join(";");
  }
  function get(name) {
    let reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
    let arr = document.cookie.match(reg);
    if (arr) {
      return decodeURIComponent(arr[2]);
    } else {
      return null;
    }
  }
  function remove(arg1) {
    if (typeof arg1 === "string") {
      set({ name: arg1, value: "", maxAge: 0 });
    } else {
      set(__spreadProps(__spreadValues({}, arg1), { maxAge: 0 }));
    }
  }
  const Cookie = {
    get,
    set,
    remove
  };

  var toggleSelection = function () {
    var selection = document.getSelection();
    if (!selection.rangeCount) {
      return function () {};
    }
    var active = document.activeElement;

    var ranges = [];
    for (var i = 0; i < selection.rangeCount; i++) {
      ranges.push(selection.getRangeAt(i));
    }

    switch (active.tagName.toUpperCase()) { // .toUpperCase handles XHTML
      case 'INPUT':
      case 'TEXTAREA':
        active.blur();
        break;

      default:
        active = null;
        break;
    }

    selection.removeAllRanges();
    return function () {
      selection.type === 'Caret' &&
      selection.removeAllRanges();

      if (!selection.rangeCount) {
        ranges.forEach(function(range) {
          selection.addRange(range);
        });
      }

      active &&
      active.focus();
    };
  };

  var deselectCurrent = toggleSelection;

  var clipboardToIE11Formatting = {
    "text/plain": "Text",
    "text/html": "Url",
    "default": "Text"
  };

  var defaultMessage = "Copy to clipboard: #{key}, Enter";

  function format(message) {
    var copyKey = (/mac os x/i.test(navigator.userAgent) ? "⌘" : "Ctrl") + "+C";
    return message.replace(/#{\s*key\s*}/g, copyKey);
  }

  function copy(text, options) {
    var debug,
      message,
      reselectPrevious,
      range,
      selection,
      mark,
      success = false;
    if (!options) {
      options = {};
    }
    debug = options.debug || false;
    try {
      reselectPrevious = deselectCurrent();

      range = document.createRange();
      selection = document.getSelection();

      mark = document.createElement("span");
      mark.textContent = text;
      // reset user styles for span element
      mark.style.all = "unset";
      // prevents scrolling to the end of the page
      mark.style.position = "fixed";
      mark.style.top = 0;
      mark.style.clip = "rect(0, 0, 0, 0)";
      // used to preserve spaces and line breaks
      mark.style.whiteSpace = "pre";
      // do not inherit user-select (it may be `none`)
      mark.style.webkitUserSelect = "text";
      mark.style.MozUserSelect = "text";
      mark.style.msUserSelect = "text";
      mark.style.userSelect = "text";
      mark.addEventListener("copy", function(e) {
        e.stopPropagation();
        if (options.format) {
          e.preventDefault();
          if (typeof e.clipboardData === "undefined") { // IE 11
            debug && console.warn("unable to use e.clipboardData");
            debug && console.warn("trying IE specific stuff");
            window.clipboardData.clearData();
            var format = clipboardToIE11Formatting[options.format] || clipboardToIE11Formatting["default"];
            window.clipboardData.setData(format, text);
          } else { // all other browsers
            e.clipboardData.clearData();
            e.clipboardData.setData(options.format, text);
          }
        }
        if (options.onCopy) {
          e.preventDefault();
          options.onCopy(e.clipboardData);
        }
      });

      document.body.appendChild(mark);

      range.selectNodeContents(mark);
      selection.addRange(range);

      var successful = document.execCommand("copy");
      if (!successful) {
        throw new Error("copy command was unsuccessful");
      }
      success = true;
    } catch (err) {
      debug && console.error("unable to copy using execCommand: ", err);
      debug && console.warn("trying IE specific stuff");
      try {
        window.clipboardData.setData(options.format || "text", text);
        options.onCopy && options.onCopy(window.clipboardData);
        success = true;
      } catch (err) {
        debug && console.error("unable to copy using clipboardData: ", err);
        debug && console.error("falling back to prompt");
        message = format("message" in options ? options.message : defaultMessage);
        window.prompt(message, text);
      }
    } finally {
      if (selection) {
        if (typeof selection.removeRange == "function") {
          selection.removeRange(range);
        } else {
          selection.removeAllRanges();
        }
      }

      if (mark) {
        document.body.removeChild(mark);
      }
      reselectPrevious();
    }

    return success;
  }

  var copyToClipboard = copy;

  var copy$1 = copyToClipboard;

  function createStorage(storage) {
    function getItem(key, defaultValue) {
      try {
        const value = storage.getItem(key);
        if (value)
          return JSON.parse(value);
        return defaultValue;
      } catch (error) {
        return defaultValue;
      }
    }
    return {
      getItem,
      setItem(key, value) {
        storage.setItem(key, JSON.stringify(value));
      },
      removeItem: storage.removeItem.bind(storage),
      clear: storage.clear.bind(storage)
    };
  }
  const session = createStorage(window.sessionStorage);
  createStorage(window.localStorage);

  function matcher(source, regexp) {
    if (typeof regexp === "string")
      return source.includes(regexp);
    return !!source.match(regexp);
  }
  function router(config) {
    const opts = {
      domain: "",
      routes: []
    };
    if ("routes" in config) {
      opts.domain = config.domain;
      opts.routes = config.routes;
    } else {
      opts.routes = Array.isArray(config) ? config : [config];
    }
    if (opts.domain) {
      const domains = Array.isArray(opts.domain) ? opts.domain : [opts.domain];
      const match = domains.some(
        (domain) => matcher(window.location.origin, domain)
      );
      if (!match)
        return;
    }
    const pathSource = window.location.pathname + window.location.search + window.location.hash;
    if (typeof opts.routes === "function") {
      opts.routes();
      return;
    }
    const routes = Array.isArray(opts.routes) ? opts.routes : [opts.routes];
    routes.forEach((route) => {
      let match = true;
      if (route.path) {
        match = matcher(pathSource, route.path);
      }
      if (route.pathname) {
        match = matcher(window.location.pathname, route.pathname);
      }
      if (route.search) {
        match = matcher(window.location.search, route.search);
      }
      if (route.hash) {
        match = matcher(window.location.hash, route.hash);
      }
      if (match)
        route.run();
    });
  }

  var e=[],t=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=!0===r.prepend?"prepend":"append",d=!0===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t[u]={}),a=t[u]&&t[u][s]?t[u][s]:t[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n<t.length;n++)e.setAttribute(t[n],r.attributes[t[n]]);var a="prepend"===s?"afterbegin":"beforeend";return i.insertAdjacentElement(a,e),e}}

  var css = ".e-hentai-infinite-scroll.g #gd2 > * {\n  cursor: pointer;\n}\n.e-hentai-infinite-scroll.g #gd2 > *:active {\n  color: #2af;\n  text-decoration: underline;\n}\n.e-hentai-infinite-scroll.g #gdt::after {\n  content: \"\";\n  display: block;\n  clear: both;\n}\n.e-hentai-infinite-scroll.g .g-scroll-body {\n  display: grid;\n  overflow: hidden auto;\n  max-height: 80vh;\n}\n.e-hentai-infinite-scroll.g .g-scroll-body::-webkit-scrollbar {\n  width: 8px;\n}\n.e-hentai-infinite-scroll.g .g-scroll-body::-webkit-scrollbar-thumb {\n  background-color: rgba(255, 255, 255, 0.15);\n  border-radius: 2px;\n}\n.e-hentai-infinite-scroll.g .g-scroll-body.large {\n  grid-template-columns: repeat(5, 1fr);\n}\n@media screen and (max-width: 1230px) {\n  .e-hentai-infinite-scroll.g .g-scroll-body.large {\n    grid-template-columns: repeat(4, 1fr);\n  }\n}\n@media screen and (max-width: 990px) {\n  .e-hentai-infinite-scroll.g .g-scroll-body.large {\n    grid-template-columns: repeat(3, 1fr);\n  }\n}\n.e-hentai-infinite-scroll.g .g-scroll-body.normal {\n  grid-template-columns: repeat(10, 1fr);\n}\n@media screen and (max-width: 1230px) {\n  .e-hentai-infinite-scroll.g .g-scroll-body.normal {\n    grid-template-columns: repeat(8, 1fr);\n  }\n}\n@media screen and (max-width: 990px) {\n  .e-hentai-infinite-scroll.g .g-scroll-body.normal {\n    grid-template-columns: repeat(6, 1fr);\n  }\n}\n.e-hentai-infinite-scroll.g .g-scroll-page-index {\n  clear: both;\n  grid-column: 1/-1;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n.e-hentai-infinite-scroll.g .g-scroll-page-index::before, .e-hentai-infinite-scroll.g .g-scroll-page-index::after {\n  display: block;\n  content: \"\";\n  width: 40px;\n  height: 1px;\n  background: #ddd;\n  margin: 0 10px;\n}\n.e-hentai-infinite-scroll.s .auto-load-img {\n  width: 100% !important;\n  max-width: 100% !important;\n  margin: 0 !important;\n  padding: 10px;\n  display: block;\n  box-sizing: border-box;\n}\n.e-hentai-infinite-scroll.s .auto-load-img-empty {\n  min-height: 1000px;\n  width: 100px !important;\n  margin: 0 auto !important;\n}\n.e-hentai-infinite-scroll.s #i3 a {\n  pointer-events: none;\n}";
  n(css,{});

  const $$1 = (selector) => document.querySelector(selector);
  const $$ = (selector) => document.querySelectorAll(selector);
  function getPageInfo() {
    const mode = $$1("#gdt").className.includes("gt200") ? "large" : "normal";
    const pageSize = mode === "normal" ? 40 : 20;
    const total = +$$1(".gtb p.gpc").textContent.match(
      new RegExp("of\\s(?<total>[0-9,]+)\\simages")
    ).groups.total;
    const url = new URL(window.location.href);
    let currentPage = 0;
    if (url.searchParams.has("p")) {
      currentPage = +url.searchParams.get("p");
    }
    const pageCount = +$$1(".gtb .ptb td:nth-last-child(2)").textContent;
    const unloadPageCount = pageCount - 1 - currentPage;
    let unloadPageLinks = Array(unloadPageCount).fill(0).map((_, i) => {
      url.searchParams.set("p", 1 + currentPage + i + "");
      return url.toString();
    });
    return {
      mode,
      url,
      total,
      currentPage,
      pageSize,
      pageCount,
      unloadPageLinks,
      childrenClass: "#gdt > a"
    };
  }
  async function fetchNextDom(url, info) {
    const storageKey = url + info.mode;
    let html = session.getItem(storageKey) || await fetch(url).then((r) => r.text());
    const doc = new DOMParser().parseFromString(html, "text/html");
    if (doc.querySelector("#gdt")) {
      info.currentPage++;
      session.setItem(storageKey, html);
      const items = doc.querySelectorAll(info.childrenClass);
      items.forEach((node) => {
        node.setAttribute("data-page", info.currentPage + "");
      });
      return items;
    } else {
      return null;
    }
  }
  let isLoading = false;
  async function loadNextPage(info, mode) {
    if (isLoading)
      return;
    let url = info.unloadPageLinks.shift();
    if (url) {
      isLoading = true;
      const items = await fetchNextDom(url, info);
      isLoading = false;
      if (items) {
        createPageIndex(info.currentPage);
        $$1("#gdt").append(...items);
        $$("#gdt .c").forEach((node) => node.remove());
      }
    }
  }
  function createPageIndex(currentPage) {
    const dom = document.createElement("div");
    dom.innerText = currentPage + 1 + "";
    dom.className = "g-scroll-page-index";
    $$1("#gdt").append(dom);
  }
  function tinyGallery() {
    const info = getPageInfo();
    const handleScroll = () => {
      const dom = document.scrollingElement;
      if ($$1("#cdiv").getBoundingClientRect().y <= dom.scrollTop + dom.clientHeight + 2e3) {
        loadNextPage(info);
      }
    };
    document.addEventListener("scroll", handleScroll);
  }
  function largeGallery() {
    const info = getPageInfo();
    $$1("#gdt").classList.add("g-scroll-body", info.mode);
    $$(info.childrenClass).forEach((node) => {
      node.setAttribute("data-page", info.currentPage + "");
    });
    const replaceCurrentURL = debounce(function() {
      const imgs = document.querySelectorAll(info.childrenClass);
      const rect = $$1("#gdt").getBoundingClientRect();
      const base = rect.top + rect.height / 2;
      for (const img of imgs) {
        const { top, bottom } = img.getBoundingClientRect();
        if (top < base && bottom > base) {
          const page = img.dataset.page;
          const url = new URL(window.location.href);
          if (+page === 0) {
            url.searchParams.delete("p");
          } else {
            url.searchParams.set("p", page);
          }
          if (window.location.href !== url.toString()) {
            history.replaceState(null, "", url);
            const activeElement = (node, idx) => {
              node.className = "";
              if (idx === +page + 1) {
                node.className = "ptds";
              }
            };
            $$(".gtb .ptt td").forEach(activeElement);
            $$(".gtb .ptb td").forEach(activeElement);
          }
          return;
        }
      }
    }, 30);
    const handleScroll = () => {
      const dom = $$1("#gdt");
      if (dom.scrollHeight - 2e3 < dom.scrollTop + dom.clientHeight) {
        loadNextPage(info);
      }
      replaceCurrentURL();
    };
    handleScroll();
    $$1("#gdt").addEventListener("scroll", handleScroll);
  }
  function addWatchTag(tag) {
    return fetch("/mytags", {
      method: "POST",
      body: new URLSearchParams({
        usertag_action: "add",
        tagname_new: tag,
        tagwatch_new: "on",
        tagcolor_new: "",
        tagweight_new: "10",
        usertag_target: "0"
      })
    });
  }
  async function injectWatchTag() {
    const node = document.querySelector("#tagmenu_act");
    const inject = () => {
      const img = document.createElement("img");
      const a = document.createElement("a");
      const br = document.createElement("br");
      node.append(br, img, a);
      img.outerHTML = '<img src="https://ehgt.org/g/mr.gif" class="mr" alt=">"> ';
      a.href = "#";
      a.textContent = "Watch";
      a.addEventListener("click", (e) => {
        e.preventDefault();
        if (window.selected_tagname) {
          addWatchTag(window.selected_tagname).then(() => {
            alert("success");
          }).catch((error) => {
            console.error(error);
            alert(error.message);
          });
        }
      });
    };
    const ob = new MutationObserver(() => {
      if (node.style.display !== "none") {
        inject();
      }
    });
    ob.observe(node, { attributes: true });
  }
  function addTitleCopyEvent() {
    $$("#gd2>*").forEach(function(node) {
      node.addEventListener("click", function() {
        if (this.textContent)
          copy$1(this.textContent);
      });
    });
  }
  async function setup$1() {
    injectWatchTag();
    addTitleCopyEvent();
    const info = getPageInfo();
    $$1("body").classList.add("e-hentai-infinite-scroll", "g");
    if (!info.unloadPageLinks.length)
      return;
    if (info.unloadPageLinks.length > 2) {
      largeGallery();
    } else {
      tinyGallery();
    }
  }

  function checkCookie() {
    const igneous = Cookie.get("igneous");
    if (!igneous || igneous === "mystery") {
      $("<button>refresh</button>").on("click", refresh).appendTo("body");
      $("<button>login</button>").on("click", login).appendTo("body");
    }
    if (igneous === "mystery") {
      $(
        "<h2>[Cookie] igneous error! Change system proxy and reload page</h2>"
      ).appendTo("body");
    }
  }
  function refresh() {
    Cookie.remove({ name: "yay", domain: ".exhentai.org" });
    Cookie.remove({ name: "igneous", domain: ".exhentai.org" });
    Cookie.remove({ name: "ipb_pass_hash", domain: ".exhentai.org" });
    Cookie.remove({ name: "ipb_member_id", domain: ".exhentai.org" });
    window.location.reload();
  }
  function login() {
    window.location.href = "https://forums.e-hentai.org/index.php?act=Login&CODE=00";
  }

  const store = {};
  function parseI3(i3) {
    return i3.match(new RegExp(`'(?<key>.*)'.*src="(?<src>.*?")(.*nl\\('(?<nl>.*)'\\))?`)).groups;
  }
  function setupInfiniteScroll() {
    function api_call(page2, nextImgKey2) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("POST", window.api_url);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.withCredentials = true;
        xhr.addEventListener("loadend", () => {
          if (200 <= xhr.status && xhr.status <= 300)
            resolve(JSON.parse(xhr.response));
          else
            reject(xhr.response);
        });
        xhr.send(
          JSON.stringify({
            method: "showpage",
            gid: window.gid,
            page: page2,
            imgkey: nextImgKey2,
            showkey: window.showkey
          })
        );
      });
    }
    const maxPageSize = parseInt(
      document.querySelector("#i2 > div.sn > div > span:nth-child(2)").textContent
    );
    let nextImgKey = document.querySelector("#i3 a[onclick]").onclick.toString().match(new RegExp("'(?<key>.*)'")).groups.key;
    let page = window.startpage + 1;
    let isLoading = false;
    async function loadImgInfo() {
      try {
        if (maxPageSize < page) {
          return;
        }
        if (isLoading)
          return;
        isLoading = true;
        const res = await api_call(page, nextImgKey);
        isLoading = false;
        const groups = parseI3(res.i3);
        const info = {
          key: res.k,
          nl: groups.nl,
          src: groups.src.slice(0, -1),
          source: res.s[0] === "/" ? res.s : "/" + res.s
        };
        store[res.k] = { info, res };
        renderImg(page, info);
        nextImgKey = groups.key;
        page++;
      } catch (error) {
        isLoading = false;
        console.error(error);
        await loadImgInfo();
      }
    }
    function renderImg(page2, info) {
      const { key, source, src } = info;
      const img = document.createElement("img");
      img.setAttribute("src", src);
      img.dataset.imgKey = key;
      img.dataset.page = page2 + "";
      img.dataset.source = source;
      img.classList.add("auto-load-img");
      img.loading = "lazy";
      document.getElementById("i3").append(img);
    }
    function resetDefaultImgDOM() {
      const groups = parseI3(document.querySelector("#i3").innerHTML);
      store[window.startkey] = {
        info: {
          key: window.startkey,
          nl: groups.nl,
          src: groups.src,
          source: location.pathname
        },
        res: {
          i: document.querySelector("#i4 > div").outerHTML,
          i3: document.querySelector("#i3").innerHTML,
          n: document.querySelector("#i4 > .sn").outerHTML,
          i5: document.querySelector("#i5").innerHTML,
          i6: document.querySelector("#i6").innerHTML,
          k: window.startkey,
          s: location.pathname
        }
      };
      const $img = document.querySelector("#i3 a img");
      $img.removeAttribute("style");
      $img.classList.add("auto-load-img");
      $img.dataset.imgKey = window.startkey;
      $img.dataset.source = location.pathname;
      document.getElementById("i3").append($img);
      document.querySelector("#i3 a").remove();
      removeSnAnchor();
    }
    document.body.classList.add("e-hentai-infinite-scroll", "s");
    resetDefaultImgDOM();
    loadImgInfo();
    document.addEventListener("scroll", () => {
      const dom = document.scrollingElement;
      if (dom.scrollHeight <= dom.scrollTop + dom.clientHeight + 2e3) {
        loadImgInfo();
      }
      updateCurrentInfo();
    });
  }
  function removeSnAnchor() {
    document.querySelectorAll(".sn a[onclick]").forEach((a) => {
      a.removeAttribute("onclick");
    });
  }
  function getCurrentActiveImg() {
    const imgs = document.querySelectorAll("#i3 img,#i3 img");
    for (const img of imgs) {
      const { top, bottom } = img.getBoundingClientRect();
      const base = 200;
      if (top < base && bottom > base) {
        return img;
      }
    }
    return null;
  }
  function updateCurrentPathname($img) {
    const source = $img.dataset.source;
    history.replaceState(null, "", source);
  }
  function updateBottomInfo($img) {
    const key = $img.dataset.imgKey;
    const { res } = store[key];
    document.querySelector("#i2").innerHTML = res.n + res.i;
    document.querySelector("#i4").innerHTML = res.i + res.n;
    document.querySelector("#i5").innerHTML = res.i5;
    document.querySelector("#i6").innerHTML = res.i6;
    removeSnAnchor();
  }
  const updateCurrentInfo = debounce(function() {
    const $img = getCurrentActiveImg();
    if (!$img)
      return;
    const source = $img.dataset.source;
    if (location.pathname === source)
      return;
    updateCurrentPathname($img);
    updateBottomInfo($img);
  }, 30);
  function setup() {
    setupInfiniteScroll();
  }

  router({
    domain: "exhentai.org",
    routes: [{ run: checkCookie }]
  });
  router([
    { pathname: /^\/g\//, run: setup$1 },
    { pathname: /^\/s\//, run: setup }
  ]);

})();