Base Resource

Base library for my scripts

11.04.2020 itibariyledir. En son verisyonu görün.

Bu script direkt olarak kurulamaz. Başka scriptler için bir kütüphanedir ve meta yönergeleri içerir // @require https://update.greasyfork.org/scripts/375557/790976/Base%20Resource.js

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name          Base Resource
// @namespace     brazenvoid
// @version       1.8.0
// @author        brazenvoid
// @license       GPL-3.0-only
// @description   Base library for my scripts
// @grant         GM_addStyle
// @run-at  	  document-end
// ==/UserScript==

GM_addStyle(
    'button.form-button{padding:0 5px;width:100%}button.show-settings{background-color:#ffa31a;border:0;margin:2px 5px;padding:2px 5px;width:100%}button.show-settings.fixed{color:#000;font-size:14px;left:0;margin:0;padding:15px 0;position:fixed;top:250px;width:30px;writing-mode:sideways-lr;z-index:999}div.form-actions{text-align:center}div.form-actions button.form-button{padding:0 15px;width:auto}div.form-actions-wrapper{display:inline-flex}div.form-actions-wrapper > div.form-group + *{margin-left:15px}div.form-group{min-height:15px;padding:5px 0}div.form-group.form-range-input-group > input{padding:0 5px;width:70px}div.form-group.form-range-input-group > input + input{margin-right:5px}div.form-section{text-align:center}div.form-section button + button{margin-left:5px}div.form-section label.title{display:block;height:20px;width:100%}div.form-section button.form-button{width:auto}hr{margin:3px}input.form-input{height:18px;text-align:center}input.form-input.check-radio-input{float:left;margin-right:5px}input.form-input.regular-input{float:right;width:100px}label.form-label{padding:2px 0}label.form-label.regular-input{float:left}label.form-label.check-radio-input{float:left}label.form-stat-label{float:right;padding:2px 0}section.form-section{color:#000;font-size:12px;font-weight:700;position:fixed;left:0;padding:5px 10px;z-index:1000}select.form-dropdown{float:right;height:18px;text-align:center;width:100px}')

/**
 * @param milliseconds
 * @return {Promise<*>}
 */
const sleep = (milliseconds) => {
    return new Promise(resolve => setTimeout(resolve, milliseconds))
}

/**
 * @param {string} classes
 * @return {RegExp}
 * @private
 */
function _generateClassesIdentificationRegex (classes)
{
    return new RegExp('(\\s|^)' + classes + '(\\s|$)')
}

/**
 * @param {Element} node
 * @param {string} classes
 * @return {Element}
 */
function addClasses (node, classes)
{
    if (!hasClasses(node, classes)) {
        if (node.className !== '') {
            node.className += ' '
        }
        node.className += classes
    }
    return node
}

/**
 * @param {Element} node
 * @param {string} classes
 * @return {boolean}
 */
function hasClasses (node, classes)
{
    return !!node.className.match(_generateClassesIdentificationRegex(classes))
}

/**
 * @param {Element} node
 * @param {string} classes
 * @return {Element}
 */
function removeClasses (node, classes)
{
    if (hasClasses(node, classes)) {
        node.className = node.className.replace(_generateClassesIdentificationRegex(classes), ' ')
    }
    return node
}

/**
 * @param {string} text
 * @return {string}
 */
function toKebabCase (text)
{
    return text.toLowerCase().replace(' ', '-')
}

class ChildObserver
{
    /**
     * @callback observerMutationHandler
     * @param {Node} target
     */

    /**
     * @param {Element|Element[]} nodes
     * @param {observerMutationHandler} handler
     * @param {boolean} doInitialRun
     * @return {ChildObserver}
     */
    static observe (nodes, handler, doInitialRun = false)
    {
        let instance = new ChildObserver(handler)
        instance.observeNodes(nodes, doInitialRun)
        return instance
    }

    /**
     * @param {observerMutationHandler} handler
     */
    constructor (handler)
    {
        /**
         * @type {{subtree: boolean, attributes: boolean, childList: boolean}}
         * @private
         */
        this._config = {
            attributes: false,
            childList: true,
            subtree: false,
        }

        /**
         * @type {observerMutationHandler}
         * @private
         */
        this._handler = handler

        /**
         * @type {MutationObserver}
         * @private
         */
        this._observer = new MutationObserver(function (mutations) {
            for (let mutation of mutations) {
                handler(mutation.target)
            }
        })
    }

