Brazen Base Search Enhancer

Base class for search enhancement scripts

От 09.06.2021. Виж последната версия.

Този скрипт не може да бъде инсталиран директно. Това е библиотека за други скриптове и може да бъде използвана с мета-директива // @require https://update.sleazyfork.org/scripts/416105/939199/Brazen%20Base%20Search%20Enhancer.js

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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

// Selectors

const SELECTOR_RESTORE_CONFIG_INPUT = 'brazen-restore-settings'
const SELECTOR_SUBSCRIPTION_LOADER_BUTTON = 'brazen-subscriptions-loader'

// Preset filter configuration keys

const CONFIG_PAGINATOR_LIMIT = 'Pagination Limit'
const CONFIG_PAGINATOR_THRESHOLD = 'Pagination Threshold'

const FILTER_DURATION_RANGE = 'Duration'
const FILTER_PERCENTAGE_RATING_RANGE = 'Rating'
const FILTER_SUBSCRIBED_VIDEOS = 'Hide Subscribed Videos'
const FILTER_TEXT_BLACKLIST = 'Blacklist'
const FILTER_TEXT_SEARCH = 'Search'
const FILTER_TEXT_SANITIZATION = 'Text Sanitization Rules'
const FILTER_TEXT_WHITELIST = 'Whitelist'
const FILTER_UNRATED = 'Unrated'
const FILTER_VIEWED_VIDEOS = 'Hide Browsed Videos'
// const FILTER_WATCHED_VIDEOS = 'Hide Watched Videos'

const STORE_SUBSCRIPTIONS = 'Account Subscriptions'
// const STORE_VIEWED_ADDRESSES = 'Viewed Addresses Store'
// const STORE_WATCHED_ADDRESSES = 'Watched Addresses Store'

const UI_MARK_WATCHED_VIDEOS = 'Mark Watched Videos'

// Item preset attributes

const ITEM_ATTRIBUTE_PRESET_DURATION_RANGE = 'presetDurationRange'
const ITEM_ATTRIBUTE_PRESET_NAME = 'presetName'
const ITEM_ATTRIBUTE_PRESET_PERCENTAGE_RATING = 'presetPercentageRating'
const ITEM_ATTRIBUTE_PRESET_USERNAME = 'username'

const ITEM_PROCESSED_ONCE = 'scriptItemProcessedOnce'

// Configuration

const OPTION_ALWAYS_SHOW_SETTINGS_PANE = 'Always Show Settings Pane'
const OPTION_DISABLE_COMPLIANCE_VALIDATION = 'Disable All Filters'

class BrazenItemAttributesResolver
{
	/**
	 * @callback ItemAttributesResolverCallback
	 * @param {JQuery} item
	 * @return {*}
	 */
	
	/**
	 * @param {JQuery.Selector} itemPageLinkSelector
	 * @param {JQuery.Selector} itemPageDeepAnalysisSelector
	 */
	constructor (itemPageLinkSelector, itemPageDeepAnalysisSelector)
	{
		/**
		 * @type {{}}
		 * @private
		 */
		this._attributes = {}
		
		/**
		 * @type {{}}
		 * @private
		 */
		this._deepAttributes = {}
		
		/**
		 * @type {boolean}
		 * @private
		 */
		this._hasDeepAttributes = false
		
		/**
		 * @type {JQuery.Selector}
		 * @protected
		 */
		this._itemPageLinkSelector = itemPageLinkSelector
		
		/**
		 * @type {JQuery.Selector}
		 * @protected
		 */
		this._itemPageDeepAnalysisSelector = itemPageDeepAnalysisSelector
		
		/**
		 * @type {JQuery<HTMLElement> | jQuery | HTMLElement}
		 * @private
		 */
		this._sandbox = $('<div id="brazen-item-attributes-resolver-sandbox" hidden/>').appendTo('body')
	}
	
