Hitomi - Search & UI Tweaks

Various search filters and user experience enhancers

2023/01/24時点のページです。最新版はこちら。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Hitomi - Search & UI Tweaks
// @namespace    brazenvoid
// @version      5.2.2
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Various search filters and user experience enhancers
// @match        https://hitomi.la/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js
// @require      https://greasyfork.org/scripts/375557-base-brazen-resource/code/Base%20Brazen%20Resource.js?version=1115796
// @require      https://greasyfork.org/scripts/416104-brazen-ui-generator/code/Brazen%20UI%20Generator.js?version=1115813
// @require      https://greasyfork.org/scripts/418665-brazen-configuration-manager/code/Brazen%20Configuration%20Manager.js?version=1114733
// @require      https://greasyfork.org/scripts/429587-brazen-item-attributes-resolver/code/Brazen%20Item%20Attributes%20Resolver.js?version=1139392
// @require      https://greasyfork.org/scripts/416105-brazen-base-search-enhancer/code/Brazen%20Base%20Search%20Enhancer.js?version=1139379
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

GM_addStyle(
    `#settings-wrapper{font-family:Open Sans;font-size:12px;font-weight:700;line-height:1;background-color:rgb(216, 210, 234);top:5vh;width:320px}
          #settings-wrapper button{font-family:Open Sans;font-size:12px;font-weight:700}
          #settings-wrapper textarea.form-input{height:25vh;line-height:2;overflow:auto;padding:0 10px;resize:vertical;white-space:pre;width:92%}
          #settings-wrapper .font-secondary{color:black}
          .bg-brand{background-color:rgb(216, 210, 234)}
          .blacklisted{background-color:lightcoral!important}.blacklisted:hover{background-color:indianred!important}
          .blacklisted.favourite{background-color:orange!important}.blacklisted.favourite:hover{background-color:darkorange!important}
          .favourite{background-color:mediumseagreen!important}.favourite:hover{background-color:forestgreen!important}`)

const IS_GALLERY_PAGE = $('#dl-button').length

const FILTER_GALLERY_TYPES = 'Show Gallery Types'
const FILTER_PAGES = 'Pages'
const FILTER_TAG_BLACKLIST = 'Tag Blacklist'
const FILTER_LANGUAGES = 'Languages'

const ENABLE_BLACKLIST = 'Enable Blacklist'
const HIGHLIGHT_BLACKLISTED_TAGS = 'Highlight Blacklisted Tags'
const OPTION_REMOVE_RELATED_GALLERIES = 'Remove Related Galleries'

const UI_FAVOURITE_TAGS = 'Favourite Tags'
const UI_SHOW_ALL_TAGS = 'Show All Gallery Tags'

class HitomiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
    constructor()
    {
        super({
            isUserLoggedIn:           false,
            itemDeepAnalysisSelector: '',
            itemLinkSelector:         '',
            itemListSelectors:        '.gallery-content',
            itemNameSelector:         'h1.lillie a',
            itemSelectors:            '.acg,.anime,.cg,.dj,.manga',
            requestDelay:             0,
            scriptPrefix:             'hitomi-sui-',
        })

        this._configurationManager
            .addCheckboxesGroup(FILTER_GALLERY_TYPES, [
                ['Anime', 'anime'],
                ['Artist CG', 'acg'],
                ['Doujinshi', 'dj'],
                ['Game CG', 'cg'],
                ['Manga', 'manga'],
            ], 'Show only selected gallery types.')
            .addCheckboxesGroup(FILTER_LANGUAGES, [
                ['N/A', 'not-applicable'],
                ['Japanese', 'japanese'],
                ['Chinese', 'chinese'],
                ['English', 'english'],
                ['Albanian', 'albanian'],
                ['Arabic', 'arabic'],
                ['Bulgarian', 'bulgarian'],
                ['Catalan', 'catalan'],
                ['Cebuano', 'cebuano'],
                ['Czech', 'czech'],
                ['Danish', 'danish'],
                ['Dutch', 'dutch'],
                ['Esperanto', 'esperanto'],
                ['Estonian', 'estonian'],
                ['Finnish', 'finnish'],
                ['French', 'french'],
                ['German', 'german'],
                ['Greek', 'greek'],
                ['Hebrew', 'hebrew'],
                ['Hungarian', 'hungarian'],
                ['Indonesian', 'indonesian'],
                ['Italian', 'italian'],
                ['Korean', 'korean'],
                ['Latin', 'latin'],
                ['Mongolian', 'mongolian'],
                ['Norwegian', 'norwegian'],
                ['Persian', 'persian'],
                ['Polish', 'polish'],
                ['Portuguese', 'portuguese'],
                ['Romanian', 'romanian'],
                ['Russian', 'russian'],
                ['Slovak', 'slovak'],
                ['Spanish', 'spanish'],
                ['Swedish', 'swedish'],
                ['Tagalog', 'tagalog'],
                ['Thai', 'thai'],
                ['Turkish', 'turkish'],
                ['Ukrainian', 'ukrainian'],
                ['Unspecified', 'unspecified'],
                ['Vietnamese', 'vietnamese'],
            ], 'Select languages to show')
            .addFlagField(ENABLE_BLACKLIST, 'Applies the blacklist.')
            .addFlagField(HIGHLIGHT_BLACKLISTED_TAGS, 'Highlights blacklisted tags. Only works with a disabled blacklist.')
            .addFlagField(OPTION_REMOVE_RELATED_GALLERIES, 'Remove related galleries section from gallery pages.')
            .addFlagField(UI_SHOW_ALL_TAGS, 'Show all gallery tags in search results.')
            .addRangeField(FILTER_PAGES, 0, Infinity, 'Close gallery pages that don\'t satisfy these page limits. Only works on galleries opened in new tabs.')
            .addRulesetField(
                FILTER_TAG_BLACKLIST,
                10,
                'Specify the tags blacklist with one rule on each line. While single tags can be specified, complex multi-tag rules with & (AND) and | (OR) can also be defined.',
                null,
                null,
                (blacklistedTags) => this._optimizeBlacklistRules(blacklistedTags))
            .addRulesetField(
                UI_FAVOURITE_TAGS,
                10,
                'Specify favourite tags that get highlighted in search results.',
                null,
                null,
                (favouriteTags) => this._optimizeFavouriteRules(favouriteTags))

        this._onFirstHitAfterCompliance = (item) => {
            this._highlightFavouriteTags(item)

            this._performComplexFlaggedOperation(
                HIGHLIGHT_BLACKLISTED_TAGS, () => !this._getConfig(ENABLE_BLACKLIST), () => this._highlightBlacklistedTags(item))

            this._performComplexFlaggedOperation(UI_SHOW_ALL_TAGS, () => !IS_GALLERY_PAGE, () => this._showAllTags(item))
        }

        this._setupUI()
        this._setupComplianceFilters()
    }

    /**
     * @private
     */
    _checkPageLimits()
    {
        if (!this._getConfig(OPTION_DISABLE_COMPLIANCE_VALIDATION)) {
            let range = this._getConfig(FILTER_PAGES)
            if (range.minimum > 0 || range.maximum > 0) {

                let navPages = $('.simplePagerNav li').length
                let pageCount = navPages > 0 ? navPages * 50 : $('.simplePagerPage1').length
                if (!Validator.isInRange(pageCount, range.minimum, range.maximum)) {
                    top.close()
                }
            }
        }
    }

    /**
     * @param {string} tag
     * @return {JQuery.Selector}
     * @private
     */
    _formatTagSelector(tag)
    {
        let selector
        tag = encodeURIComponent(tag.trim())

        if (tag.startsWith('artist%3A')) {
            selector = 'a[href="/artist/' + tag.replace('artist%3A', '') + '-all.html"]'
        } else if (tag.startsWith('series%3A')) {
            selector = 'a[href="/series/' + tag.replace('series%3A', '') + '-all.html"]'
        } else {
            selector = 'a[href="/tag/' + tag + '-all.html"]'
        }
        return selector
    }

    /**
     * @param {string[][]} ruleset
     * @param {string[]} tags
     * @return {string[][]}
     * @private
     */
    _growBlacklistRuleset(ruleset, tags)
    {
        let grownRuleset = []
        for (let tag of tags) {
            for (let rule of ruleset) {
                grownRuleset.push([...rule, this._formatTagSelector(tag)])
            }
        }
        return grownRuleset
    }

    /**
     * @param {JQuery} item
     * @private
     */
    _highlightBlacklistedTags(item)
    {
        let isBlacklisted
        for (let rule of this._configurationManager.getField(FILTER_TAG_BLACKLIST).optimized) {

            isBlacklisted = true
            for (let tagSelector of rule) {
                if (item.find(tagSelector).length === 0) {
                    isBlacklisted = false
                    break
                }
            }
            if (isBlacklisted) {
                for (let tagSelector of rule) {
                    item.find(tagSelector).addClass('blacklisted')
                }
            }
        }
    }

    /**
     * @param {JQuery} item
     * @private
     */
    _highlightFavouriteTags(item)
    {
        let favouriteTags = this._configurationManager.getField(UI_FAVOURITE_TAGS)
        if (favouriteTags.optimized.length) {
            for (let tagSelector of favouriteTags.optimized) {
                item.find(tagSelector).addClass('favourite')
            }
        }
    }

    /**
     * @return {Array}
     * @private
     */
    _optimizeBlacklistRules(blacklistedRules)
    {
        let orTags, iteratedRuleset
        let optimizedRuleset = []

        // Translate user defined rules

        for (let blacklistedRule of blacklistedRules) {

            iteratedRuleset = []
            for (let andTag of blacklistedRule.split('&')) {

                orTags = andTag.split('|')
                if (orTags.length === 1) {
                    this._updateBlacklistRuleset(iteratedRuleset, andTag)
                } else {
                    if (iteratedRuleset.length) {
                        iteratedRuleset = this._growBlacklistRuleset(iteratedRuleset, orTags)
                    } else {
                        this._updateBlacklistRuleset(iteratedRuleset, orTags)
                    }
                }
            }
            optimizedRuleset = optimizedRuleset.concat(iteratedRuleset)
        }

        // Sort rules by complexity

        return optimizedRuleset.sort((a, b) => a.length - b.length)
    }

    /**
     * @return {Array}
     * @private
     */
    _optimizeFavouriteRules(favouriteRules)
    {
        let rules = []
        for (let tag of favouriteRules) {
            rules.push(this._formatTagSelector(tag))
        }
        return rules
    }

    /**
     * @private
     */
    _setupComplianceFilters()
    {
        this._addItemComplianceFilter(FILTER_LANGUAGES, (item, valueKeys) => {
            let languageLink = item.find('tr:nth-child(3) > td:nth-child(2) a')
            if (languageLink.length) {

                languageLink = languageLink.attr('href')
                for (let key of valueKeys) {
                    if (languageLink.includes(key)) {
                        return true
                    }
                }
                return false
            }
            return valueKeys.includes('not-applicable')
        })
        this._addItemComplianceFilter(FILTER_GALLERY_TYPES, (item, valueKeys) => {
            for (let galleryClass of valueKeys) {
                if (item.hasClass(galleryClass)) {
                    return true
                }
            }
            return false
        })
        this._addItemComplexComplianceFilter(
            FILTER_TAG_BLACKLIST,
            (rules) => this._getConfig(ENABLE_BLACKLIST) && rules.length,
            (item, blacklistRuleset) => {
                let isBlacklisted
                for (let rule of blacklistRuleset) {

                    isBlacklisted = true
                    for (let tagSelector of rule) {
                        if (item.find(tagSelector).length === 0) {
                            isBlacklisted = false
                            break
                        }
                    }
                    if (isBlacklisted) {
                        return false
                    }
                }
                return true
            },
        )
    }

    /**
     * @private
     */
    _setupUI()
    {
        this._onBeforeUIBuild = () => {
            if (IS_GALLERY_PAGE) {
                this._checkPageLimits()
                this._performFlaggedOperation(OPTION_REMOVE_RELATED_GALLERIES, () => $('.gallery-content').remove())

                let tagsSection = $('.tags')
                this._highlightFavouriteTags(tagsSection)
                this._performComplexFlaggedOperation(
                    HIGHLIGHT_BLACKLISTED_TAGS, () => !this._getConfig(ENABLE_BLACKLIST), () => this._highlightBlacklistedTags(tagsSection))
            }
        }

        this._onUIBuild = () =>
            this._uiGen.createSettingsSection().append([
                this._uiGen.createTabsSection(['Filters', 'Highlights', 'Languages', 'Global', 'Stats'], [
                    this._uiGen.createTabPanel('Filters', true).append([
                        this._configurationManager.createElement(FILTER_GALLERY_TYPES),
                        this._uiGen.createSeparator(),
                        this._configurationManager.createElement(FILTER_PAGES),
                        this._uiGen.createSeparator(),
                        this._configurationManager.createElement(ENABLE_BLACKLIST),
                        this._configurationManager.createElement(FILTER_TAG_BLACKLIST),
                        this._uiGen.createSeparator(),
                        this._configurationManager.createElement(OPTION_DISABLE_COMPLIANCE_VALIDATION),
                    ]),
                    this._uiGen.createTabPanel('Highlights').append([
                        this._configurationManager.createElement(HIGHLIGHT_BLACKLISTED_TAGS),
                        this._configurationManager.createElement(UI_FAVOURITE_TAGS),
                    ]),
                    this._uiGen.createTabPanel('Languages').append([
                        this._configurationManager.createElement(FILTER_LANGUAGES),
                    ]),
                    this._uiGen.createTabPanel('Global').append([
                        this._configurationManager.createElement(UI_SHOW_ALL_TAGS),
                        this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
                        this._uiGen.createSeparator(),
                        this._createSettingsBackupRestoreFormActions(),
                    ]),
                    this._uiGen.createTabPanel('Stats').append([
                        this._uiGen.createStatisticsFormGroup(FILTER_GALLERY_TYPES),
                        this._uiGen.createStatisticsFormGroup(FILTER_LANGUAGES),
                        this._uiGen.createStatisticsFormGroup(FILTER_TAG_BLACKLIST),
                        this._uiGen.createSeparator(),
                        this._uiGen.createStatisticsTotalsGroup(),
                    ]),
                ]),
                this._createSettingsFormActions(),
                this._uiGen.createSeparator(),
                this._uiGen.createStatusSection(),
            ])

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

    /**
     * @param {JQuery} item
     * @private
     */
    _showAllTags(item)
    {
        let tags = item.find('.relatedtags > ul > li')
        let lastTag = tags.last()
        if (lastTag.text() === '...') {
            lastTag.remove()
            tags.filter('.hidden-list-item').removeClass('hidden-list-item')
        }
    }

    /**
     * @param {string[][]} ruleset
     * @param {string|string[]} tagToAdd
     * @private
     */
    _updateBlacklistRuleset(ruleset, tagToAdd)
    {
        if (ruleset.length) {
            for (let rule of ruleset) {
                rule.push(this._formatTagSelector(tagToAdd))
            }
        } else {
            let tags = typeof tagToAdd === 'string' ? [tagToAdd] : tagToAdd
            for (let tag of tags) {
                ruleset.push([this._formatTagSelector(tag)])
            }
        }
    }
}

(new HitomiSearchAndUITweaks).init()