    /**
     * @param {Element|Element[]} nodes
     * @param {boolean} doInitialRun
     */
    observeNodes (nodes, doInitialRun = false)
    {
        nodes = (Array.isArray(nodes) || nodes instanceof NodeList) ? nodes : [nodes]
        for (let node of nodes) {
            if (doInitialRun) {
                this._handler(node)
            }
            this._observer.observe(node, this._config)
        }
    }
}

class LocalStore
{
    /**
     * @callback storeEventHandler
     * @param {Object} store
     */

    /**
     * @param {string} scriptPrefix
     * @param {Object} defaults
     * @return {LocalStore}
     */
    static createGlobalConfigStore (scriptPrefix, defaults)
    {
        return new LocalStore(scriptPrefix + 'globals', defaults)
    }

    static createPresetConfigStore (scriptPrefix, defaults)
    {
        return new LocalStore(scriptPrefix + 'presets', [
            {
                name: 'default',
                config: defaults,
            },
        ])
    }

    /**
     * @param {string} key
     * @param {Object} defaults
     */
    constructor (key, defaults)
    {
        /**
         * @type {string}
         * @private
         */
        this._key = key

        /**
         * @type {Object}
         * @private
         */
        this._store = {}

        /**
         * @type {string}
         * @private
         */
        this._defaults = this._toJSON(defaults)

        /**
         * @type {storeEventHandler}
         */
        this._onChange = null
    }

    /**
     * @param {string} json
     * @return {Object}
     * @private
     */
    _fromJSON (json)
    {
        /** @type {{arrays: Object, objects: Object, properties: Object}} */
        let parsedJSON = JSON.parse(json)
        let arrayObject = {}
        let store = {}

        for (let property in parsedJSON.arrays) {
            arrayObject = JSON.parse(parsedJSON.arrays[property])
            store[property] = []

            for (let key in arrayObject) {
                store[property].push(arrayObject[key])
            }
        }
        for (let property in parsedJSON.objects) {
            store[property] = this._fromJSON(parsedJSON.objects[property])
        }
        for (let property in parsedJSON.properties) {
            store[property] = parsedJSON.properties[property]
        }
        return store
    }

    /**
     * @return {string}
     * @private
     */
    _getStore ()
    {
        return window.localStorage.getItem(this._key)
    }

    /**
     * @return {Object}
     * @private
     */
    _getDefaults ()
    {
        return this._fromJSON(this._defaults)
    }

    /**
     * @param {Object} store
     * @return {string}
     * @private
     */
    _toJSON (store)
    {
        let arrayToObject = {}
        let json = {arrays: {}, objects: {}, properties: {}}

        for (let property in store) {
            if (typeof store[property] === 'object') {
                if (Array.isArray(store[property])) {
                    for (let key in store[property]) {
                        arrayToObject[key] = store[property][key]
                    }
                    json.arrays[property] = JSON.stringify(arrayToObject)
                } else {
                    json.objects[property] = this._toJSON(store[property])
                }
            } else {
                json.properties[property] = store[property]
            }
        }
        return JSON.stringify(json)
    }

    _handleOnChange ()
    {
        if (this._onChange !== null) {
            this._onChange(this._store)
        }
    }

    /**
     * @return {LocalStore}
     */
    delete ()
    {
        window.localStorage.removeItem(this._key)
        return this
    }

    /**
     * @return {Object}
     */
    get ()
    {
        return this._store
    }

    /**
     * @return {boolean}
     */
    isPurged ()
    {
        return this._getStore() === null
    }

    /**
     * @param {storeEventHandler} handler
     * @return {LocalStore}
     */
    onChange (handler)
    {
        this._onChange = handler
        return this
    }

    /**
     * @return {LocalStore}
     */
    restoreDefaults ()
    {
        this._store = this._getDefaults()
        this._handleOnChange()
        return this
    }

    /**
     * @return {LocalStore}
     */
    retrieve ()
    {
        let storedStore = this._getStore()
        if (storedStore === null) {
            this.restoreDefaults()
        } else {
            this._store = this._fromJSON(storedStore)
        }
        this._handleOnChange()
        return this
    }

