Greasy Fork

Brazen Configuration Manager

Configuration management and related UI creation module

目前为 2021-06-19 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/418665/942026/Brazen%20Configuration%20Manager.js

// ==UserScript==
// @name         Brazen Configuration Manager
// @namespace    brazenvoid
// @version      2.1.1
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Configuration management and related UI creation module
// ==/UserScript==

const CONFIG_TYPE_CHECKBOXES_GROUP = 'checkboxes'
const CONFIG_TYPE_FLAG = 'flag'
const CONFIG_TYPE_NUMBER = 'number'
const CONFIG_TYPE_RADIOS_GROUP = 'radios'
const CONFIG_TYPE_RANGE = 'range'
const CONFIG_TYPE_RULESET = 'ruleset'
const CONFIG_TYPE_SELECT = 'select'
const CONFIG_TYPE_TEXT = 'text'

const SELECTOR_CONFIG_TRACKER_TOTAL = '#brazen-config-tracker-total'

class BrazenConfigurationManager
{
	/**
	 * @typedef {{key: string, title: string, type: string, element: null|JQuery, value: *, maximum: int, minimum: int, optimized: *, options: string[],
	 *     helpText: string, unit: string, onFormatForUI: ConfigurationManagerRulesetCallback, onTranslateFromUI: ConfigurationManagerRulesetCallback,
	 *     onOptimize: ConfigurationManagerRulesetCallback, createElement: Function, setFromUserInterface: Function, track: boolean, trackerValue: number,
	 *     updateUserInterface: Function}} ConfigurationField
	 */
	
	/**
	 * @callback ConfigurationManagerRulesetCallback
	 * @param {*} values
	 */
	
	/**
	 * @callback ExternalConfigurationChangeCallback
	 * @param {BrazenConfigurationManager} manager
	 */
	
	/**
	 * @param {BrazenUIGenerator} uiGenerator
	 * @return {BrazenConfigurationManager}
	 */
	static create (uiGenerator)
	{
		return new BrazenConfigurationManager(uiGenerator)
	}
	
	/**
	 * @param {ConfigurationField} field
	 * @returns {string}
	 */
	static generateIdForTrackerGroup (field)
	{
		return field.key + '-stat'
	}
	
	constructor (uiGenerator)
	{
		/**
		 * @type {{}}
		 * @private
		 */
		this._config = {}
		
		/**
		 * @type {ExternalConfigurationChangeCallback|null}
		 * @private
		 */
		this._onExternalConfigurationChange = null
		
		/**
		 * @type {LocalStore}
		 * @private
		 */
		this._localStore = null
		
		/**
		 * @type {LocalStore}
		 * @private
		 */
		this._localStoreId = null
		
		/**
		 * @type {number}
		 * @private
		 */
		this._syncedLocalStoreId = 0
		
		/**
		 * @type BrazenUIGenerator
		 * @private
		 */
		this._uiGen = uiGenerator
	}
	
	/**
	 * @param {string} type
	 * @param {string} name
	 * @param {*} value
	 * @param {string|null} helpText
	 * @param {boolean} enableTracking
	 * @return {ConfigurationField}
	 * @private
	 */
	_createField (type, name, value, helpText, enableTracking)
	{
		let fieldKey = this._formatFieldKey(name)
		let field = this._config[fieldKey]
		if (!field) {
			field = {
				key: fieldKey,
				element: null,
				helpText: helpText,
				title: name,
				type: type,
				value: value,
				createElement: null,
				setFromUserInterface: null,
				track: enableTracking,
				trackerValue: 0,
				updateUserInterface: null,
			}
			this._config[fieldKey] = field
		} else {
			if (helpText) {
				field.helpText = helpText
			}
			field.value = value
		}
		return field
	}
	
	/**
	 * @param {string} name
	 * @return {string}
	 * @private
	 */
	_formatFieldKey (name)
	{
		return Utilities.toKebabCase(name.replaceAll('&', 'and'))
	}
	
	/**
	 * @param {boolean} ignoreIfDefaultsSet
	 * @private
	 */
	_syncLocalStore (ignoreIfDefaultsSet)
	{
		let field
		let storeObject = this._localStore.get()
		
		if (!ignoreIfDefaultsSet || !this._localStore.wereDefaultsSet()) {
			for (let key in this._config) {
				
				field = this._config[key]
				if (typeof storeObject[key] !== 'undefined') {
					
					field.value = storeObject[key]
					if (field.type === CONFIG_TYPE_RULESET) {
						field.optimized = Utilities.callEventHandler(field.onOptimize, [field.value])
					}
				}
			}
			this.updateInterface()
		}
		return this
	}
	
	/**
	 * @return {{}}
	 * @private
	 */
	_toStoreObject ()
	{
		let storeObject = {}
		for (let key in this._config) {
			storeObject[key] = this._config[key].value
		}
		return storeObject
	}
	
	/**
	 * @private
	 */
	_regenerateStoreId ()
	{
		this._localStoreId.save({id: Utilities.generateId()})
	}
	
