Brazenvoid's Base Resource v1.6.2

Base library for my scripts

Per 03-09-2019. Zie de nieuwste versie.

Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/375557/730261/Brazenvoid%27s%20Base%20Resource%20v162.js

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name          Brazenvoid's Base Resource v1.6.2
// @namespace     brazenvoid
// @version       1.6.3
// @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: black; 
        font-size: 14px; 
        left: 0; 
        margin: 0; 
        padding: 15px 0px; 
        position: fixed; 
        top: 250px; 
        width: 30px; 
        writing-mode: sideways-lr; 
        z-index: 999; 
    }
    
    div.form-actions { 
        text-align: center; 
    }
    div.form-actions button + button { 
        margin-left: 10px; 
    }
    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: right; 
    }
    label.form-stat-label { 
        float: right; 
        padding: 2px 0; 
    }
    
    section.form-section { 
        color: black; 
        font-size: 12px; 
        font-weight: bold; 
        position: fixed; 
        left: 0; 
        padding: 5px 10px; 
        z-index: 1000; 
    }
    
    select.form-dropdown { 
        float: right; 
        height: 18px; 
        text-align: center; 
        width: 100px 
    }
`)

const sleep = (milliseconds) => {
    return new Promise(resolve => setTimeout(resolve, milliseconds))
}

function _generateClassesIdentificationRegex (classes)
{
    return new RegExp('(\\s|^)' + classes + '(\\s|$)')
}

function addClasses (node, classes)
{
    if (!hasClasses(node, classes)) {
        if (node.className !== '') {
            node.className += ' '
        }
        node.className += classes
    }
    return node
}

function hasClasses (node, classes)
{
    return !!node.className.match(_generateClassesIdentificationRegex(classes))
}

function removeClasses (node, classes)
{
    if (hasClasses(node, classes)) {
        node.className = node.className.replace(_generateClassesIdentificationRegex(classes), ' ')
    }
    return node
}

class CaseConverters
{
    static toCamel (text)
    {
        return text.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
            return index === 0 ? letter.toLowerCase() : letter.toUpperCase()
        }).replace(/\s+/g, '')
    }

    static toKebab (text)
    {
        return text.toLowerCase().replace(' ', '-')
    }

    static toKebabFromSnake (text)
    {
        return text.replace('_', '-')
    }

    static toNormalFromKebab (text)
    {
        return text.replace('-', ' ')
    }

    static toNormalFromSnake (text)
    {
        return text.replace('_', ' ')
    }

    static toSnake (text)
    {
        return text.toLowerCase().replace(' ', '_')
    }
}

class ChildObserver
{
    static observe (nodes, handler, doInitialRun = false)
    {
        let instance = new ChildObserver(handler)
        instance.observeNodes(nodes, doInitialRun)
        return instance
    }

    constructor (handler)
    {
        this.config = {
            attributes: false,
            childList: true,
            subtree: false,
        }
        this.handler = handler
        this.observer = new MutationObserver(function (mutations) {
            for (let mutation of mutations) {
                handler(mutation.target)
            }
        })
    }

    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
{
    constructor (key, defaultStore)
    {
        this.onDefaultsLoaded = null
        this.onRetrieval = null
        this.onUpdated = null

        this._key = key
        this._store = {}
        this._defaultStore = this._toJSON(defaultStore)
    }

    _fromJSON (json)
    {
        let arrayObject = {}
        let parsedJSON = JSON.parse(json)
        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
    }

    _getStore ()
    {
        return window.localStorage.getItem(this._key)
    }

    _getDefaults ()
    {
        return this._fromJSON(this._defaultStore)
    }

    _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)
    }

    delete ()
    {
        window.localStorage.removeItem(this._key)
        return this
    }

    get ()
    {
        return this._store
    }

    restoreDefaults ()
    {
        this._store = this._getDefaults()

        if (this.onDefaultsLoaded !== null) {
            this.onDefaultsLoaded(this._store)
        }
        return this
    }

    retrieve ()
    {
        let storedStore = this._getStore()
        if (storedStore === null) {
            this.restoreDefaults()
        } else {
            this._store = this._fromJSON(storedStore)
        }
        if (this.onRetrieval !== null) {
            this.onRetrieval(this._store)
        }
        return this
    }

    save ()
    {
        window.localStorage.setItem(this._key, this._toJSON(this._store))

        if (this.onUpdated !== null) {
            this.onUpdated(this._store)
        }
        return this
    }

    isPurged ()
    {
        return this._getStore() === null
    }
}

class Logger
{
    constructor (enableDebugging)
    {
        this.enableDebugging = enableDebugging
    }

    _log (message)
    {
        if (this.enableDebugging) {
            console.log(message)
        }
    }

    logTaskCompletion (task)
    {
        this._log('Completed: ' + task)
        this.logSeparator()
    }

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

    logValidation (filterName, validationResult = null)
    {
        this._log('Satisfies ' + filterName + ' Filter: ' + (validationResult ? 'true' : 'false'))
    }

    logVideoCheck (videoName)
    {
        this._log('Checking Video: ' + videoName)
    }
}

class SelectorGenerator
{
    constructor (selectorPrefix)
    {
        this.prefix = selectorPrefix
    }

    getSelector (selector)
    {
        return this.prefix + selector
    };

    getSettingsInputSelector (settingName)
    {
        return this.getSelector(CaseConverters.toKebab(settingName) + '-setting')
    }

    getSettingsRangeInputSelector (settingName, getMinInputSelector)
    {
        return this.getSelector(
            CaseConverters.toKebab(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting')
    }

    getStatLabelSelector (statisticType)
    {
        return this.getSelector(CaseConverters.toKebab(statisticType) + '-stat')
    };
}

class StatisticsRecorder
{
    constructor (logger, selectorGenerator)
    {
        this.logger = logger
        this.selectorGenerator = selectorGenerator
        this.statistics = {Total: 0}
    }

    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
{
    static appendToBody (node)
    {
        document.getElementsByTagName('body')[0].appendChild(node)
    }

    static populateChildren (node, children)
    {
        for (let child of children) {
            node.appendChild(child)
        }
        return node
    }

    constructor (showUI, selectorGenerator)
    {
        this.buttonBackroundColor = null
        this.selectorGenerator = selectorGenerator
        this.showUI = showUI
    }

    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
    }

    createFormButton (caption, onClick)
    {
        let button = document.createElement('button')
        button.classList.add('form-button')
        button.textContent = caption
        button.addEventListener('click', onClick)

        if (this.buttonBackroundColor !== null) {
            button.style.backgroundColor = this.buttonBackroundColor
        }
        return button
    }

    createFormGroup (children)
    {
        let divFormGroup = document.createElement('div')
        divFormGroup.classList.add('form-group')

        return UIGenerator.populateChildren(divFormGroup, children)
    }

    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
    }

    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
    }

    createFormGroupLabel (label, inputID = null, inputType = null)
    {
        let labelFormGroup = document.createElement('label')
        labelFormGroup.classList.add('form-label')
        labelFormGroup.textContent = label

        if (inputID !== null) {
            labelFormGroup.setAttribute('for', inputID)
        }
        if (inputType !== null) {
            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
    }

    createFormGroupStatLabel (statisticType)
    {
        let labelFormGroup = document.createElement('label')
        labelFormGroup.id = this.selectorGenerator.getStatLabelSelector(statisticType)
        labelFormGroup.classList.add('form-stat-label')
        labelFormGroup.textContent = '0'

        return labelFormGroup
    }

    createFormInputGroup (label, inputType = 'text', defaultValue = null)
    {
        let divFormInputGroup
        let inputID = this.selectorGenerator.getSettingsInputSelector(label)
        let labelFormGroup = this.createFormGroupLabel(label, inputType, inputID)
        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
        }
        return divFormInputGroup
    }

    createFormRangeInputGroup (label, inputsType = 'text', defaultValues = null)
    {
        let divFormInputGroup = this.createFormGroup([
            this.createFormGroupLabel(label, null, 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
    }

    createFormSection (title, children)
    {
        let sectionDiv = document.createElement('div')
        sectionDiv.classList.add('form-section')

        let sectionTitle = document.createElement('label')
        sectionTitle.textContent = title
        sectionTitle.classList.add('title')

        UIGenerator.populateChildren(sectionDiv, [sectionTitle])

        return UIGenerator.populateChildren(sectionDiv, children)
    }

    createFormSectionButton (caption, tooltip, onClick)
    {
        let button = this.createFormButton(caption, onClick)
        button.title = tooltip

        return button
    }

    createSection (IDSuffix, backgroundColor, top, width, children)
    {
        let section = document.createElement('section')
        section.id = this.selectorGenerator.getSelector(IDSuffix)
        section.classList.add('form-section')
        section.style.display = this.showUI ? 'block' : 'none'
        section.style.top = top
        section.style.width = width
        section.style.backgroundColor = backgroundColor

        return UIGenerator.populateChildren(section, children)
    }

    createSeparator ()
    {
        return document.createElement('hr')
    }

    createSettingsFormActions (localStore, onClick, addTopPadding = false)
    {
        let divFormActions = this.createFormActions([
            this.createFormButton('Apply', onClick),
            this.createFormButton('Reset', function () {
                localStore.retrieve()
                onClick()
            }),
        ])
        if (addTopPadding) {
            divFormActions.style.paddingTop = '10px'
        }
        return divFormActions
    }

    createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null)
    {
        let dropdownID = this.selectorGenerator.getSettingsInputSelector(label)

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

    createSettingsHideButton (settingsSectionIDSuffix)
    {
        let settingsSectionID = this.selectorGenerator.getSelector(settingsSectionIDSuffix)
        return this.createFormButton('Hide', function () {
            document.getElementById(settingsSectionID).style.display = 'none'
        })
    }

    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
    }

    createStatisticsFormGroup (statisticsType, label = null)
    {
        if (label === null) {
            label = statisticsType
        }
        return this.createFormGroup([
            this.createFormGroupLabel('Filtered ' + label + ' Videos'),
            this.createFormGroupStatLabel(statisticsType),
        ])
    }

    createStoreFormSection (localStore)
    {
        return this.createFormSection('Store', [
            this.createFormActions([
                this.createFormSectionButton('Update', 'Save UI settings in store', function () {
                    localStore.save()
                }),
                this.createFormSectionButton(
                    'Reset',
                    'Reset store values to user defaults',
                    function () {
                        localStore.restoreDefaults()
                    },
                ),
                this.createFormSectionButton('Purge', 'Purge store', function () {
                    localStore.delete()
                }),
            ])
        ])
    }

    getSettingsInput (label)
    {
        return document.getElementById(this.selectorGenerator.getSettingsInputSelector(label))
    }

    getSettingsInputCheckedStatus (label)
    {
        return this.getSettingsInput(label).checked
    }

    getSettingsInputValue (label, lowerBound)
    {
        return this.getSettingsRangeInput(label, lowerBound).value
    }

    getSettingsRangeInput (label, getMinInput)
    {
        return document.getElementById(this.selectorGenerator.getSettingsRangeInputSelector(label, getMinInput))
    }

    getSettingsRangeInputValue (label, getMinInputValue)
    {
        return this.getSettingsRangeInput(label, getMinInputValue).value
    }

    setSettingsInputCheckedStatus (label, bool)
    {
        this.getSettingsInput(label).checked = bool
    }

    setSettingsInputValue (label, value)
    {
        this.getSettingsInput(label).value = value
    }

    setSettingsRangeInputValue (label, lowerBound, upperBound)
    {
        this.getSettingsRangeInput(label, true).value = lowerBound
        this.getSettingsRangeInput(label, false).value = upperBound
    }
}

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

    constructor (statisticsRecorder)
    {
        this._blacklist = []
        this._filters = []
        this._optimizedBlacklist = []
        this._optimizedSanitizationRules = {}
        this._sanitizationRules = []
        this._statisticsRecorder = statisticsRecorder
    }

    addBlacklistFilter (blacklistedWords)
    {
        this._blacklist = blacklistedWords
        return this
    }

    addConditionalFilter (name, shouldBe, getValueCallback)
    {
        return this
    }

    addRangeFilter (name, conditionCallback)
    {
        return this
    }

    addSanitizationFilter (sanitizationRules, videoTitleSelector, videoPageTitleSelector)
    {
        this._sanitizationRules = sanitizationRules
        return this
    }

    optimize ()
    {
        for (let i = 0; i < this._blacklist.length; i++) {
            this._optimizedBlacklist[i] = new RegExp(this._blacklist[i], 'ig')
        }

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

            for (let i = 0; i < this._sanitizationRules[substitute].length; i++) {
                this._optimizedSanitizationRules[substitute][i] = new RegExp(this._sanitizationRules[substitute][i],
                    'ig')
            }
        }
        return this
    }

    sanitize (text)
    {
        for (const substitute in this._optimizedSanitizationRules) {
            for (const subject of this._optimizedSanitizationRules[substitute]) {
                text = text.replace(subject, substitute)
            }
        }
        return text
    }

    sanitizeVideoItem (videoNameNode)
    {
        videoNameNode.textContent = this.sanitize(videoNameNode.textContent)
        return this
    }

    sanitizeVideoPage (videoNameNodeSelector)
    {
        let videoNameNode = document.querySelector(videoNameNodeSelector)
        if (videoNameNode !== null) {

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

    validateBlackList (text)
    {
        let validationCheck = true

        if (this._optimizedBlacklist.length > 0) {

            for (const blacklistedWord of this._optimizedBlacklist) {

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

    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
    }

    validateRangeFilter (name, lowerBound, upperBound, getValueCallback)
    {
        if (lowerBound > 0 || upperBound > 0) {
            return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
        }
        return true
    }
}