    /**
     * @return {LocalStore}
     */
    save ()
    {
        window.localStorage.setItem(this._key, this._toJSON(this._store))
        this._handleOnChange()
        return this
    }

    /**
     * @param {*} data
     * @return {LocalStore}
     */
    update (data)
    {
        this._store = data
        return this.save()
    }
}

class Logger
{
    /**
     * @param {boolean} enableDebugging
     */
    constructor (enableDebugging)
    {
        /**
         * @type {boolean}
         * @private
         */
        this._enableDebugging = enableDebugging
    }

    /**
     * @param {string} message
     * @private
     */
    _log (message)
    {
        if (this._enableDebugging) {
            console.log(message)
        }
    }

    /**
     * @param {string} task
     */
    logTaskCompletion (task)
    {
        this._log('Completed: ' + task)
        this.logSeparator()
    }

    logSeparator ()
    {
        this._log('------------------------------------------------------------------------------------')
    }

    /**
     * @param {string}  filterName
     * @param {boolean} validationResult
     */
    logValidation (filterName, validationResult)
    {
        this._log('Satisfies ' + filterName + ' Filter: ' + (validationResult ? 'true' : 'false'))
    }

    /**
     * @param {string} videoName
     */
    logVideoCheck (videoName)
    {
        this._log('Checking Video: ' + videoName)
    }
}

class SelectorGenerator
{
    /**
     * @param {string} selectorPrefix
     */
    constructor (selectorPrefix)
    {
        /**
         * @type {string}
         * @private
         */
        this._prefix = selectorPrefix
    }

    /**
     * @param {string} selector
     * @return {string}
     */
    getSelector (selector)
    {
        return this._prefix + selector
    }

    /**
     * @param {string} settingName
     * @return {string}
     */
    getSettingsInputSelector (settingName)
    {
        return this.getSelector(toKebabCase(settingName) + '-setting')
    }

