E-Hentai - UX Tweaks

Numerous features to enrich your browsing experience

As of 15.10.2023. See апошняя версія.

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.3.2
// @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://greasyfork.org/scripts/375557-base-brazen-resource/code/Base%20Brazen%20Resource.js?version=1244990
// @require      https://greasyfork.org/scripts/416104-brazen-ui-generator/code/Brazen%20UI%20Generator.js?version=1264763
// @require      https://greasyfork.org/scripts/418665-brazen-configuration-manager/code/Brazen%20Configuration%20Manager.js?version=1245040
// @require      https://greasyfork.org/scripts/429587-brazen-item-attributes-resolver/code/Brazen%20Item%20Attributes%20Resolver.js?version=1244644
// @require      https://greasyfork.org/scripts/416105-brazen-base-search-enhancer/code/Brazen%20Base%20Search%20Enhancer.js?version=1264747
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

GM_addStyle(
    `#settings-wrapper{min-width:300px;width:300px}.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_SEARCH_PAGE = $('#f_search').length
const IS_SMALL_WINDOW = $('.stuffbox').length

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_FAVOURITE_TAGS = 'Favourite Tags'
const UI_DISLIKED_TAGS = 'Disliked Tags'
const UI_VISITED_HIGHLIGHT = 'Highlight Visited'
const UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES = 'Auto Next Page'

class EHentaiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
    constructor()
    {
        super({
            isUserLoggedIn: false,
            itemDeepAnalysisSelector: '',
            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) {
                    return 'div[id="td_' + tag.replace(' ', '_') + '"]'
                }
                return 'div.gt[title="' + tag + '"], div.gtl[title="' + tag + '"]'
            },
        })
        this._setupFeatures()
        this._setupUI()
        this._setupEvents()
    }

    /**
     * @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 += '+' + 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 = window.location.pathname.split('/').pop().trim()
                if (existingTag.length) {
                    queryParams.set('f_search', 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().replace('%2B', '+')
            if (updatedParams !== existingParams) {
                window.location = window.location.origin + '?' + updatedParams
            }
        }
    }

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

        this._onAfterUIBuild.push(() => this._uiGen.getSelectedSection()[0].userScript = this)

        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 = (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(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES, 'Automatically navigates to the next page after opening all images.')
            .addFlagField(UI_VISITED_HIGHLIGHT, 'Colours the visited gallery links black, to make them distinct.')
            .addRangeField(UI_DEFAULTS_PAGE_RANGE, 0, 2000, 'Enable default page range filter in searches.')
            .addFlagField(UI_DEFAULTS_PAGE_RANGE_ENABLE, 'Always set these page limits in searches. Ignored if you set your own values on the page.')
            .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.')
            .addFlagField(UI_DEFAULTS_RATING_ENABLE, 'Enable default rating filter in searches')
            .addRulesetField(UI_DEFAULTS_TAGS, 5, 'Always add the following tags in search. Can be overridden with at least one tag present.')
            .addFlagField(UI_DEFAULTS_TAGS_ENABLE, 'Enable default tags in searches.')

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

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

    /**
     * @private
     */
    _setupUI()
    {
        let openGalleryPages = []
        if (IS_GALLERY_PAGE) {
            openGalleryPages = [
                this._uiGen.createSeparator(),
                this._uiGen.createFormButton(
                    'Open Gallery Images',
                    'Opens all images on current page of this gallery.',
                    () => {

                        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
                            }
                        }
                    }),
                this._uiGen.createBreakSeparator(),
                this._configurationManager.createElement(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES),
            ]
        }

        this._userInterface = [
            this._uiGen.createTabsSection(['Filters', 'Highlights', 'Defaults', 'Global'], [
                this._uiGen.createTabPanel('Filters', true).append([
                    this._configurationManager.createElement(OPTION_ENABLE_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_VISITED_HIGHLIGHT),
                    this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
                    this._uiGen.createSeparator(),
                    this._createSettingsBackupRestoreFormActions(),
                ]),
            ]),
            this._uiGen.createStatisticsFormGroup(FILTER_TAG_BLACKLIST),
            ...openGalleryPages,
            this._uiGen.createSeparator(),
            this._createSettingsFormActions(),
            this._uiGen.createSeparator(),
            this._uiGen.createStatusSection(),
        ]
    }
}

(new EHentaiSearchAndUITweaks).init()