E-Hentai - UX Tweaks

Numerous features to enrich your browsing experience

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 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.16.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/Base%20Brazen%20Resource.js
// @require      https://update.greasyfork.org/scripts/416104/Brazen%20UI%20Generator.js
// @require      https://update.greasyfork.org/scripts/418665/Brazen%20Configuration%20Manager.js
// @require      https://update.greasyfork.org/scripts/429587/Brazen%20Item%20Attributes%20Resolver.js
// @require      https://update.greasyfork.org/scripts/416105/Brazen%20Base%20Search%20Enhancer.js
// @grant        GM_addStyle
// @grant        GM_download
// @run-at       document-end
// ==/UserScript==

GM_addStyle(
    `#bv-ui{font-size:1.25rem;min-width:310px;width:310px}.adult-tag::after{content:'\\2713';position:absolute;top:-8px;right:0;font-size:10px;color:aquamarine}.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}.underage-tag::after{content:'\\2717';position:absolute;top:-8px;right:0;font-size:10px;color:red}.unknown-age-tag::after{content:'\\003F';position:absolute;top:-8px;right:0;font-size:10px;color:yellow}`)

const IS_GALLERY_PAGE = $('#gdt').length
const IS_IMAGE_PAGE = window.location.pathname.startsWith('/s/')
const IS_SEARCH_PAGE = $('#f_search').length
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 IS_EXTENDED_LAYOUT = IS_SEARCH_PAGE && $('table.itg.glte').length > 0
const IS_MINIMAL_LAYOUT = !IS_EXTENDED_LAYOUT && $('table.itg.gltm').length > 0
const IS_COMPACT_LAYOUT = !IS_MINIMAL_LAYOUT && $('table.itg.gltc').length > 0
const IS_THUMBNAIL_LAYOUT = !IS_COMPACT_LAYOUT && $('div.itg.gld').length > 0

const ITEM_RATED_BLUE = 'ratedBlue'
const ITEM_RATED_GREEN = 'ratedGreen'
const ITEM_RATED_RED = 'ratedRed'
const ITEM_TAGS = 'tags'
const ITEM_WATCHED = 'watched'

const FILTER_RATED_VIDEOS = 'Hide Rated Galleries'
const FILTER_UNDERAGE_CHARACTERS_OPTION = 'Enable Underage Females Filter'
const FILTER_UNDERAGE_CHARACTERS_BLACKLIST = 'Underage Females Blacklist'
const FILTER_WATCHED_FROM_SEARCH = 'Hide Watched Galleries'

const STYLE_GALLERY_HIGHLIGHT = 'gallery-highlight'

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_OPEN_GALLERIES_AUTO_NEXT = 'Auto Next Page'
const UI_OPEN_GALLERY_PAGES_AUTO_NEXT = 'Auto Next Page'
const UI_OPEN_GALLERY_PAGES_THROTTLING = 'Throttling Bypass'
const UI_OPEN_GALLERY_PAGES_DOWNLOAD = 'Download The Image'

const UI_EMBED_TORRENTS = 'Embed Torrent Downloads'
const UI_VISITED_HIGHLIGHT = 'Highlight Visited'
const UI_GALLERY_HIGHLIGHTS = 'Gallery Highlights'
const UI_GALLERY_HIGHLIGHTS_COlOUR = 'Highlight Colour'

const UI_TAG_HIGHLIGHTS_ADULT_CHARACTER = 'Adult Characters'
const UI_TAG_HIGHLIGHTS_DISLIKED = 'Disliked Tags'
const UI_TAG_HIGHLIGHTS_FAVOURITE = 'Favourite Tags'
const UI_TAG_HIGHLIGHTS_UNDERAGE_CHARACTER = 'Underage Characters'
const UI_TAG_HIGHLIGHTS_UNKNOWN_AGE_CHARACTER = 'Unknown Age Characters'