    /**
     * @param {string} settingName
     * @param {boolean} getMinInputSelector
     * @return {string}
     */
    getSettingsRangeInputSelector (settingName, getMinInputSelector)
    {
        return this.getSelector(toKebabCase(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting')
    }

    /**
     * @param {string} statisticType
     * @return {string}
     */
    getStatLabelSelector (statisticType)
    {
        return this.getSelector(toKebabCase(statisticType) + '-stat')
    }
}

class StatisticsRecorder
{
    /**
     * @param {Logger} logger
     * @param {SelectorGenerator} selectorGenerator
     */
    constructor (logger, selectorGenerator)
    {
        /**
         * @type {Logger}
         * @private
         */
        this._logger = logger

        /**
         * @type {SelectorGenerator}
         * @private
         */
        this._selectorGenerator = selectorGenerator

        /**
         * @type {{Total: number}}
         * @private
         */
        this._statistics = {Total: 0}
    }

    /**
     * @param {string} statisticType
     * @param {boolean} validationResult
     * @param {number} value
     * @param {boolean} log
     */
    record (statisticType, validationResult, value = 1, log = true)
    {
        if (!validationResult) {
            if (typeof this._statistics[statisticType] !== 'undefined') {
                this._statistics[statisticType] += value
            } else {
                this._statistics[statisticType] = value
            }
            this._statistics.Total += value
        }
        if (log) {
            this._logger.logValidation(statisticType, validationResult)
        }
    }

    reset ()
    {
        for (const statisticType in this._statistics) {
            this._statistics[statisticType] = 0
        }
    }

    updateUI ()
    {
        let label, labelSelector

        for (const statisticType in this._statistics) {
            labelSelector = this._selectorGenerator.getStatLabelSelector(statisticType)
            label = document.getElementById(labelSelector)
            if (label !== null) {
                label.textContent = this._statistics[statisticType]
            }
        }
    }
}

class UIGenerator
{
    /**
     * @param {Element|Node} node
     */
    static appendToBody (node)
    {
        document.getElementsByTagName('body')[0].appendChild(node)
    }

    /**
     * @param {Element} node
     * @param {Element[]} children
     * @return {Element}
     */
    static populateChildren (node, children)
    {
        for (let child of children) {
            node.appendChild(child)
        }
        return node
    }

    /**
     * @param {boolean} showUI
     * @param {SelectorGenerator} selectorGenerator
     */
    constructor (showUI, selectorGenerator)
    {
        /**
         * @type {*}
         * @private
         */
        this._buttonBackroundColor = null

        /**
         * @type {HTMLElement}
         * @private
         */
        this._section = null

        /**
         * @type {SelectorGenerator}
         * @private
         */
        this._selectorGenerator = selectorGenerator

        /**
         * @type {boolean}
         * @private
         */
        this._showUI = showUI

        /**
         * @type {HTMLLabelElement}
         * @private
         */
        this._statusLine = null

        /**
         * @type {string}
         * @private
         */
        this._statusText = ''
    }

    /**
     * @param {Element} node
     * @param {string} text
     * @return {this}
     * @private
     */
    _addHelpTextOnHover (node, text)
    {
        let instance = this
        node.addEventListener('mouseover', function () {
            instance.updateStatus(text, true)
        })
        node.addEventListener('mouseout', function () {
            instance.resetStatus()
        })
    }

    /**
     * @param {Element[]} children
     * @return {Element}
     */
    addSectionChildren (children)
    {
        return UIGenerator.populateChildren(this._section, children)
    }

    /**
     * @param {Element[]} children
     * @return {HTMLDivElement}
     */
    createFormActions (children)
    {
        let wrapperDiv = document.createElement('div')
        wrapperDiv.classList.add('form-actions-wrapper')

        UIGenerator.populateChildren(wrapperDiv, children)

        let formActionsDiv = document.createElement('div')
        formActionsDiv.classList.add('form-actions')
        formActionsDiv.appendChild(wrapperDiv)

        return formActionsDiv
    }

    /**
     * @param {string} caption
     * @param {EventListenerOrEventListenerObject} onClick
     * @param {string} hoverHelp
     * @return {HTMLButtonElement}
     */
    createFormButton (caption, onClick, hoverHelp = '')
    {
        let button = document.createElement('button')
        button.classList.add('form-button')
        button.textContent = caption
        button.addEventListener('click', onClick)

        if (hoverHelp !== '') {
            this._addHelpTextOnHover(button, hoverHelp)
        }
        if (this._buttonBackroundColor !== null) {
            button.style.backgroundColor = this._buttonBackroundColor
        }
        return button
    }

    /**
     * @param {Element[]} children
     * @return {Element}
     */
    createFormGroup (children)
    {
        let divFormGroup = document.createElement('div')
        divFormGroup.classList.add('form-group')

        return UIGenerator.populateChildren(divFormGroup, children)
    }

    /**
     * @param {string} id
     * @param {Array} keyValuePairs
     * @param {*} defaultValue
     * @return {HTMLSelectElement}
     */
    createFormGroupDropdown (id, keyValuePairs, defaultValue = null)
    {
        let dropdown = document.createElement('select'), item
        dropdown.id = id
        dropdown.classList.add('form-dropdown')

        for (let [key, value] of keyValuePairs) {
            item = document.createElement('option')
            item.textContent = value
            item.value = key
            dropdown.appendChild(item)
        }
        dropdown.value = defaultValue === null ? keyValuePairs[0][0] : defaultValue

        return dropdown
    }

    /**
     * @param {string} id
     * @param {string} type
     * @param {*} defaultValue
     * @return {HTMLInputElement}
     */
    createFormGroupInput (id, type, defaultValue = null)
    {
        let inputFormGroup = document.createElement('input')
        inputFormGroup.id = id
        inputFormGroup.classList.add('form-input')
        inputFormGroup.type = type

        switch (type) {
            case 'number':
            case 'text':
                inputFormGroup.classList.add('regular-input')

                if (defaultValue !== null) {
                    inputFormGroup.value = defaultValue
                }
                break
            case 'radio':
            case 'checkbox':
                inputFormGroup.classList.add('check-radio-input')

                if (defaultValue !== null) {
                    inputFormGroup.checked = defaultValue
                }
                break
        }
        return inputFormGroup
    }

    /**
     * @param {string} label
     * @param {string} inputID
     * @param {string} inputType
     * @return {HTMLLabelElement}
     */
    createFormGroupLabel (label, inputID = '', inputType = '')
    {
        let labelFormGroup = document.createElement('label')
        labelFormGroup.classList.add('form-label')
        labelFormGroup.textContent = label

        if (inputID !== '') {
            labelFormGroup.setAttribute('for', inputID)
        }
        if (inputType !== '') {
            switch (inputType) {
                case 'number':
                case 'text':
                    labelFormGroup.classList.add('regular-input')
                    labelFormGroup.textContent += ': '
                    break
                case 'radio':
                case 'checkbox':
                    labelFormGroup.classList.add('check-radio-input')
                    break
            }
        }
        return labelFormGroup
    }

    /**
     * @param {string} statisticType
     * @return {HTMLLabelElement}
     */
    createFormGroupStatLabel (statisticType)
    {
        let labelFormGroup = document.createElement('label')
        labelFormGroup.id = this._selectorGenerator.getStatLabelSelector(statisticType)
        labelFormGroup.classList.add('form-stat-label')
        labelFormGroup.textContent = '0'

        return labelFormGroup
    }

    /**
     * @param {string} label
     * @param {string} inputType
     * @param {*} defaultValue
     * @param {string|null} hoverHelp
     * @return {Element}
     */
    createFormInputGroup (label, inputType = 'text', defaultValue = null, hoverHelp = null)
    {
        let divFormInputGroup
        let inputID = this._selectorGenerator.getSettingsInputSelector(label)
        let labelFormGroup = this.createFormGroupLabel(label, inputID, inputType)
        let inputFormGroup = this.createFormGroupInput(inputID, inputType, defaultValue)

        switch (inputType) {
            case 'number':
            case 'text':
                divFormInputGroup = this.createFormGroup([labelFormGroup, inputFormGroup])
                break
            case 'radio':
            case 'checkbox':
                divFormInputGroup = this.createFormGroup([inputFormGroup, labelFormGroup])
                break
        }
        if (hoverHelp !== null) {
            this._addHelpTextOnHover(divFormInputGroup, hoverHelp)
        }
        return divFormInputGroup
    }

    /**
     * @param {string} label
     * @param {string} inputsType
     * @param {*} defaultValues
     * @return {Element}
     */
    createFormRangeInputGroup (label, inputsType = 'text', defaultValues = null)
    {
        let divFormInputGroup = this.createFormGroup([
            this.createFormGroupLabel(label, '', inputsType),
            this.createFormGroupInput(
                this._selectorGenerator.getSettingsRangeInputSelector(label, false),
                inputsType,
                defaultValues === null ? null : defaultValues[1],
            ),
            this.createFormGroupInput(
                this._selectorGenerator.getSettingsRangeInputSelector(label, true),
                inputsType,
                defaultValues === null ? null : defaultValues[0],
            ),
        ])
        divFormInputGroup.classList.add('form-range-input-group')

        return divFormInputGroup
    }

    /**
     * @param {string} title
     * @param {Element[]} children
     * @return {Element|HTMLDivElement}
     */
    createFormSection (title, children)
    {
        let sectionDiv = document.createElement('div')
        sectionDiv.classList.add('form-section')

        if (title !== '') {
            let sectionTitle = document.createElement('label')
            sectionTitle.textContent = title
            sectionTitle.classList.add('title')
            UIGenerator.populateChildren(sectionDiv, [sectionTitle])
        }
        return UIGenerator.populateChildren(sectionDiv, children)
    }

    /**
     * @param {string} caption
     * @param {string} tooltip
     * @param {EventListenerOrEventListenerObject} onClick
     * @param {string} hoverHelp
     * @return {HTMLButtonElement}
     */
    createFormSectionButton (caption, tooltip, onClick, hoverHelp = '')
    {
        let button = this.createFormButton(caption, onClick, hoverHelp)
        button.title = tooltip

        return button
    }

    /**
     * @param {string} label
     * @param {*} defaultValue
     * @return {Element}
     */
    createFormTextAreaGroup (label, defaultValue = null)
    {
        let labelElement = this.createFormGroupLabel(label)
        labelElement.style.textAlign = 'center'

        let textAreaElement = document.createElement('textarea')
        textAreaElement.id = this._selectorGenerator.getSettingsInputSelector(label)
        textAreaElement.classList.add('form-input')
        textAreaElement.value = defaultValue

        return this.createFormGroup([labelElement, textAreaElement])
    }

    /**
     * @param {string} IDSuffix
     * @param {*} backgroundColor
     * @param {*} top
     * @param {*} width
     * @return {this}
     */
    createSection (IDSuffix, backgroundColor, top, width)
    {
        this._section = document.createElement('section')
        this._section.id = this._selectorGenerator.getSelector(IDSuffix)
        this._section.classList.add('form-section')
        this._section.style.display = this._showUI ? 'block' : 'none'
        this._section.style.top = top
        this._section.style.width = width
        this._section.style.backgroundColor = backgroundColor

        return this
    }

    /**
     * @return {HTMLHRElement}
     */
    createSeparator ()
    {
        return document.createElement('hr')
    }

    /**
     * @param {LocalStore} localStore
     * @param {EventListenerOrEventListenerObject} onClick
     * @param {boolean} addTopPadding
     * @return {HTMLDivElement}
     */
    createSettingsFormActions (localStore, onClick, addTopPadding = false)
    {
        let divFormActions = this.createFormSection('', [
            this.createFormActions([
                this.createFormButton('Apply', onClick,
                    'Filter the videos as per the settings in the dialog.'),
                this.createFormButton('Reset', function () {
                    localStore.retrieve()
                    onClick()
                }, 'Restore and apply saved configuration.'),
            ]),
        ])
        if (addTopPadding) {
            divFormActions.style.paddingTop = '10px'
        }
        return divFormActions
    }

    /**
     * @param {string} label
     * @param {Array} keyValuePairs
     * @param {*} defaultValue
     * @return {Element}
     */
    createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null)
    {
        let dropdownID = this._selectorGenerator.getSettingsInputSelector(label)

        return this.createFormGroup([
            this.createFormGroupLabel(label, dropdownID, 'text'),
            this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue),
        ])
    }