	/**
	 * @param {string} name
	 * @param {string[]} options
	 * @param {string} helpText
	 * @param {boolean} enableTracking
	 * @returns {BrazenConfigurationManager}
	 */
	addCheckboxesGroup (name, options, helpText, enableTracking = true)
	{
		let field = this._createField(CONFIG_TYPE_CHECKBOXES_GROUP, name, [], helpText, enableTracking)
		
		field.options = options
		
		field.createElement = () => this._uiGen.createFormConfigSwitchesGroup(field)
		field.setFromUserInterface = () => {
			field.value = []
			field.element.find('input:checked').each((index, element) => field.value.push($(element).val()))
		}
		field.updateUserInterface = () => {
			let elements = field.element.find('input').prop('checked', false)
			for (let checkboxIndex of field.value) {
				elements.eq(checkboxIndex).prop('checked', true)
			}
		}
		return this
	}
	
	/**
	 * @param {string} name
	 * @param {string} helpText
	 * @param {boolean} enableTracking
	 * @returns {BrazenConfigurationManager}
	 */
	addFlagField (name, helpText, enableTracking = true)
	{
		let field = this._createField(CONFIG_TYPE_FLAG, name, false, helpText, enableTracking)
		
		field.createElement = () => this._uiGen.createFormConfigSwitch(field)
		field.setFromUserInterface = () => field.value = field.element.find('input').prop('checked')
		field.updateUserInterface = () => field.element.find('input').prop('checked', field.value)
		
		return this
	}
	
	/**
	 * @param {string} name
	 * @param {string} unit
	 * @param {number} minimum
	 * @param {number} maximum
	 * @param {string} helpText
	 * @param {boolean} enableTracking
	 * @returns {BrazenConfigurationManager}
	 */
	addNumberField (name, unit, minimum, maximum, helpText, enableTracking = true)
	{
		let field = this._createField(CONFIG_TYPE_NUMBER, name, minimum, helpText, enableTracking)
		
		field.minimum = minimum
		field.maximum = maximum
		field.unit = unit
		
		field.createElement = () => this._uiGen.createFormConfigNumberInput(field)
		field.setFromUserInterface = () => field.value = field.element.find('input').val()
		field.updateUserInterface = () => field.element.find('input').val(field.value)
		
		return this
	}
	
	/**
	 * @param {string} name
	 * @param {string} unit
	 * @param {number} minimum
	 * @param {number} maximum
	 * @param {string} helpText
	 * @param {boolean} enableTracking
	 * @returns {BrazenConfigurationManager}
	 */
	addRangeField (name, unit, minimum, maximum, helpText, enableTracking = true)
	{
		let field = this._createField(CONFIG_TYPE_RANGE, name, {minimum: minimum, maximum: minimum}, helpText, enableTracking)
		
		field.minimum = minimum
		field.maximum = maximum
		field.unit = unit
		
		field.createElement = () => this._uiGen.createFormConfigRangeInput(field)
		field.setFromUserInterface = () => field.value = {
			minimum: field.element.find('.range-min').val(),
			maximum: field.element.find('.range-max').val(),
		}
		field.updateUserInterface = () => {
			field.element.find('.range-min').val(field.value.minimum)
			field.element.find('.range-max').val(field.value.maximum)
		}
		return this
	}
	
	/**
	 * @param {string} name
	 * @param {string|null} helpText
	 * @param {boolean} enableTracking
	 * @return {ConfigurationField}
	 */
	addRulesetField (name, helpText, enableTracking = true)
	{
		let field = this._createField(CONFIG_TYPE_RULESET, name, [], helpText, enableTracking)
		
		field.optimized = null
		field.onTranslateFromUI = null
		field.onFormatForUI = null
		field.onOptimize = null
		
		field.createElement = () => this._uiGen.createFormConfigRulesetAccordion(field)
		field.setFromUserInterface = () => {
			let value = Utilities.trimAndKeepNonEmptyStrings(field.element.find('.ruleset-input').val().split(REGEX_LINE_BREAK))
			field.value = Utilities.callEventHandler(field.onTranslateFromUI, [value], value)
			field.optimized = Utilities.callEventHandler(field.onOptimize, [field.value])
		}
		field.updateUserInterface = () => field.element
		                                       .find('.ruleset-input')
		                                       .val(Utilities.callEventHandler(field.onFormatForUI, [field.value], field.value).join('\n'))
		return field
	}
	
	/**
	 * @param {string} name
	 * @param {string[]} options
	 * @param {string} helpText
	 * @param {boolean} enableTracking
	 * @returns {BrazenConfigurationManager}
	 */
	addSelectField (name, options, helpText, enableTracking = true)
	{
		let field = this._createField(CONFIG_TYPE_SELECT, name, 0, helpText, enableTracking)
		
		field.options = options
		
		field.createElement = () => this._uiGen.createFormConfigSelect(field)
		field.setFromUserInterface = () => field.value = field.element.find('select').val()
		field.updateUserInterface = () => field.element.find('select').val(field.value).trigger('change')
		
		return this
	}
	
