Brazen Base Search Enhancer

Base class for search enhancement scripts

Ekde 2020/12/11. Vidu La ĝisdata versio.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.sleazyfork.org/scripts/416105/879317/Brazen%20Base%20Search%20Enhancer.js

// ==UserScript==
// @name         Brazen Base Search Enhancer
// @namespace    brazen
// @version      2.1.3
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Base class for search enhancement scripts
// ==/UserScript==

const CONFIG_PAGINATOR_LIMIT = 'Pagination Limit'
const CONFIG_PAGINATOR_THRESHOLD = 'Pagination Threshold'
const OPTION_ALWAYS_SHOW_SETTINGS_PANE = 'Always Show Settings Pane'
const OPTION_DISABLE_COMPLIANCE_VALIDATION = 'Disable Item Compliance Validation'

const FILTER_TEXT_BLACKLIST = 'Blacklist'
const FILTER_TEXT_SEARCH = 'Search'
const FILTER_TEXT_SANITIZATION = 'Text Sanitization Rules'
const FILTER_TEXT_WHITELIST = 'Whitelist'

class BrazenBaseSearchEnhancer
{
    /**
     * @return BrazenBaseSearchEnhancer
     */
    static initialize ()
    {
        BrazenBaseSearchEnhancer.throwOverrideError()
    }

    static throwOverrideError ()
    {
        throw new Error('Method must be overridden.')
    }

    /**
     * @param {string} scriptPrefix
     * @param {string|string[]} itemClasses
     * @param {boolean} enablePaginator
     */
    constructor (scriptPrefix, itemClasses, enablePaginator = false)
    {
        /**
         * Array of item compliance filters ordered in intended sequence of execution
         * @type {Function[]}
         * @protected
         */
        this._complianceFilters = []

        /**
         * @type {string[]}
         * @protected
         */
        this._itemClasses = Array.isArray(itemClasses) ? itemClasses : [itemClasses]

        /**
         * @type {string}
         * @protected
         */
        this._itemClassesSelector = '.' + this._itemClasses.join(',.')

        /**
         * Pagination manager
         * @type Paginator|null
         * @protected
         */
        this._paginator = null

        /**
         * @type {string}
         * @protected
         */
        this._scriptPrefix = scriptPrefix

        /**
         * @type {StatisticsRecorder}
         * @protected
         */
        this._statistics = new StatisticsRecorder(this._scriptPrefix)

        /**
         * @type {BrazenUIGenerator}
         * @protected
         */
        this._uiGen = new BrazenUIGenerator(this._scriptPrefix)

        /**
         * @type {Validator}
         * @protected
         */
        this._validator = (new Validator(this._statistics))

        /**
         * Local storage store with defaults
         * @type {ConfigurationManager}
         * @protected
         */
        this._configurationManager = ConfigurationManager.create(this._uiGen).
            addFlagField(OPTION_DISABLE_COMPLIANCE_VALIDATION, 'Disables all search filters.').
            addFlagField(OPTION_ALWAYS_SHOW_SETTINGS_PANE, 'Always show configuration interface.').
            addNumberField(CONFIG_PAGINATOR_LIMIT, 0, 50, 'Limit paginator to concatenate the specified number of maximum pages. Zero equates to infinite.').
            addNumberField(CONFIG_PAGINATOR_THRESHOLD, 10, 1000, 'Make paginator ensure the specified number of minimum results.').
            addRulesetField(FILTER_TEXT_BLACKLIST, '', null, null, (rules) => Utilities.buildWholeWordMatchingRegex(rules)).
            addRulesetField(FILTER_TEXT_SANITIZATION, '', (rules) => {
                let sanitizationRulesText = []
                for (let substitute in rules) {
                    sanitizationRulesText.push(substitute + '=' + rules[substitute].join(','))
                }
                return sanitizationRulesText

            }, (rules) => {
                let sanitizationRules = {}, fragments, validatedTargetWords
                for (let sanitizationRule of rules) {

                    if (sanitizationRule.includes('=')) {
                        fragments = sanitizationRule.split('=')
                        if (fragments[0] === '') {
                            fragments[0] = ' '
                        }

                        validatedTargetWords = Utilities.trimAndKeepNonEmptyStrings(fragments[1].split(','))
                        if (validatedTargetWords.length) {
                            sanitizationRules[fragments[0]] = validatedTargetWords
                        }
                    }
                }
                return sanitizationRules
            }, (rules) => {
                let optimizedRules = {}
                for (const substitute in rules) {
                    optimizedRules[substitute] = Utilities.buildWholeWordMatchingRegex(rules[substitute])
                }
                return optimizedRules
            }).
            addRulesetField(FILTER_TEXT_WHITELIST, '', null, null, (rules) => Utilities.buildWholeWordMatchingRegex(rules)).
            addTextField(FILTER_TEXT_SEARCH, 'Show videos with these comma separated words in their names.')

        // Events

        /**
         * Operations to perform after script initialization
         * @type {Function}
         * @protected
         */
        this._onAfterInitialization = null

        /**
         * Operations to perform after UI generation
         * @type {Function}
         * @protected
         */
        this._onAfterUIBuild = null

        /**
         * Operations to perform before compliance validation. This callback can also be used to skip compliance validation by returning false.
         * @type {null}
         * @protected
         */
        this._onBeforeCompliance = null

        /**
         * Operations to perform before UI generation
         * @type {Function}
         * @protected
         */
        this._onBeforeUIBuild = null

        /**
         * Operations to perform after compliance checks, the first time a item is retrieved
         * @type {Function}
         * @protected
         */
        this._onFirstHitAfterCompliance = null

        /**
         * Operations to perform before compliance checks, the first time a item is retrieved
         * @type {Function}
         * @protected
         */
        this._onFirstHitBeforeCompliance = null

        /**
         * Get item lists from the page
         * @type {Function}
         * @protected
         */
        this._onGetItemLists = null

        /**
         * Logic to hide a non-compliant item
         * @type {Function}
         * @protected
         */
        this._onItemHide = (item) => item.hide(100)

        /**
         * Logic to show compliant item
         * @type {Function}
         * @protected
         */
        this._onItemShow = (item) => item.show(100)

        /**
         * Must return the generated settings section node
         * @type {Function}
         * @protected
         */
        this._onUIBuild = null

        /**
         * Validate initiating initialization.
         * Can be used to stop script init on specific pages or vice versa
         * @type {Function}
         * @protected
         */
        this._onValidateInit = () => true
    }