    /**
     * @return {HTMLButtonElement}
     */
    createSettingsHideButton ()
    {
        let section = this._section
        return this.createFormButton('<< Hide', function () {
            section.style.display = 'none'
        })
    }

    /**
     * @param {string} caption
     * @param {Element} settingsSection
     * @param {boolean} fixed
     * @return {HTMLButtonElement}
     */
    createSettingsShowButton (caption, settingsSection, fixed = true)
    {
        let controlButton = document.createElement('button')
        controlButton.textContent = caption
        controlButton.classList.add('show-settings')

        if (fixed) {
            controlButton.classList.add('fixed')
        }
        controlButton.addEventListener('click', function () {
            let settingsUI = document.getElementById(settingsSection.id)
            settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none'
        })
        return controlButton
    }

    /**
     * @param {string} statisticsType
     * @param {string} label
     * @return {Element}
     */
    createStatisticsFormGroup (statisticsType, label = '')
    {
        if (label === '') {
            label = statisticsType
        }
        return this.createFormGroup([
            this.createFormGroupLabel('Filtered ' + label + ' Videos'),
            this.createFormGroupStatLabel(statisticsType),
        ])
    }

    /**
     * @return {Element|HTMLDivElement}
     */
    createStatusSection ()
    {
        this._statusLine = UI.createFormGroupLabel('Status')
        this._statusLine.id = this._selectorGenerator.getSelector('status')

        return UI.createFormSection('', [this._statusLine])
    }

