E-Hentai - UX Tweaks

Numerous features to enrich your browsing experience

נכון ליום 20-09-2024. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         E-Hentai - UX Tweaks
// @namespace    brazenvoid
// @version      1.6.0
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Numerous features to enrich your browsing experience
// @match        https://e-hentai.org/*
// @match        https://exhentai.org/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @require      https://update.greasyfork.org/scripts/375557/1244990/Base%20Brazen%20Resource.js
// @require      https://update.greasyfork.org/scripts/416104/1451214/Brazen%20UI%20Generator.js
// @require      https://update.greasyfork.org/scripts/418665/1451241/Brazen%20Configuration%20Manager.js
// @require      https://update.greasyfork.org/scripts/429587/1244644/Brazen%20Item%20Attributes%20Resolver.js
// @require      https://update.greasyfork.org/scripts/416105/1451242/Brazen%20Base%20Search%20Enhancer.js
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

GM_addStyle(
    `#settings-wrapper{min-width:310px;width:310px}.disliked-tag{background-color:lightcoral !important;color:white !important}.disliked-tag:hover{background-color:indianred !important}.disliked-tag > a{color:white !important}.disliked-tag.favourite-tag{background-color:orange !important}.disliked-tag.favourite-tag:hover{background-color:darkorange !important}.favourite-tag{background-color:mediumseagreen !important;color:white !important}.favourite-tag:hover{background-color:forestgreen !important}.favourite-tag > a{color:white !important}`)

const IS_GALLERY_PAGE = $('#gdt').length
const IS_IMAGE_PAGE = window.location.pathname.startsWith('/s/')
const IS_SEARCH_PAGE = $('#f_search').length
const IS_EXTENDED_LAYOUT = IS_SEARCH_PAGE && $('.itg.glte').length > 0
const IS_SMALL_WINDOW = $('.stuffbox').length
const IS_TAG_SEARCH_PAGE = window.location.pathname.startsWith('/tag')
const IS_UPLOADER_SEARCH_PAGE = window.location.pathname.startsWith('/uploader')
const IS_WATCHED_PAGE = document.querySelectorAll('.ido > div > p.ip')?.length > 0

const ITEM_TAGS = 'tags'
const ITEM_WATCHED = 'watched'

const FILTER_HIDE_WATCHED_FROM_SEARCH = 'Watched Galleries in Search'

const UI_DEFAULTS_PAGE_RANGE = 'Page Range'
const UI_DEFAULTS_PAGE_RANGE_ENABLE = 'Enable Page Range Filter'
const UI_DEFAULTS_RATING = 'Rating'
const UI_DEFAULTS_RATING_ENABLE = 'Enable Rating Filter'
const UI_DEFAULTS_TAGS = 'Tags'
const UI_DEFAULTS_TAGS_ENABLE = 'Enable Default Tags'

const UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES = 'Auto Next Page'
const UI_DISLIKED_TAGS = 'Disliked Tags'
const UI_FAVOURITE_TAGS = 'Favourite Tags'
const UI_EMBED_TORRENTS = 'Embed Torrent Downloads'
const UI_VISITED_HIGHLIGHT = 'Highlight Visited'

class EHentaiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
  constructor()
  {
    super({
      isUserLoggedIn: false,
      itemDeepAnalysisSelector: 'div.gm',
      itemLinkSelector: 'td.gl3m.glname > a, td.gl3c.glname > a, div.gl2e > div > a, a',
      itemListSelectors: 'table.itg, div.itg',
      itemNameSelector: 'td.gl3m.glname > a > div.glink, td.gl3c.glname > a > div.glink, div.gl4e.glname > div.glink, div.gl4t.glname.glink',
      itemSelectors: 'td.gl2e, div.gl1t',
      requestDelay: 0,
      scriptPrefix: 'e-hentai-ux-',
      tagSelectorGenerator: (tag) => {
        tag = tag.trim()
        if (IS_GALLERY_PAGE) {
          let tagAttribute = tag.replaceAll(' ', '_')
          return 'div[id="td_' + tagAttribute + '"], a[id="ta_' + tagAttribute + '"]'
        }
        return 'div.gt[title="' + tag + '"], div.gtl[title="' + tag + '"]'
      },
    })
    this._setupFeatures()
    this._setupUI()
    this._setupEvents()
  }

  /**
   * @param {string} tag
   * @return {string}
   * @private
   */
  _formatTag(tag)
  {
    if (tag.includes(':') && !tag.includes('"') && (tag.includes(' ') || tag.includes('+'))) {
      tag = tag.replace(':', ':"') + '"'
    }
    return tag
  }

  /**
   * @param {{}} range
   * @param {URLSearchParams} queryParams
   * @private
   */
  _handleDefaultPageRangeFilter(range, queryParams)
  {
    if (range.minimum > 0) {
      queryParams.set('f_spf', range.minimum)
    }
    if (range.maximum > 0) {
      queryParams.set('f_spf', range.maximum)
    }
  }

  /**
   * @param {string} rating
   * @param {URLSearchParams} queryParams
   * @private
   */
  _handleDefaultRatingsFilter(rating, queryParams)
  {
    queryParams.set('f_srdd', rating)
  }

  /**
   * @param {string[]} tags
   * @param {URLSearchParams} queryParams
   * @private
   */
  _handleDefaultTags(tags, queryParams)
  {
    let existingTags = queryParams.get('f_search')
    let updatedTags = existingTags
    let include = true

    for (let tag of tags) {
      if (!existingTags.includes(tag)) {
        updatedTags += '+' + this._formatTag(tag)
      } else {
        include = false
        break
      }
    }

    if (include) {
      queryParams.set('f_search', updatedTags)
    }
  }

  /**
   * @private
   */
  _handleDefaults()
  {
    let queryParams = new URLSearchParams(window.location.search)
    let existingParams = queryParams.toString()

    if (!queryParams.has('next') &&
        (this._getConfig(UI_DEFAULTS_PAGE_RANGE_ENABLE) || this._getConfig(UI_DEFAULTS_RATING_ENABLE) ||
            this._getConfig(UI_DEFAULTS_TAGS_ENABLE))) {

      if (!queryParams.has('f_search')) {

        let existingTag = ''
        let urlSegments = window.location.pathname.split('/')

        if (IS_TAG_SEARCH_PAGE) {
          existingTag = urlSegments.pop().trim()
        } else if (IS_UPLOADER_SEARCH_PAGE) {
          existingTag = 'uploader:' + urlSegments.pop().trim()
        }
        queryParams.set('f_search', existingTag.length ? this._formatTag(existingTag) : '')
      }

      if (!queryParams.has('advsearch')) {
        queryParams.set('advsearch', '1')
      }

      let validatePageRange = (range, defaultValidator) => defaultValidator(range) && !queryParams.has('f_spf') &&
          !queryParams.has('f_spt')

      this._performTogglableComplexOperation(UI_DEFAULTS_PAGE_RANGE_ENABLE, UI_DEFAULTS_PAGE_RANGE, validatePageRange,
          (range) => {
            this._handleDefaultPageRangeFilter(range, queryParams)
          })

      let validateRatingFilter = (range, defaultValidator) => defaultValidator(range) && !queryParams.has('f_srdd')

      this._performTogglableComplexOperation(UI_DEFAULTS_RATING_ENABLE, UI_DEFAULTS_RATING, validateRatingFilter,
          (rating) => {
            this._handleDefaultRatingsFilter(rating, queryParams)
          })

      this._performTogglableOperation(UI_DEFAULTS_TAGS_ENABLE, UI_DEFAULTS_TAGS, (tags) => {
        this._handleDefaultTags(tags, queryParams)
      })

      let updatedParams = queryParams.toString().replaceAll('%2B', '+')
      if (updatedParams !== existingParams) {
        if (IS_TAG_SEARCH_PAGE || IS_UPLOADER_SEARCH_PAGE) {
          window.location = window.location.origin + '?' + updatedParams
        } else {
          window.location = window.location.origin + window.location.pathname + '?' + updatedParams
        }
      }
    }
  }

  /**
   * @private
   */
  _handleOpenGalleryImages()
  {
    let images = $('div.gdtl a > img, div.gdtm a > img')
    let firstPage = images.first().attr('alt')
    let firstPageNumber = Number(firstPage)
    let paddedPageLength = firstPage.length
    let maxPages = firstPageNumber + images.length - 1

    for (let page = maxPages; page >= firstPageNumber; page--) {

      let paddedPage = page.toString().padStart(paddedPageLength, '0')
      window.open(images.filter('[alt="' + paddedPage + '"]').parent().attr('href'))
    }

    if (this._getConfig(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES)) {

      let page = window.location.href.split('=')[1] || 0
      let pageNavs = $('.ptt td')
      let maxPages = Number.parseInt(pageNavs.eq(pageNavs.length - 2).children('a').text()) - 1
      if (page < maxPages) {

        let uri = window.location.href
        if (page === 0) {
          uri += '?p=1'
        } else {
          uri = uri.replace('?p=' + page++, '?p=' + page)
        }
        window.location = uri
      }
    }
  }

  /**
   * @private
   */
  _handleTorrentDownloadsEmbedding()
  {
    let link = $('#gd5 > .g2 > a').eq(1)
    if (!link.text().endsWith('(0)')) {

      let container = $('<div class="gm"></div>').insertBefore('#cdiv')
      container.load(link.attr('onclick').replace('return popUp(\'', '').replace('\',610,590)', '') + ' form', () => {
        container.prepend('<h1 style="font-size:10pt; font-weight:bold; margin:3px; text-align:center">Torrents</h1>')
        link.parent().remove()
      })
    }
  }

  /**
   * @private
   */
  _setupEvents()
  {
    this._onValidateInit = () => !IS_SMALL_WINDOW

    this._onUIBuild(() => {

      this._performOperation(UI_VISITED_HIGHLIGHT, () => {
        GM_addStyle(`td.gl2e > div > a:visited > .glname > .glink {color: black;}`)
      })

      if (IS_SEARCH_PAGE) {
        this._handleDefaults()
      }
    })

    this._onUIBuilt(() => {

      this._uiGen.getSelectedSection()[0].userScript = this

      if (IS_GALLERY_PAGE) {
        this._performOperation(UI_EMBED_TORRENTS, () => this._handleTorrentDownloadsEmbedding())
      }
    })

    this._onItemHide = (item) => {
      if (item.is('td.gl2e')) {
        item.parent().addClass('noncompliant-item')
        item.parent().hide()
      } else {
        item.removeClass('noncompliant-item')
        item.hide()
      }
    }

    this._onItemShow.push((item) => {
      if (item.is('td.gl2e')) {
        item.parent().removeClass('noncompliant-item')
        item.parent().show()
      } else {
        item.removeClass('noncompliant-item')
        item.show()
      }
    })
  }

  /**
   * @private
   */
  _setupFeatures()
  {
    this._configurationManager.
        addFlagField(
            FILTER_HIDE_WATCHED_FROM_SEARCH,
            'Hides watched galleries from searches initiated other than the watched page.').
        addFlagField(
            UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES, 'Automatically navigates to the next page after opening all images.').
        addFlagField(
            UI_DEFAULTS_PAGE_RANGE_ENABLE,
            'Always set these page limits in searches. Ignored if you set your own values on the page.').
        addFlagField(
            UI_DEFAULTS_RATING_ENABLE, 'Enable default rating filter in searches').
        addFlagField(
            UI_DEFAULTS_TAGS_ENABLE, 'Enable default tags in searches.').
        addFlagField(
            UI_EMBED_TORRENTS, 'Embed torrent downloads in gallery pages.').
        addFlagField(
            UI_VISITED_HIGHLIGHT, 'Colours the visited gallery links black, to make them distinct.').
        addRadiosGroup(
            UI_DEFAULTS_RATING,
            [
              ['2 stars', '2'],
              ['3 stars', '3'],
              ['4 stars', '4'],
              ['5 stars', '5'],
            ],
            'Always set this rating filter in searches. Ignored if you set your own value on the page.').
        addRangeField(
            UI_DEFAULTS_PAGE_RANGE, 0, 2000, 'Enable default page range filter in searches.').
        addRulesetField(
            UI_DEFAULTS_TAGS,
            3,
            'Always add the following tags in search. Can be overridden with at least one tag present.')

    this._addItemTagAttribute(ITEM_TAGS, !IS_EXTENDED_LAYOUT, false, (item) => {
      let tags = []
      let tagElements = item.find('.gt,.gtl')
      if (IS_EXTENDED_LAYOUT) {
        tagElements.each((_i, e) => {
          tags.push($(e).attr('title'))
        })
      } else {
        tagElements.each((_i, e) => {
          let tagID = $(e).find('a').attr('id')
          if (tagID.startsWith('ta_')) {
            tagID = tagID.replace('ta_', '')
          }
          if (tagID.startsWith('td_')) {
            tagID = tagID.replace('td_', '')
          }
          tags.push(tagID.replace('_', ' '))
        })
      }
      return tags
    })

    this._itemAttributesResolver.addAttribute(
        ITEM_WATCHED,
        (item) => item.find('.gt[style],.gtl[style]').length > 0)

    let otherTagSections = IS_GALLERY_PAGE ? $('#taglist') : null

    this._addItemComplexComplianceFilter(
        FILTER_HIDE_WATCHED_FROM_SEARCH,
        (enabled) => !IS_GALLERY_PAGE && !IS_WATCHED_PAGE && enabled,
        (item) => !this._get(item, ITEM_WATCHED))

    this._addItemTagHighlights(
        UI_FAVOURITE_TAGS,
        otherTagSections,
        'favourite-tag',
        'Specify favourite tags to highlight.',
        10,
        'disliked-tag')

    this._addItemTagHighlights(
        UI_DISLIKED_TAGS,
        otherTagSections,
        'disliked-tag',
        'Specify disliked tags to highlight.',
        10,
        'favourite-tag')

    this._addItemTagBlacklistFilter(ITEM_TAGS, false, 20)
  }

  /**
   * @private
   */
  _setupUI()
  {
    let galleryOptions = []
    if (IS_GALLERY_PAGE) {

      galleryOptions = [
        this._uiGen.createSeparator(),
        this._uiGen.createFormButton(
            'Open Gallery Images',
            'Opens all images on current page of this gallery.',
            () => this._handleOpenGalleryImages(),
        ),
      ]
    }

    this._userInterface = [
      this._uiGen.createTabsSection(['Filters', 'Highlights', 'Defaults', 'Global'], [
        this._uiGen.createTabPanel('Filters', true).append([
          this._configurationManager.createElement(FILTER_HIDE_WATCHED_FROM_SEARCH),
          this._configurationManager.createElement(OPTION_ENABLE_TAG_BLACKLIST),
          this._configurationManager.createElement(FILTER_TAG_BLACKLIST),
        ]),
        this._uiGen.createTabPanel('Highlights').append([
          this._configurationManager.createElement(UI_FAVOURITE_TAGS),
          this._configurationManager.createElement(UI_DISLIKED_TAGS),
        ]),
        this._uiGen.createTabPanel('Defaults').append([
          this._configurationManager.createElement(UI_DEFAULTS_PAGE_RANGE_ENABLE),
          this._configurationManager.createElement(UI_DEFAULTS_PAGE_RANGE),
          this._uiGen.createSeparator(),
          this._configurationManager.createElement(UI_DEFAULTS_RATING),
          this._uiGen.createBreakSeparator(),
          this._configurationManager.createElement(UI_DEFAULTS_RATING_ENABLE),
          this._uiGen.createSeparator(),
          this._configurationManager.createElement(UI_DEFAULTS_TAGS_ENABLE),
          this._configurationManager.createElement(UI_DEFAULTS_TAGS),
        ]),
        this._uiGen.createTabPanel('Global').append([
          this._configurationManager.createElement(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES),
          this._configurationManager.createElement(UI_EMBED_TORRENTS),
          this._configurationManager.createElement(UI_VISITED_HIGHLIGHT),
          this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
          this._uiGen.createSeparator(),
          this._createSettingsBackupRestoreFormActions(),
        ]),
      ]),
      (IS_GALLERY_PAGE || IS_WATCHED_PAGE) ? '' : this._uiGen.createStatisticsFormGroup(
          FILTER_HIDE_WATCHED_FROM_SEARCH),
      IS_GALLERY_PAGE ? '' : this._uiGen.createStatisticsFormGroup(FILTER_TAG_BLACKLIST),
      ...galleryOptions,
      this._uiGen.createSeparator(),
      this._createSettingsFormActions(),
      this._uiGen.createSeparator(),
      this._uiGen.createStatusSection(),
    ]
  }
}

if (!IS_IMAGE_PAGE) {
  (new EHentaiSearchAndUITweaks).init()
}