Sleazy Fork is available in English.

一键批量加载页面[EX绅士][exhentai]

一键批量加载所有后续页面内容

// ==UserScript==
// @name         一键批量加载页面[EX绅士][exhentai]
// @namespace    exhentaigetallpage
// @version     0.7.5
// @description  一键批量加载所有后续页面内容
// @author       allence_frede
// @match     *://exhentai.org/tag/*
// @match     *://exhentai.org/*f_search*
// @match     *://exhentai.org/g/*
// @grant        none
// @require    http://code.jquery.com/jquery-1.11.0.min.js
// @license    GNU GPLv3
// ==/UserScript==

class commonTools {
  data_type = Symbol('dataType')

  constructor() {
    this.#injectionPrototypeTool()
    Object.assign(this, this.#commonTools())
  }
  /**
   * 注入原型链工具函数
   */
  #injectionPrototypeTool() {
    let _this = this
    // 获取数据类型
    Object.prototype[this.data_type] = function () {
      return Object.prototype.toString.call(this).replace(/\[object (.*)\]/, '$1').toLocaleLowerCase()
    }
    // 网络请求函数
    String.prototype.webApi = async function () {
      return new Promise((resolve, reject) => {
        fetch(this)
          .then(res => res.text())
          .catch(error => reject(error))
          .then(res => resolve(res))
      })
    }
    // 字符串格式化为数字
    String.prototype.parseNum = function () {
      let new_num = this.replace(',', '')
      return new_num.includes('.') ? parseFloat(new_num) : parseInt(new_num)
    }
    // 字符串检查是否包含多个值
    String.prototype.includes_ee = function (check = '', mode = 'and') {
      if (Object[_this.data_type].call(check) == 'array') {
        if (mode == 'and') {
          return check.every(item => this.includes(item))
        } else if (mode == 'or') {
          return check.some(item => this.includes(item))
        } else {
          return false
        }
      } else {
        return this.includes(check)
      }
    }
    // 判断是否为json字符串
    String.prototype.isJson = function () {
      try {
        return Boolean(JSON.parse(this))
      } catch {
        return false
      }
    }
    // json字符串解析为对象
    String.prototype.toObject = function () {
      try {
        let json = JSON.parse(this)
        return Boolean(json) ? json : false
      } catch {
        return false
      }
    }
  }
  /**
   * 通用工具
   * @returns 工具对象
   */
  #commonTools() {
    let config_name = 'EXEE_CFG'
    // 获取配置
    function getCfg(name = '') {
      let cfg = window.localStorage.getItem(config_name)
      if (cfg && cfg.isJson()) {
        cfg = cfg.toObject()
      } else {
        return undefined
      }
      if (name) {
        return cfg[name]
      } else {
        return cfg
      }
    }
    // 保存设置
    function saveCfg(name, val = null) {
      if (Object[this.data_type].call(name) == 'object') {
        window.localStorage.setItem(config_name, JSON.stringify(name))
        return true
      }
      let cfg = getCfg()
      if (cfg) {
        cfg[name] = val
      } else {
        cfg = {
          [name]: val
        }
      }
      cfg = JSON.stringify(cfg)
      window.localStorage.setItem(config_name, cfg)
      return true
    }

    return {
      getCfg,
      saveCfg
    }
  }
}