	/**
	 * @param {string} attributeName
	 * @returns {string}
	 * @private
	 */
	_generateAttributeName (attributeName)
	{
		return 'scriptItemAttribute' + attributeName[0].toUpperCase() + attributeName.slice(1)
	}
	
	/**
	 * @param {string} name
	 * @param {ItemAttributesResolverCallback} resolutionCallback
	 * @returns {this}
	 */
	addAttribute (name, resolutionCallback)
	{
		this._attributes[name] = resolutionCallback
		return this
	}
	
	/**
	 * @param {string} name
	 * @param {ItemAttributesResolverCallback} resolutionCallback
	 * @returns {this}
	 */
	addDeepAttribute (name, resolutionCallback)
	{
		this._deepAttributes[name] = resolutionCallback
		this._hasDeepAttributes = true
		return this
	}
	
	/**
	 * @param {JQuery} item
	 * @param {string} attributeName
	 * @returns {*}
	 */
	getAttribute (item, attributeName)
	{
		return item[0][this._generateAttributeName(attributeName)]
	}
	
	/**
	 * @param {JQuery} item
	 * @param {Function|null} afterResolutionCallback
	 */
	resolveAttributes (item, afterResolutionCallback = null)
	{
		let element = item[0]
		for (const attributeName in this._attributes) {
			element[this._generateAttributeName(attributeName)] = this._attributes[attributeName](item)
		}
		if (this._hasDeepAttributes) {
			this._sandbox.load(item.find(this._itemPageLinkSelector).attr('href') + ' ' + this._itemPageDeepAnalysisSelector, () => {
				for (const attributeName in this._deepAttributes) {
					element[this._generateAttributeName(attributeName)] = this._deepAttributes[attributeName](this._sandbox)
				}
				this._sandbox.empty()
			})
		}
	}
}

class BrazenBaseSearchEnhancer
{
	/**
	 * @typedef {{configKey: string, validate: SearchEnhancerFilterValidationCallback, comply: SearchEnhancerFilterComplianceCallback}} ComplianceFilter
	 */
	
	/**
	 * @callback SearchEnhancerFilterValidationCallback
	 * @param {*} configValues
	 * @return boolean
	 */
	
	/**
	 * @callback SearchEnhancerFilterComplianceCallback
	 * @param {JQuery} item
	 * @param {*} configValues
	 * @return {*}
	 */
	
	/**
	 * @callback SubscriptionsFilterExclusionsCallback
	 * @return {boolean}
	 */
	
	/**
	 * @callback SubscriptionsFilterUsernameCallback
	 * @param {JQuery} item
	 * @return {boolean|string}
	 */
	
	/**
	 * @return BrazenBaseSearchEnhancer
	 */
	static initialize ()
	{
		BrazenBaseSearchEnhancer.throwOverrideError()
	}
	
	static throwOverrideError ()
	{
		throw new Error('Method must be overridden.')
	}
	
