Hitomi - Search & UI Tweaks

Various search filters and user experience enhancers

目前為 2022-11-08 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Hitomi - Search & UI Tweaks
// @namespace    brazenvoid
// @version      5.0.1
// @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-resource/code/Base%20Resource.js?version=899286
// @require      https://greasyfork.org/scripts/416104-brazen-ui-generator/code/Brazen%20UI%20Generator.js?version=899448
// @require      https://greasyfork.org/scripts/418665-brazen-configuration-manager/code/Brazen%20Configuration%20Manager.js?version=892799
// @require      https://greasyfork.org/scripts/429587-brazen-item-attributes-resolver/code/Brazen%20Item%20Attributes%20Resolver.js?version=951949
// @require      https://greasyfork.org/scripts/416105-brazen-base-search-enhancer/code/Brazen%20Base%20Search%20Enhancer.js?version=1114816
// @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:300px}
           #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%}
           .bg-brand{background-color:rgb(216, 210, 234)}`)

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

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

const OPTION_REMOVE_RELATED_GALLERIES = 'Remove Related Galleries'

class HitomiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
    constructor()
    {
        super({
            isUserLoggedIn:               false,
            itemListSelectors:            '.gallery-content',
            itemNameSelector:             'h1.lillie a',
            itemSelectors:                '.acg,.anime,.cg,.dj,.manga',
            itemPageLinkSelector:         '',
            itemPageDeepAnalysisSelector: '',
            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(OPTION_REMOVE_RELATED_GALLERIES, 'Remove related galleries section from gallery pages.')
            .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))
        
        this._setupUI()
        this._setupComplianceFilters()
    }
    
    /**
     * @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
    }
    
    /**
     * @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)
    }
    
    /**
     * @private
     */
    _removeRelatedGalleries()
    {
        if (IS_GALLERY_PAGE && this._configurationManager.getValue(OPTION_REMOVE_RELATED_GALLERIES)) {
            $('.gallery-content').remove()
        }
    }
    
    /**
     * @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._addItemComplianceFilter(FILTER_TAG_BLACKLIST, (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._removeRelatedGalleries()
            }
        }
        
        this._onUIBuild = () =>
            this._uiGen.createSettingsSection().append([
                this._uiGen.createTabsSection(['Filters', 'Languages', 'Global', 'Stats'], [
                    this._uiGen.createTabPanel('Filters', true).append([
                        this._configurationManager.createElement(FILTER_GALLERY_TYPES),
                        this._uiGen.createSeparator(),
                        this._configurationManager.createElement(FILTER_TAG_BLACKLIST),
                        this._uiGen.createSeparator(),
                        this._configurationManager.createElement(OPTION_DISABLE_COMPLIANCE_VALIDATION),
                    ]),
                    this._uiGen.createTabPanel('Languages').append([
                        this._configurationManager.createElement(FILTER_LANGUAGES),
                    ]),
                    this._uiGen.createTabPanel('Global').append([
                        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 {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()