	/**
	 * @param {string} name
	 * @param {string} helpText
	 * @param {boolean} enableTracking
	 * @returns {BrazenConfigurationManager}
	 */
	addTextField (name, helpText, enableTracking = true)
	{
		let field = this._createField(CONFIG_TYPE_TEXT, name, '', helpText, enableTracking)
		
		field.createElement = () => this._uiGen.createFormConfigInput(field)
		field.setFromUserInterface = () => field.value = field.element.find('input').val()
		field.updateUserInterface = () => field.element.find('input').val(field.value)
		
		return this
	}
	
	backup ()
	{
		let backupConfig = this._toStoreObject()
		backupConfig.id = this._syncedLocalStoreId
		return Utilities.objectToJSON(backupConfig)
	}
	
	/**
	 * @param {string} configName
	 * @returns {JQuery}
	 */
	createElement (configName)
	{
		let field = this.getFieldOrFail(configName)
		if (field.track) {
			this._uiGen.createFormStatGroup(field)
		}
		return field.createElement()
	}
	
	/**
	 * @param {string} configName
	 * @return {ConfigurationField|null}
	 */
	getField (configName)
	{
		return this._config[this._formatFieldKey(configName)]
	}
	
	/**
	 * @param {string} configName
	 * @return {ConfigurationField}
	 */
	getFieldOrFail (configName)
	{
		let field = this._config[this._formatFieldKey(configName)]
		if (field) {
			return field
		}
		throw new Error('Field named "' + configName + '" could not be found')
	}
	
	/**
	 * @param {string|ConfigurationField} configName
	 * @return {*}
	 */
	getValue (configName)
	{
		let field = typeof configName === 'string' ? this.getFieldOrFail(configName) : configName
		return field.optimized ?? field.value
	}
	
	/**
	 * @param {string} configName
	 * @return {boolean}
	 */
	hasField (configName)
	{
		return typeof this.getField(configName) !== 'undefined'
	}
	
	/**
	 * @return {BrazenConfigurationManager}
	 */
	initialize (scriptName)
	{
		let scriptKey = this._formatFieldKey(scriptName)
		this._localStore = new LocalStore(scriptKey + '-settings', this._toStoreObject())
		this._localStore.onChange(() => this.updateInterface())
		this._localStoreId = new LocalStore(scriptKey + '-settings-id', {id: Utilities.generateId()})
		
		$(document).on('visibilitychange', () => {
			if (!document.hidden && this._syncedLocalStoreId !== this._localStoreId.get().id) {
				Utilities.callEventHandler(this._onExternalConfigurationChange, [this])
			}
		})
		return this._syncLocalStore(true)
	}
	
	/**
	 * @param {ExternalConfigurationChangeCallback} eventHandler
	 * @return {BrazenConfigurationManager}
	 */
	onExternalConfigurationChange (eventHandler)
	{
		this._onExternalConfigurationChange = eventHandler
		return this
	}
	
	/**
	 * @param {string} backedUpConfiguration
	 */
	restore (backedUpConfiguration)
	{
		let backupConfig = Utilities.objectFromJSON(backedUpConfiguration)
		
		if (typeof backupConfig.id !== 'undefined') {
			this._syncedLocalStoreId = backupConfig.id
			this._localStoreId.save({id: backupConfig.id})
			delete backupConfig.id
		} else {
			this._regenerateStoreId()
		}
		
		this._localStore.save(backupConfig)
		this._syncLocalStore(false)
		
		return this
	}
	
	revertChanges ()
	{
		return this._syncLocalStore(false)
	}
	
	save ()
	{
		this.update()._localStore.save(this._toStoreObject())
		this._regenerateStoreId()
		
		return this
	}
	
	/**
	 * @param {ConfigurationField} field
	 * @param {number|boolean} value
	 */
	trackState (field, value = 1)
	{
		if (field.track) {
			field.trackerValue += value
		}
	}
	
	update ()
	{
		let field
		for (let fieldName in this._config) {
			field = this._config[fieldName]
			if (field.element) {
				field.setFromUserInterface()
			}
		}
		return this
	}
	
	resetTrackers ()
	{
		let field
		for (const fieldName in this._config) {
			field = this._config[fieldName]
			field.trackerValue = 0
		}
	}
	
	updateInterface ()
	{
		let field
		for (let fieldName in this._config) {
			field = this._config[fieldName]
			if (field.element) {
				field.updateUserInterface()
			}
		}
		return this
	}
	
	updateTrackersInterface ()
	{
		let field
		let total = 0;
		for (const fieldName in this._config) {
			field = this._config[fieldName]
			if (field.track) {
				$('#' + this.constructor.generateIdForTrackerGroup(field)).text(field.trackerValue)
				total += field.trackerValue
			}
		}
		$(SELECTOR_CONFIG_TRACKER_TOTAL).text(total)
	}
}