	/**
	 * @param {Object} configuration
	 * @param {Object} configuration.about
	 * @param {string} configuration.about.name
	 * @param {string} configuration.about.version
	 * @param {string} configuration.about.released
	 * @param {boolean} configuration.isUserLoggedIn
	 * @param {JQuery.Selector} configuration.itemPageDeepAnalysisSelector
	 * @param {JQuery.Selector} configuration.itemPageLinkSelector
	 * @param {string} configuration.itemSelectors
	 */
	constructor (configuration)
	{
		/**
		 * Array of item compliance filters ordered in intended sequence of execution
		 * @type {ComplianceFilter[]}
		 * @private
		 */
		this._complianceFilters = []
		
		/**
		 * @type {boolean}
		 * @private
		 */
		this._isUserLoggedIn = configuration.isUserLoggedIn
		
		/**
		 * @type {string}
		 * @private
		 */
		this._itemClassesSelector = configuration.itemSelectors
		
		/**
		 * Pagination manager
		 * @type BrazenPaginator|null
		 * @private
		 */
		this._paginator = null
		
		/**
		 * @type {BrazenItemAttributesResolver}
		 * @protected
		 */
		this._itemAttributesResolver = new BrazenItemAttributesResolver(configuration.itemPageLinkSelector, configuration.itemPageDeepAnalysisSelector)
		
		/**
		 * @type {{name: string, version: string, released: string}}
		 * @private
		 */
		this._scriptBrandingInformation = configuration.about
		
		/**
		 * @type {BrazenSubscriptionsLoader}
		 * @protected
		 */
		this._subscriptionsLoader = new BrazenSubscriptionsLoader(
			(status) => this._subscriptionsLoaderButton.text(status),
			(subscriptions) => {
				this._configurationManager.getField(STORE_SUBSCRIPTIONS).value = subscriptions.length ? '"' + subscriptions.join('""') + '"' : ''
				this._configurationManager.save()
				
				BrazenUIGenerator.addTransientChangeToButton(
					this._subscriptionsLoaderButton, 'btn-secondary', 'btn-success', 'Load Subscriptions', 'Subscriptions Loaded!')
			})
		
		/**
		 * @type {JQuery}
		 * @protected
		 */
		this._subscriptionsLoaderButton = null
		
		/**
		 * @type {JQuery<HTMLElement> | jQuery | HTMLElement}
		 * @protected
		 */
		this._syncConfigButton = $('<button id="brazen-sync-config-btn" style="position: fixed">${SVG_REFRESH}</button>')
			.hide()
			.appendTo($('body'))
			.on('click', () => {
				this._onResetSettings()
				this._syncConfigButton.hide()
			})
		
		/**
		 * @type {BrazenUIGenerator}
		 * @protected
		 */
		this._uiGen = new BrazenUIGenerator(configuration.about.name)
		
		/**
		 * Local storage store with defaults
		 * @type {BrazenConfigurationManager}
		 * @protected
		 */
		this._configurationManager =
			BrazenConfigurationManager.create(this._uiGen)
			                          .addFlagField(OPTION_DISABLE_COMPLIANCE_VALIDATION, 'Disables all search filters.')
			                          .addFlagField(OPTION_ALWAYS_SHOW_SETTINGS_PANE, 'Always show configuration interface.')
			                          .onExternalConfigurationChange((manager) => {
				                          this._syncConfigButton.show()
			                          })
		
		// 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}
		 * @param {JQuery} item
		 * @protected
		 */
		this._onFirstHitAfterCompliance = null
		
		/**
		 * Operations to perform before compliance checks, the first time a item is retrieved
		 * @type {Function}
		 * @param {JQuery} item
		 * @protected
		 */
		this._onFirstHitBeforeCompliance = null
		
		/**
		 * Get item lists from the page
		 * @type {Function}
		 * @protected
		 */
		this._onGetItemLists = null
		
		/**
		 * Get item name from its node
		 * @type {Function}
		 * @param {JQuery} item
		 * @protected
		 */
		this._onGetItemName = null
		
		/**
		 * Logic to hide a non-compliant item
		 * @type {Function}
		 * @param {JQuery} item
		 * @protected
		 */
		this._onItemHide = (item) => item.addClass('noncompliant-item').hide()
		
		/**
		 * Logic to show compliant item
		 * @type {Function}
		 * @param {JQuery} item
		 * @protected
		 */
		this._onItemShow = (item) => item.removeClass('noncompliant-item').show()
		
		/**
		 * Must be used to compose the UI
		 * @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
	}
	
	/**
	 * @param item
	 * @private
	 */
	_complyItem (item)
	{
		let itemComplies = true
		
		if (!this._configurationManager.getValue(OPTION_DISABLE_COMPLIANCE_VALIDATION) &&
			this._validateItemWhiteList(item) &&
			Utilities.callEventHandler(this._onBeforeCompliance, [item], true)
		) {
			let configField
			
			for (let complianceFilter of this._complianceFilters) {
				
				configField = this._configurationManager.getFieldOrFail(complianceFilter.configKey)
				if (complianceFilter.validate(this._configurationManager.getValue(configField))) {
					
					itemComplies = complianceFilter.comply(item, this._configurationManager.getValue(configField))
					this._configurationManager.trackState(configField, itemComplies)
					
					if (!itemComplies) {
						break
					}
				}
			}
		}
		itemComplies ? Utilities.callEventHandler(this._onItemShow, [item]) : Utilities.callEventHandler(this._onItemHide, [item])
		item.css('opacity', 'unset')
	}
	
	/**
	 * @param {string} configKey
	 * @param {SearchEnhancerFilterValidationCallback|null} validationCallback
	 * @param {SearchEnhancerFilterComplianceCallback} complianceCallback
	 * @protected
	 */
	_addItemComplexComplianceFilter (configKey, validationCallback, complianceCallback)
	{
		this._addItemComplianceFilter(configKey, complianceCallback, validationCallback)
	}
	
	/**
	 * @param {string} configKey
	 * @param {SearchEnhancerFilterComplianceCallback|string} action
	 * @param {SearchEnhancerFilterValidationCallback|null} validationCallback
	 * @protected
	 */
	_addItemComplianceFilter (configKey, action, validationCallback = null)
	{
		let configType = this._configurationManager.getField(configKey).type
		if (typeof action === 'string') {
			let attributeName = action
			switch (configType) {
				case CONFIG_TYPE_FLAG:
					action = (item) => this.getItemAttribute(item, attributeName)
					break
				case CONFIG_TYPE_RANGE:
					action = (item, range) => Validator.isInRange(this.getItemAttribute(item, attributeName), range.minimum, range.maximum)
					break
				default:
					throw new Error('Associated config type requires explicit action callback definition.')
			}
		}
		if (validationCallback === null) {
			switch (configType) {
				case CONFIG_TYPE_FLAG:
				case CONFIG_TYPE_RADIOS_GROUP:
				case CONFIG_TYPE_SELECT:
					validationCallback = (value) => value
					break
				case CONFIG_TYPE_CHECKBOXES_GROUP:
					validationCallback = (valueKeys) => valueKeys.length
					break
				case CONFIG_TYPE_NUMBER:
					validationCallback = (value) => value > 0
					break
				case CONFIG_TYPE_RANGE:
					validationCallback = (range) => range.minimum > 0 || range.maximum > 0
					break
				case CONFIG_TYPE_RULESET:
					validationCallback = (rules) => rules.length
					break
				case CONFIG_TYPE_TEXT:
					validationCallback = (value) => value.length
					break
				default:
					throw new Error('Associated config type requires explicit validation callback definition.')
			}
		}
		this._complianceFilters.push({
			configKey: configKey,
			validate: validationCallback,
			comply: action,
		})
	}
	
	/**
	 * @param {string} helpText
	 * @protected
	 */
	_addItemBlacklistFilter (helpText)
	{
		this._configurationManager
		    .addRulesetField(FILTER_TEXT_BLACKLIST, 3, helpText)
			.onOptimize = (rules) => Utilities.buildWholeWordMatchingRegex(rules) ?? ''
		
		this._addItemComplexComplianceFilter(
			FILTER_TEXT_BLACKLIST,
			(value) => value !== '',
			(item, value) => this.getItemAttribute(item, ITEM_ATTRIBUTE_PRESET_NAME).match(value) === null,
		)
	}
	
	/**
	 * @param {JQuery.Selector} durationNodeSelector
	 * @param {string|null} helpText
	 * @protected
	 */
	_addItemDurationRangeFilter (durationNodeSelector, helpText = null)
	{
		this._configurationManager.addRangeField(FILTER_DURATION_RANGE, 'seconds', 0, 100000, helpText ?? 'Filter items by duration.')
		
		this._itemAttributesResolver.addAttribute(ITEM_ATTRIBUTE_PRESET_DURATION_RANGE, (item) => {
			let duration = 0
			let durationNode = item.find(durationNodeSelector)
			if (durationNode.length) {
				duration = durationNode.text().split(':')
				duration = (parseInt(duration[0]) * 60) + parseInt(duration[1])
			}
			if (duration === 0 && !durationNode.length) {
				duration = -1
			}
			return duration
		})
		
		this._addItemComplianceFilter(FILTER_DURATION_RANGE, (item, range) => {
			let duration = this.getItemAttribute(item, ITEM_ATTRIBUTE_PRESET_DURATION_RANGE)
			return duration > 0 ? Validator.isInRange(duration, range.minimum, range.maximum) : duration === -1
		})
	}
	
	/**
	 * @param {JQuery.Selector} ratingNodeSelector
	 * @param {string|null} helpText
	 * @param {string|null} unratedHelpText
	 * @protected
	 */
	_addItemPercentageRatingRangeFilter (ratingNodeSelector, helpText = null, unratedHelpText = null)
	{
		this._configurationManager.addRangeField(FILTER_PERCENTAGE_RATING_RANGE, '%', 0, 100000, helpText ?? 'Filter items by percentage rating.')
		    .addFlagField(FILTER_UNRATED, unratedHelpText ?? 'Hide items with zero or no rating.')
		
		this._itemAttributesResolver.addAttribute(ITEM_ATTRIBUTE_PRESET_PERCENTAGE_RATING, (item) => {
			let rating = item.find(ratingNodeSelector)
			return rating.length === 0 ? 0 : parseInt(rating.text().replace('%', ''))
		})
		
		this._addItemComplianceFilter(FILTER_PERCENTAGE_RATING_RANGE, (item, range) => {
			let rating = this.getItemAttribute(item, ITEM_ATTRIBUTE_PRESET_PERCENTAGE_RATING)
			return rating === 0 ? !this._configurationManager.getValue(FILTER_UNRATED) : Validator.isInRange(rating, range.minimum, range.maximum)
		})
	}
	
	/**
	 * @param {string} helpText
	 * @protected
	 */
	_addItemTextSanitizationFilter (helpText)
	{
		let field = this._configurationManager.addRulesetField(FILTER_TEXT_SANITIZATION, 2, helpText, false)
		field.onTranslateFromUI = (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
		}
		field.onFormatForUI = (rules) => {
			let sanitizationRulesText = []
			for (let substitute in rules) {
				sanitizationRulesText.push(substitute + '=' + rules[substitute].join(','))
			}
			return sanitizationRulesText
			
		}
		field.onOptimize = (rules) => {
			let optimizedRules = {}
			for (const substitute in rules) {
				optimizedRules[substitute] = Utilities.buildWholeWordMatchingRegex(rules[substitute])
			}
			return optimizedRules
		}
	}
	
	/**
	 * @param {string|null} helpText
	 * @protected
	 */
	_addItemTextSearchFilter (helpText = null)
	{
		this._configurationManager.addTextField(FILTER_TEXT_SEARCH, helpText ?? 'Show videos with these comma separated words in their names.')
		this._addItemComplianceFilter(FILTER_TEXT_SEARCH, (item, value) => this.getItemAttribute(item, ITEM_ATTRIBUTE_PRESET_NAME).includes(value))
	}
	
	// /**
	//  * @protected
	//  */
	// _addItemCustomWatchedFilters ()
	// {
	//     this._configurationManager.
	//         addFlagField(FILTER_VIEWED_VIDEOS, 'Tracks and hides all items present on opened pages on next page load. Should be purged regularly.').
	//         addFlagField(FILTER_WATCHED_VIDEOS, 'Tracks and hides 3,000 recent seen items.').
	//         addTextField(STORE_VIEWED_ADDRESSES, '').
	//         addTextField(STORE_WATCHED_ADDRESSES, '')
	// }
	
	/**
	 * @param {string} helpText
	 * @protected
	 */
	_addItemWhitelistFilter (helpText)
	{
		this._configurationManager
		    .addRulesetField(FILTER_TEXT_WHITELIST, 3, helpText)
			.onOptimize = (rules) => Utilities.buildWholeWordMatchingRegex(rules)
	}
	
	_addPaginationConfiguration ()
	{
		this._configurationManager.addNumberField(CONFIG_PAGINATOR_LIMIT, 'pages', 1, 50,
			'Limit paginator to concatenate the specified number of maximum pages.')
		    .addNumberField(CONFIG_PAGINATOR_THRESHOLD, 'results', 1, 1000, 'Make paginator ensure the specified number of minimum results.')
	}
	
	/**
	 * @param {SubscriptionsFilterExclusionsCallback} exclusionsCallback Add page exclusions here
	 * @param {SubscriptionsFilterUsernameCallback} getItemUsername Return username of the item or return false to skip
	 * @return {BrazenSubscriptionsLoader}
	 * @protected
	 */
	_addSubscriptionsFilter (exclusionsCallback, getItemUsername)
	{
		this._configurationManager.addFlagField(FILTER_SUBSCRIBED_VIDEOS, 'Hide videos from subscribed channels.')
		    .addTextField(STORE_SUBSCRIPTIONS, 'Recorded subscription accounts.')
		
		this._itemAttributesResolver.addAttribute(ITEM_ATTRIBUTE_PRESET_USERNAME, (item) => getItemUsername(item))
		
		this._addItemComplexComplianceFilter(
			FILTER_SUBSCRIBED_VIDEOS,
			(value) => value && this._isUserLoggedIn && exclusionsCallback(),
			(item) => {
				let username = this.getItemAttribute(item, ITEM_ATTRIBUTE_PRESET_USERNAME)
				return username === false ? true : !(new RegExp('"([^"]*' + username + '[^"]*)"')).test(
					this._configurationManager.getValue(STORE_SUBSCRIPTIONS))
			})
		return this._subscriptionsLoader
	}
	
	/**
	 * 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.css('opacity', 0.5).each((index, element) => {
			let item = $(element)
			
			// First run processing
			
			if (typeof element[ITEM_PROCESSED_ONCE] === 'undefined') {
				element[ITEM_PROCESSED_ONCE] = false
				this._itemAttributesResolver.resolveAttributes(item)
				Utilities.callEventHandler(this._onFirstHitBeforeCompliance, [item])
			}
			
			// Compliance filtering
			
			this._complyItem(item)
			
			// After first run processing
			
			if (!element[ITEM_PROCESSED_ONCE]) {
				Utilities.callEventHandler(this._onFirstHitAfterCompliance, [item])
				element[ITEM_PROCESSED_ONCE] = true
			}
		})
		this._configurationManager.updateTrackersInterface()
	}
	
	/**
	 * @protected
	 * @return {JQuery}
	 */
	_createPaginationTab ()
	{
		return this._uiGen.createAccordionTab('Pagination', [
			this._configurationManager.createElement(CONFIG_PAGINATOR_THRESHOLD),
			this._configurationManager.createElement(CONFIG_PAGINATOR_LIMIT)
		])
	}
	
	/**
	 * @protected
	 * @return {JQuery}
	 */
	_createSettingsBackupRestoreTab ()
	{
		let inputControl = $(`<input id="${SELECTOR_RESTORE_CONFIG_INPUT}" class="form-control" type="text">`)
		
		return this._uiGen.createAccordionTab('Backup & Restore', [
			this._uiGen.createFormHelpText(
				this._uiGen.createFormGroup(
					this._uiGen
					    .createButton('Backup Configuration', 'secondary', (button) => this._onBackupSettings(button))
					    .addClass('btn-block')
				),
				null,
				'Backup settings to the clipboard.',
			),
			this._uiGen.createFormHelpText(this._uiGen.createFormGroup(inputControl), inputControl, 'Paste backed up configuration.'),
			this._uiGen.createFormHelpText(
				this._uiGen.createFormGroup(
					this._uiGen
					    .createButton('Restore Configuration', 'danger', (button) => this._onRestoreSettings(button))
					    .addClass('btn-block')
				),
				null,
				'Restore backup settings.',
			),
		])
	}
	
	/**
	 * @protected
	 * @return {JQuery}
	 */
	_createSubscriptionLoaderTab ()
	{
		this._subscriptionsLoaderButton = this._uiGen.createButton('Load Subscriptions', 'secondary', (button) => {
			if (this._isUserLoggedIn) {
				this._subscriptionsLoaderButton.removeClass('btn-secondary').addClass('btn-warning')
				this._subscriptionsLoader.run()
			} else {
				BrazenUIGenerator.addTransientChangeToButton(
					button, 'btn-secondary', 'btn-danger', 'Load Subscriptions', 'Please login before proceeding.')
			}
			button.prop('disabled', false)
			
		}).attr('id', SELECTOR_SUBSCRIPTION_LOADER_BUTTON)
		
		return this._uiGen.createFormHelpText(
			this._uiGen.createFormGroup(this._subscriptionsLoaderButton), null, 'Makes a copy of your subscriptions in cache for related filters.')
	}
	
	/**
	 * @private
	 */
	_onApplyNewSettings ()
	{
		this._configurationManager.update()
		this._validateCompliance()
	}
	
	/**
	 * @param {JQuery} button
	 * @private
	 */
	_onBackupSettings (button)
	{
		button.prop('disabled', false)
		navigator.clipboard
		         .writeText(this._configurationManager.backup())
		         .then(() => BrazenUIGenerator.addTransientChangeToButton(
			         button, 'btn-secondary', 'btn-success', 'Backup Configuration', 'Config Copied To Clipboard!'))
		         .catch(() => button.removeClass('btn-secondary').addClass('btn-danger').text('Error!'))
	}
	
	/**
	 * @private
	 */
	_onResetSettings ()
	{
		this._configurationManager.revertChanges()
		this._validateCompliance()
	}
	
	/**
	 * @param {JQuery} button
	 * @private
	 */
	_onRestoreSettings (button)
	{
		let settings = $('#' + SELECTOR_RESTORE_CONFIG_INPUT).val().trim()
		if (!settings) {
			button.prop('disabled', false)
			BrazenUIGenerator.addTransientContentChangeToButton(button, 'Restore Configuration', 'No Configuration Found!')
		} else {
			try {
				this._configurationManager.restore(settings)
				
				button.prop('disabled', false)
				BrazenUIGenerator.addTransientChangeToButton(
					button, 'btn-danger', 'btn-success', 'Restore Configuration', 'Configuration Restored!')
				
				this._validateCompliance()
			} catch (e) {
				button.text('Error!')
			}
		}
	}
	
	/**
	 * @private
	 */
	_onSaveSettings ()
	{
		this._onApplyNewSettings()
		this._configurationManager.save()
	}
	
	/**
	 * @protected
	 */
	_setupUI ()
	{
		this.constructor.throwOverrideError()
	}
	
	/**
	 * @protected
	 */
	_setupAttributes ()
	{
		this.constructor.throwOverrideError()
	}
	
	/**
	 * @protected
	 */
	_setupCompliance ()
	{
		this.constructor.throwOverrideError()
	}
	
	/**
	 * @protected
	 */
	_setupConfiguration ()
	{
		this.constructor.throwOverrideError()
	}
	
	/**
	 * @protected
	 */
	_setupComplianceFilters()
	{
		this.constructor.throwOverrideError()
	}
	
	/**
	 * @param {boolean} firstRun
	 * @protected
	 */
	_validateCompliance (firstRun = false)
	{
		let itemLists = Utilities.callEventHandler(this._onGetItemLists)
		if (!firstRun) {
			this._configurationManager.resetTrackers()
			itemLists.each((index, itemsList) => {
				this._complyItemsList($(itemsList))
			})
		} else {
			itemLists.each((index, itemList) => {
				let itemListObject = $(itemList)
				
				if (this._paginator && itemListObject.is(this._paginator.getListSelector())) {
					ChildObserver.create().onNodesAdded((itemsAdded) => {
						this._complyItemsList($(itemsAdded), true)
						this._paginator.run(this._configurationManager.getValue(CONFIG_PAGINATOR_THRESHOLD),
							this._configurationManager.getValue(CONFIG_PAGINATOR_LIMIT))
					}).observe(itemList)
				} else {
					ChildObserver.create().onNodesAdded((itemsAdded) => this._complyItemsList($(itemsAdded), true)).observe(itemList)
				}
				
				this._complyItemsList(itemListObject)
			})
		}
		if (this._paginator) {
			this._paginator.run(this._configurationManager.getValue(CONFIG_PAGINATOR_THRESHOLD), this._configurationManager.getValue(CONFIG_PAGINATOR_LIMIT))
		}
	}
	
	/**
	 * @param {JQuery} item
	 * @return {boolean}
	 * @protected
	 */
	_validateItemWhiteList (item)
	{
		let field = this._configurationManager.getField(FILTER_TEXT_WHITELIST)
		if (field) {
			let validationResult = field.value.length ? Validator.regexMatches(this.getItemAttribute(item, ITEM_ATTRIBUTE_PRESET_NAME), field.optimized) : true
			this._configurationManager.trackState(field, validationResult)
			return validationResult
		}
		return true
	}
	
	/**
	 * @param {JQuery} item
	 * @param {string} attributeName
	 * @returns {*}
	 */
	getItemAttribute (item, attributeName)
	{
		return this._itemAttributesResolver.getAttribute(item, attributeName)
	}
	
	/**
	 * Initialize the script and do basic UI removals
	 */
	initialize ()
	{
		if (Utilities.callEventHandler(this._onValidateInit)) {
			
			this._setupConfiguration()
			this._configurationManager.initialize(this._scriptBrandingInformation.name)
			
			this._setupAttributes()
			this._itemAttributesResolver.addAttribute(
				ITEM_ATTRIBUTE_PRESET_NAME,
				(item) => Utilities.callEventHandlerOrFail('getItemName', this._onGetItemName, [item]),
			)
			
			this._setupCompliance()
			this._setupComplianceFilters()
			this._setupUI()
			
			if (this._paginator) {
				this._paginator.initialize()
			}
			
			Utilities.callEventHandler(this._onBeforeUIBuild)
			Utilities.callEventHandler(this._onUIBuild)
			this._uiGen.createAccordionTab('About', [
				this._uiGen.createFormLabelGroup('Name', this._scriptBrandingInformation.name),
				this._uiGen.createFormLabelGroup('Version', this._scriptBrandingInformation.version),
				this._uiGen.createFormLabelGroup('Release Date', this._scriptBrandingInformation.released),
				this._uiGen.createFormLabelGroup('Author', 'Brazenvoid'),
				this._uiGen.createFormLabelGroup('Support', 'https://www.patreon.com/brazen_scripts'),
			])
			this._uiGen.initialize()
			Utilities.callEventHandler(this._onAfterUIBuild)
			
			this._configurationManager.updateInterface()
			
			this._validateCompliance(true)
			
			Utilities.callEventHandler(this._onAfterInitialization)
		}
	}
	
	/**
	 * @returns {boolean}
	 */
	isUserLoggedIn ()
	{
		return this._isUserLoggedIn
	}
}