    /**
     * @param {LocalStore} localStore
     * @return {Element}
     */
    createStoreFormSection (localStore)
    {
        return this.createFormSection('Saved Configuration', [
            this.createFormActions([
                this.createFormSectionButton('Update', 'Save UI settings in store', function () {
                    localStore.save()
                }, 'Saves applied settings.'),
                this.createFormSectionButton('Purge', 'Purge store', function () {
                    localStore.delete()
                }, 'Removes saved settings. Settings will then be sourced from the defaults defined in the script.'),
            ]),
        ])
    }

    createTextualSettingsButtonAndSection ()
    {
        let UI = new UIGenerator(this._showUI, this._selectorGenerator)
        let section = UI.createSection(
            'textual-settings',
            this._section.style.backgroundColor,
            this._section.style.top,
            this._section.style.width,
        ).addSectionChildren([
            UI.createFormTextAreaGroup('Blacklisted Words'),
            UI.createSeparator(),
            UI.createFormButton('Update', function () {

            }, 'Videos having name containing any blacklisted words will be filtered out.')
        ])
        UI.constructor.appendToBody(section)
        return this.createFormButton('More Settings >>', function () {
            section.style.display = section.style.display === 'none' ? 'block' : 'none'
        })
    }

    /**
     * @param {string} label
     * @return {HTMLElement}
     */
    getSettingsInput (label)
    {
        return document.getElementById(this._selectorGenerator.getSettingsInputSelector(label))
    }

