// ==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)
}
}