    /**
     * Filters items as per settings
     * @param {JQuery} itemsList
     * @param {boolean} fromObserver
     * @protected
     */
    _complyItemsList (itemsList, fromObserver = false)
    {
        let items = fromObserver ? itemsList.filter(this._itemClassesSelector) : itemsList.find(this._itemClassesSelector)
        items.each((index, element) => {
            let item = $(element)
            if (typeof element['scriptProcessedOnce'] === 'undefined') {
                element.scriptProcessedOnce = false
                Utilities.callEventHandler(this._onFirstHitBeforeCompliance, [item])
            }

            this._validateItemCompliance(item)

            if (!element['scriptProcessedOnce']) {
                Utilities.callEventHandler(this._onFirstHitAfterCompliance, [item])
                element.scriptProcessedOnce = true
            }

            this._statistics.updateUI()
        })
    }

    /**
     * @protected
     */
    _createSettingsFormActions ()
    {
        return this._uiGen.createFormSection().append([
            this._uiGen.createFormActions([
                this._uiGen.createFormButton('Apply', () => this._onApplyNewSettings(), 'Apply settings.'),
                this._uiGen.createFormButton('Save', () => this._onSaveSettings(), 'Apply and update saved configuration.'),
                this._uiGen.createFormButton('Reset', () => this._onResetSettings(), 'Revert to saved configuration.'),
            ]),
        ])
    }

    /**
     * @param {JQuery} UISection
     * @private
     */
    _embedUI (UISection)
    {
        UISection.on('mouseleave', (event) => {
            if (!this._configurationManager.getValue(OPTION_ALWAYS_SHOW_SETTINGS_PANE)) {
                $(event.currentTarget).hide(300)
            }
        })
        if (this._configurationManager.getValue(OPTION_ALWAYS_SHOW_SETTINGS_PANE)) {
            UISection.show()
        }
        this._uiGen.constructor.appendToBody(UISection)
        this._uiGen.constructor.appendToBody(this._uiGen.createSettingsShowButton('', UISection))
    }

    _onApplyNewSettings ()
    {
        this._configurationManager.update()
        this._validateCompliance()
    }

    _onResetSettings ()
    {
        this._configurationManager.revertChanges()
        this._validateCompliance()
    }

    _onSaveSettings ()
    {
        this._onApplyNewSettings()
        this._configurationManager.save()
    }

    /**
     * @param {boolean} firstRun
     * @protected
     */
    _validateCompliance (firstRun = false)
    {
        let itemLists = Utilities.callEventHandler(this._onGetItemLists)
        if (!firstRun) {
            this._statistics.reset()
            itemLists.each((index, itemsList) => {
                this._complyItemsList($(itemsList))
            })
        } else {
            itemLists.each((index, itemList) => {
                ChildObserver.create().onNodesAdded((itemsAdded) => this._complyItemsList($(itemsAdded), true)).observe(itemList)
                this._complyItemsList($(itemList))
            })
        }
        if (this._paginator) {
            this._paginator.run(this._configurationManager.getValue(CONFIG_PAGINATOR_THRESHOLD), this._configurationManager.getValue(CONFIG_PAGINATOR_LIMIT))
        }
    }

    /**
     * @param {JQuery} item
     * @protected
     */
    _validateItemCompliance (item)
    {
        let itemComplies = true

        if (!this._configurationManager.getValue(OPTION_DISABLE_COMPLIANCE_VALIDATION) && Utilities.callEventHandler(this._onBeforeCompliance, [item]) !== false) {
            for (let complianceFilter of this._complianceFilters) {
                if (!complianceFilter(item)) {
                    itemComplies = false
                    break
                }
            }
        }
        itemComplies ? Utilities.callEventHandler(this._onItemShow, [item]) : Utilities.callEventHandler(this._onItemHide, [item])
    }

    /**
     * Initialize the script and do basic UI removals
     */
    init ()
    {
        if (Utilities.callEventHandler(this._onValidateInit)) {

            this._configurationManager.initialize(this._scriptPrefix)

            if (this._paginator) {
                this._paginator.initialize()
            }

            Utilities.callEventHandler(this._onBeforeUIBuild)
            this._embedUI(Utilities.callEventHandler(this._onUIBuild))
            Utilities.callEventHandler(this._onAfterUIBuild)

            this._configurationManager.updateInterface()

            this._validateCompliance(true)

            this._uiGen.updateStatus('Initial run completed.')

            Utilities.callEventHandler(this._onAfterInitialization)
        }
    }
}