    /**
     * @param {string} label
     * @return {boolean}
     */
    getSettingsInputCheckedStatus (label)
    {
        return this.getSettingsInput(label).checked
    }

    /**
     * @param {string} label
     * @return {*}
     */
    getSettingsInputValue (label)
    {
        return this.getSettingsInput(label).value
    }

    /**
     * @param {string} label
     * @param {boolean} getMinInput
     * @return {HTMLElement}
     */
    getSettingsRangeInput (label, getMinInput)
    {
        return document.getElementById(this._selectorGenerator.getSettingsRangeInputSelector(label, getMinInput))
    }

    /**
     * @param {string} label
     * @param {boolean} getMinInputValue
     * @return {*}
     */
    getSettingsRangeInputValue (label, getMinInputValue)
    {
        return this.getSettingsRangeInput(label, getMinInputValue).value
    }

    resetStatus ()
    {
        this._statusLine.textContent = this._statusText
    }

    /**
     * @param {string} label
     * @param {boolean} bool
     */
    setSettingsInputCheckedStatus (label, bool)
    {
        this.getSettingsInput(label).checked = bool
    }

    /**
     * @param {string} label
     * @param {*} value
     */
    setSettingsInputValue (label, value)
    {
        this.getSettingsInput(label).value = value
    }

    /**
     * @param {string} label
     * @param {number} lowerBound
     * @param {number} upperBound
     */
    setSettingsRangeInputValue (label, lowerBound, upperBound)
    {
        this.getSettingsRangeInput(label, true).value = lowerBound
        this.getSettingsRangeInput(label, false).value = upperBound
    }

    /**
     * @param {string} status
     * @param {boolean} transient
     */
    updateStatus (status, transient = false)
    {
        if (!transient) {
            this._statusText = status
        }
        this._statusLine.textContent = status
    }
}

class Validator
{
    static iFramesRemover ()
    {
        GM_addStyle(' iframe { display: none !important; } ')
    }

    /**
     * @param {StatisticsRecorder} statisticsRecorder
     */
    constructor (statisticsRecorder)
    {
        /**
         * @type {Array}
         * @private
         */
        this._blacklist = []

        /**
         * @type {Array}
         * @private
         */
        this._filters = []

        /**
         * @type {RegExp}
         * @private
         */
        this._optimizedBlacklist = null

        /**
         * @type {Object}
         * @private
         */
        this._optimizedSanitizationRules = {}

        /**
         * @type {Object}
         * @private
         */
        this._sanitizationRules = []

        /**
         * @type {StatisticsRecorder}
         * @private
         */
        this._statisticsRecorder = statisticsRecorder
    }

    _buildWholeWordMatchingRegex (words)
    {
        let patternedWords = []
        for (let i = 0; i < words.length; i++) {
            patternedWords.push('\\b' + words[i] + '\\b')
        }
        return new RegExp('(' + patternedWords.join('|') + ')', 'gi')
    }

    /**
     * @param {string[]} blacklistedWords
     * @return {Validator}
     */
    addBlacklistFilter (blacklistedWords)
    {
        this._blacklist = blacklistedWords
        return this
    }

    /**
     * @param {Object} sanitizationRules
     * @return {Validator}
     */
    addSanitizationFilter (sanitizationRules)
    {
        this._sanitizationRules = sanitizationRules
        return this
    }

    /**
     * @return {Validator}
     */
    optimize ()
    {
        this._optimizedBlacklist = this._buildWholeWordMatchingRegex(this._blacklist)

        for (const substitute in this._sanitizationRules) {
            this._optimizedSanitizationRules[substitute] =
                this._buildWholeWordMatchingRegex(this._sanitizationRules[substitute])
        }
        return this
    }

    /**
     * @param {string} text
     * @return {string}
     */
    sanitize (text)
    {
        for (const substitute in this._optimizedSanitizationRules) {
            text = text.replace(this._optimizedSanitizationRules[substitute], substitute)
        }
        return text.trim()
    }