class EXEE extends commonTools {
  #page_all = 0
  #page_loaded = 1
  #page_loading = false
  get #is_last_page() {
    return /prev=1$/.test(EXEE.#page_url) ||
      EXEE.#next_url == '' ||
      this.#page_loaded == this.#page_all
  }
  #page_style = {
    gallery: false,
    search_list: true,
  }
  #show_panel = false
  get #cfg() {
    let def_cfg = {
      load_pages_in_once: 20,
      page_use_new_layout: true,
      load_pages_at_first_open: false,
      hide_panel: false
    }
    // 读取本地配置
    let origin_cfg = this.getCfg()
    if (origin_cfg) {
      Object.assign(def_cfg, origin_cfg)
    }
    return def_cfg
  }
  set #cfg(val) {
    try {
      let cfg = this.#cfg
      for (const key in val) {
        cfg[key] = val[key]
      }
      this.saveCfg(cfg)
      return true
    } catch (err) {
      console.error('保存配置失败', err)
      return false
    }
  }
  #gallery_next_url = []
  #autoLoadPageBtnText = () => this.#cfg.load_pages_at_first_open ? '开启首次打开自动加载' : '关闭首次打开自动加载'
  #useNewLayoutBtnText = () => this.#cfg.page_use_new_layout ? '启用页面新样式' : '关闭页面新样式'
  static #page_url = window.location.href
  static #next_url = window.nexturl
  static #c_node = () => {
    let node = document.createElement('div')
    node.className = 'c'
    return node
  }
  /**
   * 构造函数
   */
  constructor() {
    super()
    this.#checkPageInit()
  }
  /**
   * 启动注入页面
   */
  injection() {
    this.#openInNewTab()
    if (this.#show_panel) {
      this.#injectionCss()
      this.#injectionPanel()
      if (this.#cfg.hide_panel) {
        this.#hidePanel()
      }
      this.#injectionNewLayout()
      if (this.#cfg.page_use_new_layout) {
        this.#useNewLayout(this.#cfg.page_use_new_layout)
      }
      this.#bindPanelmethod()
      // 自动读取后续页面
      if (this.#cfg.load_pages_at_first_open) {
        this.#pageLoadMore()
      }
    }
    if (this.#page_style.gallery) {
      //去除购买广告入口
      $('#spa').remove()
    }
  }
  /**
   * 读取后续页面的数据
   */
  async #pageLoadMore() {
    this.#page_loading = true
    let load_count = this.#cfg.load_pages_in_once
    let hit = undefined
    if (this.#page_style.search_list) {
      let nav_top = undefined
      let nav_buttom = undefined
      while (!this.#is_last_page && load_count > 0) {
        let res = await this.#getPage(EXEE.#next_url)
        if (res) {
          hit = res.match(/var nexturl="(?<target>.*?)";/s)
          if (hit?.groups?.target) {
            EXEE.#next_url = hit.groups.target
          } if (hit?.groups?.target.length == 0) {
            EXEE.#next_url = ''
          }
          window.res = res
          hit = res.match(/<div class="itg gld">(?<target>.*?)<\/div><div class="searchnav">/)
          if (hit?.groups?.target) {
            hit.groups.target = this.#openInNewTab(hit.groups.target)
            $('.itg.gld').append(hit.groups.target)
            this.#page_loaded++
            $('#egap .page-now').text(this.#page_loaded)
          }
          hit = res.match(/<div class="searchnav">(?<target>.*?)<\/div><div class="itg gld/s)
          if (hit?.groups?.target) nav_top = hit.groups.target
          hit = res.match(/<\/div><\/div><\/div><\/div><div class="searchnav">(?<target>.*?)<\/div>\n<div class="dp"/s)
          if (hit?.groups?.target) nav_buttom = hit.groups.target
          load_count--
        }
      }
      if (nav_top) $('.searchnav').eq(0).html(nav_top)
      if (nav_buttom) $('.searchnav').eq(1).html(nav_buttom)
    }
    if (this.#page_style.gallery) {
      let nav = undefined
      while (this.#gallery_next_url.length > 0 && load_count > 0) {
        let res = await this.#getPage(this.#gallery_next_url.shift())
        if (res) {
          hit = res.match(/<div id="gdt".+?>(?<target>.*?)<div class="gtb">/s)
          if (hit?.groups?.target) {
            hit.groups.target = this.#openInNewTab(hit.groups.target)
            $('#gdt').append(hit.groups.target)
            this.#page_loaded++
            $('#egap .page-now').text(this.#page_loaded)
          }
          hit = res.match(/<table class="ptt".*?>(?<target>.*?)<\/table>/s)
          if (hit?.groups?.target) {
            nav = hit.groups.target
          }
          load_count--
        }
      }
      if (nav) {
        $('.ptt').html(nav)
        $('.ptb').html(nav)
      }
    }
    this.#page_loading = false
  }
  /**
   * 使用新页面样式
   */
  #useNewLayout(use_layout = true) {
    if (use_layout) {
      if (this.#page_style.gallery) {
        $('#gdt .c').remove()
      }
      if (this.#page_style.search_list) {

      }
      $('#gdt').addClass('new-layout')
      $('.ido').addClass('new-layout')
      $('.gdtl').addClass('new-layout')
      $('.gld').addClass('new-layout')
    } else {
      if (this.#page_style.gallery) {
        $('#gdt').append(EXEE.#c_node)
      }
      $('#gdt').removeClass('new-layout')
      $('.ido').removeClass('new-layout')
      $('.gdtl').removeClass('new-layout')
      $('.gld').removeClass('new-layout')
    }
  }
  /**
   * 注入页面新样式
   */
  #injectionNewLayout() {
    let style = `
      <style type="text/css">
        #gdt.new-layout {
          max-width: none !important;
          display:  grid !important;
        }
        @media screen and (min-width:1080px) {
          .ido.new-layout {
            max-width: 100% !important;
          }
          .gdtl.new-layout {
            width: auto !important;
            max-width: 239px !important;
            min-width: 214px !important;
          }
          .gld.new-layout {
            display: grid;
            grid-template-columns:repeat(5,1fr)
          }
        }
        @supports(display: grid) {
          @media screen and (min-width:2171px) {
            .gld.new-layout {
              grid-template-columns:repeat(8,1fr) !important;
            }
            #gdt.new-layout {
              grid-template-columns:repeat(10,1fr) !important;
            }
          }
          @media screen and (min-width:1951px) and (max-width:2171px) {
            .gld.new-layout {
              grid-template-columns:repeat(7,1fr) !important;
            }
            #gdt.new-layout {
              grid-template-columns:repeat(9,1fr) !important;
            }
          }
          @media screen and (min-width:1740px) and (max-width:1951px) {
            .gld.new-layout {
              grid-template-columns:repeat(6,1fr) !important;
            }
            #gdt.new-layout {
              grid-template-columns:repeat(8,1fr) !important;
            }
          }
          @media screen and (min-width:1531px) and (max-width:1740px) {
            .gld.new-layout {
              grid-template-columns:repeat(5,1fr) !important;
            }
            #gdt.new-layout {
              grid-template-columns:repeat(7,1fr) !important;
            }
          }
          @media screen and (min-width:1360px) and (max-width:1531px) {
            .gld.new-layout {
              grid-template-columns:repeat(5,1fr) !important;
            }
            #gdt.new-layout {
              grid-template-columns:repeat(5,1fr) !important;
            }
          }
          @media screen and (min-width:1080px) and (max-width:1360px) {
            .gld.new-layout {
              grid-template-columns:repeat(4,1fr) !important;
            }
            #gdt.new-layout {
              grid-template-columns:repeat(5,1fr) !important;
            }
          }
          @media screen and (min-width:0px) and (max-width:1080px) {
            .gld.new-layout {
              grid-template-columns:repeat(4,1fr) !important;
            }
            #gdt.new-layout {
              grid-template-columns:repeat(4,1fr) !important;
            }
          }
        }
      </style>`
    style = $(style)
    $("body").append(style)
    style = undefined
  }
  /**
   * 注入面板样式
   */
  #injectionCss() {
    let css = `<style>
      #egap{
        position: fixed;
        top:10px;
        left:10px;
        z-index: 5000;
        -webkit-user-select:none;
        -moz-user-select:none;
        -ms-user-select:none;
        user-select:none;
        display: flex;
      }
      #egap #panel{
        opacity: 0.3;
      }
      #egap #panel.close{
        display: none;
      }
      #egap #panel:hover{
        opacity:1;
      }
      #egap .item{
        min-width:120px;
        height:30px;
        line-height:30px;
        color:#EEEEEE;
        background: #4f535b;
        border: 1px solid #000000;
        text-align: center;
        font-size:14px;
        cursor:pointer;
        box-sizing: border-box;
        padding: 0 6px;
        margin-top: 10px;
      }
      #egap .item.show-panel{
        min-width: min-content;
        width: 28px;
        height: 97px;
        line-height: 1.2;
        word-break: break-all;
        padding: 5px 0;
        margin: 0;
        display: block;
        opacity: 0.3;
      }
      #egap .item.show-panel:hover{
        opacity:1;
      }
      #egap .item.show-panel.close{
        display: none;
      }
      #egap #newLoadPagesInOnce{
        width: 120px;
      }
      #egap .cursor-clear{
        cursor: default !important;
      }
    </style>`
    $('body').prepend(css)
  }
  /**
   * 注入面板
   */
  #injectionPanel() {
    let page_info = '<span class="page-now">' + this.#page_loaded + '</span>'
    if (this.#page_style.gallery) {
      page_info += ' / ' + this.#page_all
    }
    let el = `<div id="egap">
      <div id="panel">
        <div class="item page-info cursor-clear">当前已加载
          ${page_info}
        </div>
        <div class="item" method="pageLoadMore">一键加载后续<span id="page-load">${this.#cfg.load_pages_in_once}</span>页</div>
        <div class="item" method="setLoadPagesInOnce">设置连续加载页数</div>
        <div class="item" method="setAutoLoadPageAtFirst">${this.#autoLoadPageBtnText()}</div>
        <div class="item" method="useNewLayout">${this.#useNewLayoutBtnText()}</div>
        <div class="item" method="pageViewSwitch">图墙列表倒序显示</div>
        <div class="item" method="goToTop">回到顶部</div>
        <div class="item" method="hidePanel">收起面板</div>
      </div>
      <div class="item show-panel close" method="showPanel">ex绅士增强</div>
    </div>`
    $('body').prepend(el)
  }
  /**
   * 隐藏面板
   */
  #hidePanel(save = true) {
    $('div[method="hidePanel"]').parent().addClass('close')
    $('div[method="hidePanel"]').parent().siblings('.show-panel').removeClass('close')
    if (save) {
      this.#cfg = { hide_panel: true }
    }
  }
  /**
   * 展示面板
   */
  #showPanel(save = true) {
    $('div[method="showPanel"]').addClass('close')
    $('div[method="showPanel"]').siblings('#panel').removeClass('close')
    if (save) {
      this.#cfg = { hide_panel: false }
    }
  }
  /**
   * 绑定面板按钮方法
   */
  #bindPanelmethod() {
    $('div[method="setAutoLoadPageAtFirst"]').off('click').on('click', () => {
      this.#cfg = { load_pages_at_first_open: !this.#cfg.load_pages_at_first_open }
      $('div[method="setAutoLoadPageAtFirst"]').text(this.#autoLoadPageBtnText())
    })
    $('div[method="pageLoadMore"]').off('click').on('click', async () => {
      if (this.#page_loading) return
      await this.#pageLoadMore()
    })
    $('div[method="goToTop"]').off('click').on('click', () => {
      $('html , body').animate({ scrollTop: 0 }, 'fast')
    })
    $('div[method="hidePanel"]').off('click').on('click', () => {
      this.#hidePanel()
    })
    $('div[method="showPanel"]').off('click').on('click', () => {
      this.#showPanel()
    })
    $('div[method="useNewLayout"]').off('click').on('click', () => {
      this.#cfg = { page_use_new_layout: !this.#cfg.page_use_new_layout }
      this.#useNewLayout(this.#cfg.page_use_new_layout)
      $('div[method="useNewLayout"]').text(this.#useNewLayoutBtnText())
    })
    $('div[method="pageViewSwitch"]').off('click').on('click', () => {
      if (this.#page_loading) {
        window.alert('加载后续中,暂不可操作')
      } else {
        if (this.#page_style.search_list) {
          let node_html = ''
          $('.itg.gld>.gl1t').toArray().reverse().forEach(item => {
            node_html += item.outerHTML
          })
          $('.itg.gld').html(node_html)
          node_html = undefined
        }
      }
    })
    $('div[method="setLoadPagesInOnce"]').off('click').on('click', () => {
      let page_count = window.prompt('请输入页数X(X>0)', this.#cfg.load_pages_in_once)
      if (page_count) {
        if (/^\d+$/.test(page_count)) {
          $('#page-load').text(page_count)
          this.#cfg = { load_pages_in_once: parseInt(page_count) }
        } else {
          window.alert('请输入有效数字')
        }
      }
    })
  }
  /**
   * 根据配置初始化页面
   */
  #checkPageInit() {
    if (EXEE.#page_url.includes_ee(['f_search=', '/tag/', '/g/'], 'or')) {
      this.#show_panel = true
    }
    if (EXEE.#page_url.includes_ee('/g/')) {
      this.#page_all = $('.ptb td').toArray().reverse()[1].innerText.parseNum()
      for (const k in this.#page_style) {
        this.#page_style[k] = Boolean(k == 'gallery')
      }
      if (EXEE.#page_url.includes_ee(['&p=', '?p='], 'or')) {
        let page_num = EXEE.#page_url.match(/p=(?<target>\d+)/)
        if (page_num?.groups?.target) {
          this.#page_loaded = page_num.groups.target.parseNum() + 1
        }
        for (let index = this.#page_loaded; index < this.#page_all; index++) {
          this.#gallery_next_url.push(EXEE.#page_url.replace(/p=(\d+)/, `p=${index}`))
        }
      }
      if (this.#page_all > 1 && !this.#gallery_next_url.length) {
        for (let index = this.#page_loaded; index < this.#page_all; index++) {
          this.#gallery_next_url.push(`${EXEE.#page_url}?p=${index}`)
        }
      }
    }
  }
  /**
   * 在新窗口打开结果/图片
   * @param string el 查询结果字符串
   */
  #openInNewTab(el = null) {
    if (el === null) {
      $('.gl1t a,.gdtl a,#gdt a').attr('target', '_blank')
      return true
    }

    el = el.replaceAll('<a', '<a target="_blank"')
    return el
  }
  // 远程请求
  async #getPage(url) {
    if (this.#is_last_page) return false
    try {
      return await url.webApi(url)
    } catch (error) {
      console.error(`${url}请求失败`)
      console.error('失败原因', error)
      return ''
    }
  }
}

let e = new EXEE()
// 挂载插件到页面
e.injection()