let selectorItem = '', selectorItemLink = '', selectorItemList = '', selectorItemName = ''
if (IS_EXTENDED_LAYOUT) {
  selectorItem = 'tr'
  selectorItemLink = '.gl2e > div > a'
  selectorItemList = '.itg.glte > tbody'
  selectorItemName = '.gl4e.glname > div.glink'
} else if (IS_COMPACT_LAYOUT) {
  selectorItem = 'tr'
  selectorItemLink = '.gl3c.glname > a'
  selectorItemList = '.itg.gltc > tbody'
  selectorItemName = '.gl3c.glname > a > div.glink'
} else if (IS_MINIMAL_LAYOUT) {
  selectorItem = 'tr'
  selectorItemLink = '.gl3m.glname > a'
  selectorItemList = '.itg.gltm > tbody'
  selectorItemName = '.gl3m.glname > a > div.glink'
} else if (IS_THUMBNAIL_LAYOUT) {
  selectorItem = 'div.gl1t'
  selectorItemLink = '.gl3t > a'
  selectorItemList = '.itg.gld'
  selectorItemName = '.gl4t.glname'
}

class EHentaiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
  constructor()
  {
    super({
      downloadsDelay: 2,
      isUserLoggedIn: false,
      itemDeepAnalysisSelector: 'div.gm',
      itemLinkSelector: selectorItemLink,
      itemListSelectors: selectorItemList,
      itemNameSelector: selectorItemName,
      itemSelectors: selectorItem,
      itemSelectionMethod: 'children',
      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._setupFilters()
    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 {JQuery} item
   * @return {string[]}
   * @private
   */
  _gatherItemTags(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
  }

  /**
   * @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_spt', 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
        }
      }
    }
  }

  /**
   * @param {boolean} removeElement
   * @private
   */
  _handleDownloadMedia(removeElement)
  {
    let image = document.querySelector('#img')

    this._addDownload(image, document.title.replace(' --- Porn', ''), image.src.split('/').pop(), removeElement)
  }

  /**
   * @param {JQuery} item
   * @private
   */
  _handleGalleryHighlights(item)
  {
    let mode = this._getConfig(UI_GALLERY_HIGHLIGHTS)
    let itemHasHighlight = item.hasClass(STYLE_GALLERY_HIGHLIGHT)

    if (mode !== 'Disabled') {

      let itemTags = this._get(item, ITEM_TAGS), doHighlight, tag
      if (itemTags) {

        for (let rule of this._configurationManager.getField(UI_FAVOURITE_TAGS).optimized) {

          doHighlight = true
          for (let tagSelector of rule) {

            tag = tagSelector.split('"], div.gtl[title="').pop().replace('"]', '')

            if ((mode === 'All' && !itemTags.includes(tag)) ||
                (mode === 'Source' && ((!tag.startsWith('artist:') && !tag.startsWith('group:')) || !itemTags.includes(tag)))) {
              doHighlight = false
              break
            }
          }

          if (doHighlight) {
            if (!itemHasHighlight) {
              item.addClass(STYLE_GALLERY_HIGHLIGHT)
            }
            break
          }
        }

        if (!doHighlight && itemHasHighlight) {
          item.removeClass(STYLE_GALLERY_HIGHLIGHT)
        }
      }

    } else if (itemHasHighlight) {
      item.removeClass(STYLE_GALLERY_HIGHLIGHT)
    }
  }

  _handleOpenGalleries()
  {
    let links = document.querySelectorAll('.' + CLASS_COMPLIANT_ITEM + ' > ' + selectorItemLink)
    if (links.length > 0) {
      links.forEach(itemLink => window.open(itemLink.href))

      if (this._getConfig(UI_OPEN_GALLERIES_AUTO_NEXT)) {
        document.querySelector('#unext').click()
      }
    }
  }

  /**
   * @private
   */
  async _handleOpenGalleryImages()
  {
    let bypassThrottling = this._getConfig(UI_OPEN_GALLERY_PAGES_THROTTLING)
    let images = $('#gdt > a')
    let iteration = 0
    let firstPageNumber = images.first().attr('href').split('-').pop()
    let maxPages = firstPageNumber + images.length - 1

    for (let page = images.length - 1; page >= 0; page--) {

      window.open(images.eq(page).attr('href'))

      if (bypassThrottling && page !== 0) {
        await Utilities.sleep(2000)
      }
    }

    if (this._getConfig(UI_OPEN_GALLERY_PAGES_AUTO_NEXT)) {

      let page = window.location.href.split('=')[1] || 0
      let pageNavs = $('.ptt td')

      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
      }
    }
  }

  /**
   * @param {JQuery} item
   * @param {string} option
   * @private
   */
  _handleRatedGalleries(item, option)
  {
    let doesntComply
    switch (option) {
      case 'Blue':
        doesntComply = this._get(item, ITEM_RATED_BLUE)
        break
      case 'Green':
        doesntComply = this._get(item, ITEM_RATED_GREEN)
        break
      case 'Red':
        doesntComply = this._get(item, ITEM_RATED_RED)
        break
      case 'All':
        doesntComply = this._get(item, ITEM_RATED_BLUE) || this._get(item, ITEM_RATED_GREEN) || this._get(item, ITEM_RATED_RED)
        break
    }
    return !doesntComply
  }

  /**
   * @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()
      })
    }
  }

  _handleUnderageFilter(otherTagSections)
  {
    this._configurationManager.
        addFlagField(FILTER_UNDERAGE_CHARACTERS_OPTION, 'Applies the underage filter.').
        addRulesetField(
            FILTER_UNDERAGE_CHARACTERS_BLACKLIST,
            5,
            `Filter and mark character tags that are not adults in any canon timelines.
            Set only character names. Reference can be added as a comment after ' // ' token.`,
            (characterNames) => {
              this._configurationManager.getField(UI_TAG_HIGHLIGHTS_UNDERAGE_CHARACTER).value = this._morphCharacterNamesToTags(characterNames)
              return characterNames
            },
            null,
            (characterNames) => {
              let rules = []
              for (let characterName of characterNames) {
                rules.push('character:' + characterName)
              }
              return this._optimizeTagRuleset(rules, false)
            })

    this._addItemTagHighlights(
        UI_TAG_HIGHLIGHTS_UNDERAGE_CHARACTER,
        otherTagSections,
        'underage-tag',
        '',
        0,
        'favourite-tag disliked-tag')

    this._addItemTagHighlights(
        UI_TAG_HIGHLIGHTS_UNKNOWN_AGE_CHARACTER,
        otherTagSections,
        'unknown-age-tag',
        'Specify characters which don\'t have officially declared age.',
        5,
        '',
        (characterNames) => this._morphCharacterNamesToTags(characterNames))

    this._addItemTagHighlights(
        UI_TAG_HIGHLIGHTS_ADULT_CHARACTER,
        otherTagSections,
        'adult-tag',
        'Specify characters checked to be adults.',
        5,
        '',
        (characterNames) => this._morphCharacterNamesToTags(characterNames))

    this._addItemComplexComplianceFilter(
        FILTER_UNDERAGE_CHARACTERS_BLACKLIST,
        (rules) => this._getConfig(FILTER_UNDERAGE_CHARACTERS_OPTION) && rules.length,
        (item, blacklistRuleset) => this._handleComplianceForBlacklistFilter(item, ITEM_TAGS, blacklistRuleset),
    )
  }

  /**
   * @param {string[]} characterNames
   * @return {string[]}
   * @private
   */
  _morphCharacterNamesToTags(characterNames)
  {
    let rules = []
    for (let characterName of characterNames) {
      rules.push('character:' + characterName.split(' // ')[0])
    }
    return rules
  }

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

    this._onBeforeUIBuild.push(() => {

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

      if (IS_SEARCH_PAGE) {
        this._handleDefaults()
        GM_addStyle('.gallery-highlight{background-color:' + this._getConfig(UI_GALLERY_HIGHLIGHTS_COlOUR) + ' !important;border:whitesmoke 2px solid}')
      }

      if (IS_IMAGE_PAGE && this._getConfig(UI_OPEN_GALLERY_PAGES_DOWNLOAD)) {
        this._handleDownloadMedia(true)
        this._disableUI = true
      }
    })

    this._onAfterUIBuild.push(() => {

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

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

    this._onFirstHitBeforeCompliance.push((item) => {
      let tags = this._itemAttributesResolver.get(item, ITEM_TAGS) || []
      let galleryType = item.find('.cn,.cs').first().text()
      let galleryTypeMap = new Map([
        ['Artist CG', 'acg'],
        ['Asian Porn', 'aporn'],
        ['Cosplay', 'cosplay'],
        ['Doujinshi', 'dj'],
        ['Game CG', 'gcg'],
        ['Image Set', 'img'],
        ['Manga', 'manga'],
        ['Misc', 'misc'],
        ['Non-H', 'nonh'],
        ['Western', 'west'],
      ])
      tags.push('type:' + (galleryTypeMap.get(galleryType)))

      this._itemAttributesResolver.set(item, ITEM_TAGS, tags)
    })

    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()
      }
    })

    if (IS_SEARCH_PAGE) {
      this._onItemShow.push((item) => this._handleGalleryHighlights(item))
    }
  }

  /**
   * @private
   */
  _setupFeatures()
  {
    this._configurationManager.
        addColorField(
            UI_GALLERY_HIGHLIGHTS_COlOUR, 'Colour to highlight the galleries with. Requires refresh to change.').
        addFlagField(
            FILTER_WATCHED_FROM_SEARCH, 'Hides watched galleries from searches initiated other than the watched page.').
        addFlagField(
            UI_OPEN_GALLERIES_AUTO_NEXT, 'Automatically navigates to the next page after opening all galleries.').
        addFlagField(
            UI_OPEN_GALLERY_PAGES_AUTO_NEXT, 'Automatically navigates to the next page after opening all images.').
        addFlagField(
            UI_OPEN_GALLERY_PAGES_DOWNLOAD, 'Download image on a gallery page to a folder named after the gallery.').
        addFlagField(
            UI_OPEN_GALLERY_PAGES_THROTTLING,
            'Paces the page opening logic to not trigger the site\'s throttling mechanism which limits resolution to 1280x.').
        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(
            FILTER_RATED_VIDEOS,
            [
              ['Disabled', 'Disabled'],
              ['Red', 'Red'],
              ['Blue', 'Blue'],
              ['Green', 'Green'],
              ['All', 'All'],
            ],
            'Hides galleries rated by you with the colour set in site settings or all.').
        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.').
        addRadiosGroup(
            UI_GALLERY_HIGHLIGHTS,
            [
              ['Disabled', 'Disabled'],
              ['All Favourite Tags', 'All'],
              ['Only Group / Artist Tags', 'Source'],
            ],
            'Highlights favourite galleries in search results with at least one matching tag.').
        addRangeField(
            UI_DEFAULTS_PAGE_RANGE, 0, 2000, 'Enable default page range filter in searches.').
        addRulesetField(
            UI_DEFAULTS_TAGS, 4, '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) => this._gatherItemTags(item))

    this._itemAttributesResolver.
        addAttribute(ITEM_WATCHED, (item) => item.find('.gt[style],.gtl[style]').attr('style')?.startsWith('color:#f1f1f1') || false).
        addAttribute(ITEM_RATED_BLUE, (item) => item.find('.irb').length > 0).
        addAttribute(ITEM_RATED_GREEN, (item) => item.find('.irg').length > 0).
        addAttribute(ITEM_RATED_RED, (item) => item.find('.irr').length > 0)
  }

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

    this._addItemComplexComplianceFilter(
        FILTER_RATED_VIDEOS,
        (option) => option !== 'Disabled',
        (item, option) => this._handleRatedGalleries(item, option))

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

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

    this._addItemTagBlacklistFilter(ITEM_TAGS, false, 20)
    this._addItemBlacklistFilter('Hide galleries with specified phrases in their names.', 10)

    this._handleUnderageFilter(otherTagSections)
  }

  /**
   * @private
   */
  _setupUI()
  {
    let pageSpecificButton, statistics = []
    if (IS_GALLERY_PAGE) {

      pageSpecificButton = this._uiGen.createFormButton(
          'Open Gallery Images',
          'Opens all images on current page of this gallery.',
          () => this._handleOpenGalleryImages())

    } else if (IS_IMAGE_PAGE) {

      pageSpecificButton = this._uiGen.createFormButton(
          'Download Image',
          'Downloads the image on the page.',
          () => this._handleDownloadMedia(false))

    } else {

      pageSpecificButton = this._uiGen.createFormButton(
          'Open All Galleries',
          'Opens all galleries on current page.',
          () => this._handleOpenGalleries())
      statistics = [
        this._uiGen.createStatisticsFormGroup(FILTER_TEXT_BLACKLIST),
        this._uiGen.createStatisticsFormGroup(FILTER_TAG_BLACKLIST),
        this._uiGen.createStatisticsFormGroup(FILTER_RATED_VIDEOS),
        this._uiGen.createStatisticsFormGroup(FILTER_UNDERAGE_CHARACTERS_BLACKLIST),
        IS_WATCHED_PAGE ? '' : this._uiGen.createStatisticsFormGroup(FILTER_WATCHED_FROM_SEARCH),
      ]
    }

    this._userInterface = [
      this._uiGen.createTabsSection(['Filters', 'Filters 2', 'Underage Filter', 'Galleries', 'Tag Highlights', 'Search Defaults', 'UI', 'Backup'], [
        this._uiGen.createTabPanel('Filters', true).append([
          this._configurationManager.createElement(FILTER_WATCHED_FROM_SEARCH),
          this._configurationManager.createElement(OPTION_ENABLE_TAG_BLACKLIST),
          this._configurationManager.createElement(FILTER_TAG_BLACKLIST),
          this._configurationManager.createElement(OPTION_DISABLE_COMPLIANCE_VALIDATION),
        ]),
        this._uiGen.createTabPanel('Filters 2').append([
          this._configurationManager.createElement(FILTER_RATED_VIDEOS),
          this._uiGen.createSeparator(),
          this._configurationManager.createElement(OPTION_ENABLE_TEXT_BLACKLIST),
          this._configurationManager.createElement(FILTER_TEXT_BLACKLIST),
        ]),
        this._uiGen.createTabPanel('Galleries').append([
          this._uiGen.createTitle('Open Galleries'),
          this._configurationManager.createElement(UI_OPEN_GALLERIES_AUTO_NEXT),
          this._uiGen.createSeparator(),
          this._uiGen.createTitle('Open Images'),
          this._configurationManager.createElement(UI_OPEN_GALLERY_PAGES_AUTO_NEXT),
          this._configurationManager.createElement(UI_OPEN_GALLERY_PAGES_THROTTLING),
          this._uiGen.createSeparator(),
          this._uiGen.createTitle('Image Pages'),
          this._configurationManager.createElement(UI_OPEN_GALLERY_PAGES_DOWNLOAD),
          this._uiGen.createSeparator(),
          this._configurationManager.createElement(UI_GALLERY_HIGHLIGHTS),
          this._uiGen.createBreakSeparator(),
          this._uiGen.createBreakSeparator(),
          this._configurationManager.createElement(UI_GALLERY_HIGHLIGHTS_COlOUR),
        ]),
        this._uiGen.createTabPanel('Tag Highlights').append([
          this._configurationManager.createElement(UI_TAG_HIGHLIGHTS_FAVOURITE),
          this._configurationManager.createElement(UI_TAG_HIGHLIGHTS_DISLIKED),
        ]),
        this._uiGen.createTabPanel('Search 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('UI').append([
          this._configurationManager.createElement(UI_EMBED_TORRENTS),
          this._configurationManager.createElement(UI_VISITED_HIGHLIGHT),
          this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
        ]),
        this._uiGen.createTabPanel('Underage Filter').append([
          this._configurationManager.createElement(FILTER_UNDERAGE_CHARACTERS_OPTION),
          this._configurationManager.createElement(FILTER_UNDERAGE_CHARACTERS_BLACKLIST),
          this._configurationManager.createElement(UI_TAG_HIGHLIGHTS_UNKNOWN_AGE_CHARACTER),
          this._configurationManager.createElement(UI_TAG_HIGHLIGHTS_ADULT_CHARACTER),
        ]),
        this._uiGen.createTabPanel('Backup').append([
          this._createSettingsBackupRestoreFormActions(),
        ]),
      ]),
      this._uiGen.createBottomSection([
        ...statistics,
        this._uiGen.createSeparator(),
        pageSpecificButton,
        this._uiGen.createSeparator(),
        this._createSettingsFormActions(),
      ]),
    ]
  }
}

(new EHentaiSearchAndUITweaks).init()