    /**
     * @param {Element} videoNameNode
     * @return {Validator}
     */
    sanitizeVideoItem (videoNameNode)
    {
        videoNameNode.textContent = this.sanitize(videoNameNode.textContent)
        return this
    }

    /**
     * @param {string} videoNameNodeSelector
     * @return {Validator}
     */
    sanitizeVideoPage (videoNameNodeSelector)
    {
        let videoNameNode = document.querySelector(videoNameNodeSelector)
        if (videoNameNode !== null) {

            let sanitizedVideoName = this.sanitize(videoNameNode.textContent)
            videoNameNode.textContent = sanitizedVideoName
            document.title = sanitizedVideoName
        }
        return this
    }

    /**
     * @param {string} text
     * @return {boolean}
     */
    validateBlackList (text)
    {
        let validationCheck = true

        if (this._optimizedBlacklist !== null) {
            validationCheck = text.match(this._optimizedBlacklist) === null
            this._statisticsRecorder.record('Blacklist', validationCheck)
        }
        return validationCheck
    }

    /**
     * @param {string} name
     * @param {number} value
     * @param {number[]} bounds
     * @return {boolean}
     */
    validateRange (name, value, bounds)
    {
        let validationCheck = true

        if (bounds[0] > 0 && bounds[1] > 0) {
            validationCheck = value >= bounds[0] && value <= bounds[1]
        } else {
            if (bounds[0] > 0) {
                validationCheck = value >= bounds[0]
            }
            if (bounds[1] > 0) {
                validationCheck = value <= bounds[1]
            }
        }
        this._statisticsRecorder.record(name, validationCheck)

        return validationCheck
    }

    /**
     * @param {string} name
     * @param {number} lowerBound
     * @param {number} upperBound
     * @param getValueCallback
     * @return {boolean}
     */
    validateRangeFilter (name, lowerBound, upperBound, getValueCallback)
    {
        if (lowerBound > 0 || upperBound > 0) {
            return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
        }
        return true
    }
}

class TubeSiteSearchAndTweaks
{
    /**
     * @param {string} scriptPrefix
     * @param {Object} defaultPreset
     * @param {Object} globalConfiguration
     */
    static create (scriptPrefix, defaultPreset, globalConfiguration)
    {
        return new TubeSiteSearchAndTweaks(scriptPrefix, defaultPreset, globalConfiguration)
    }

    /**
     * @param {string} scriptPrefix
     * @param {Object} defaultPreset
     * @param {Object} globalConfiguration
     */
    constructor (scriptPrefix, defaultPreset, globalConfiguration)
    {
        /**
         * @type {Object}
         * @private
         */
        this._appliedPreset = null

        /**
         * @type {Object}
         * @private
         */
        this._defaultPreset = defaultPreset

        /**
         * {LocalStore}
         */
        this._globalConfigurationStore = LocalStore.createGlobalConfigStore(scriptPrefix, globalConfiguration)

        /**
         * {Object}
         */
        this._globalConfiguration = this._globalConfigurationStore.retrieve().get()

        /**
         * @type {LocalStore}
         * @private
         */
        this._presetsStore = LocalStore.createPresetConfigStore(scriptPrefix, defaultPreset)

        /**
         * @type {{name: string, config: Object}[]}
         * @private
         */
        this._presets = this._presetsStore.retrieve().get()

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

    /**
     * @param {string} name
     * @param {Object} config
     * @return {this}
     */
    createPreset (name, config)
    {
        this._presets.push({
            name: name,
            config: config,
        })
        this._presetsStore.update(this._presets)
        return this
    }

    /**
     * @param {string} name
     * @return {this}
     */
    deletePreset (name)
    {
        for (let i = 0; i < this._presets.length; i++) {
            if (this._presets[i].name === name) {
                this._presets.splice(i, 1)
                this._presetsStore.update(this._presets)
                break
            }
        }
        return this
    }

    /**
     * @param name
     * @return {{name: string, config: Object}|null}
     */
    findPreset (name)
    {
        for (let preset of this._presets) {
            if (preset.name === name) {
                return preset
            }
        }
        return null
    }

    /**
     * @return {{name: string, config: Object}}
     */
    getAppliedPreset ()
    {
        return this._appliedPreset
    }
}