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 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.15.1
// @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{min-width:310px;width:310px}.bv-section{font-size:1.25rem}.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_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_WATCHED_FROM_SEARCH = 'Hide Watched Galleries'
const FILTER_RATED_VIDEOS = 'Hide Rated 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_DISLIKED_TAGS = 'Disliked Tags'
const UI_FAVOURITE_TAGS = 'Favourite Tags'
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'

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

  /**
   * @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, 6, '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_FAVOURITE_TAGS,
        otherTagSections,
        'favourite-tag',
        'Specify favourite tags to highlight.',
        12,
        'disliked-tag')

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

    this._addItemTagBlacklistFilter(ITEM_TAGS, false, 21)
    this._addItemBlacklistFilter('Hide galleries with specified phrases in their names.', 12)
  }

  /**
   * @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),
        IS_WATCHED_PAGE ? '' : this._uiGen.createStatisticsFormGroup(FILTER_WATCHED_FROM_SEARCH),
      ]
    }

    this._userInterface = [
      this._uiGen.createTabsSection(['Filters', 'Filters 2', '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_FAVOURITE_TAGS),
          this._configurationManager.createElement(UI_DISLIKED_TAGS),
        ]),
        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('Backup').append([
          this._createSettingsBackupRestoreFormActions(),
        ]),
      ]),
      this._uiGen.createBottomSection([
        ...statistics,
        this._uiGen.createSeparator(),
        pageSpecificButton,
        this._uiGen.createSeparator(),
        this._createSettingsFormActions(),
      ]),
    ]
  }
}

(new EHentaiSearchAndUITweaks).init()