Greasy Fork is available in English.

Brazen Configuration Manager

Configuration management and related UI creation module

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/scripts/418665/1481350/Brazen%20Configuration%20Manager.js

// ==UserScript==
// @name         Brazen Configuration Manager
// @namespace    brazenvoid
// @version      1.6.0
// @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'

class BrazenConfigurationManager
{
  /**
   * @typedef {{title: string, type: string, element: null|JQuery, value: *, maximum: int, minimum: int, options: string[], helpText: string,
   *            onFormatForUI: ConfigurationManagerRulesetCallback, onTranslateFromUI: ConfigurationManagerRulesetCallback,
   *            onOptimize: ConfigurationManagerRulesetCallback, createElement: Function, setFromUserInterface: Function, updateUserInterface: Function,
   *            optimized?: *}} ConfigurationField
   */

  /**
   * @callback ConfigurationManagerRulesetCallback
   * @param {*} values
   */

  /**
   * @callback ExternalConfigurationChangeCallback
   * @param {BrazenConfigurationManager} manager
   */

  /**
   * @param {BrazenUIGenerator} uiGenerator
   */
  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 {BrazenUIGenerator} uiGenerator
   * @return {BrazenConfigurationManager}
   */
  static create(uiGenerator)
  {
    return new BrazenConfigurationManager(uiGenerator)
  }

  /**
   * @param {string} type
   * @param {string} name
   * @param {*} value
   * @param {string|null} helpText
   * @return ConfigurationField
   * @private
   */
  _createField(type, name, value, helpText)
  {
    let fieldKey = this._formatFieldKey(name)
    let field = this._config[fieldKey]
    if (!field) {
      field = {
        element: null,
        helpText: helpText,
        title: name,
        type: type,
        value: value,
        createElement: null,
        setFromUserInterface: null,
        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)
  }

  /**
   * @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
  }

  /**
   * @param id
   * @private
   */
  _updateLocalStoreId(id = null)
  {
    if (id === null) {
      id = Utilities.generateId()
    }
    this._localStoreId.save({id: id})
    this._syncedLocalStoreId = id
  }

  /**
   * @param {string} name
   * @param {array} keyValuePairs
   * @param {string} helpText
   * @returns {BrazenConfigurationManager}
   */
  addCheckboxesGroup(name, keyValuePairs, helpText)
  {
    let field = this._createField(CONFIG_TYPE_CHECKBOXES_GROUP, name, [], helpText)

    field.options = keyValuePairs

    field.createElement = () => {
      field.element = this._uiGen.createFormCheckBoxesGroupSection(field.title, field.options, field.helpText)
      return field.element
    }
    field.setFromUserInterface = () => {
      field.value = []
      field.element.find('input:checked').each((index, element) => {
        field.value.push($(element).attr('data-value'))
      })
    }
    field.updateUserInterface = () => {
      let elements = field.element.find('input')
      for (let key of field.value) {
        elements.filter('[data-value="' + key + '"]').prop('checked', true)
      }
    }
    return this
  }

  /**
   * @param {string} name
   * @param {string} helpText
   * @returns {BrazenConfigurationManager}
   */
  addFlagField(name, helpText)
  {
    let field = this._createField(CONFIG_TYPE_FLAG, name, false, helpText)

    field.createElement = () => {
      let inputGroup = this._uiGen.createFormInputGroup(field.title, 'checkbox', field.helpText)
      field.element = inputGroup.find('input')
      return inputGroup
    }
    field.setFromUserInterface = () => {
      field.value = field.element.prop('checked')
    }
    field.updateUserInterface = () => {
      field.element.prop('checked', field.value)
    }
    return this
  }

  /**
   * @param {string} name
   * @param {int} minimum
   * @param {int} maximum
   * @param {string} helpText
   * @returns {BrazenConfigurationManager}
   */
  addNumberField(name, minimum, maximum, helpText)
  {
    let field = this._createField(CONFIG_TYPE_NUMBER, name, minimum, helpText)

    field.minimum = minimum
    field.maximum = maximum

    field.createElement = () => {
      let inputGroup = this._uiGen.createFormInputGroup(field.title, 'number', field.helpText).
          attr('min', field.minimum).
          attr('max', field.maximum)
      field.element = inputGroup.find('input')
      return inputGroup
    }
    field.setFromUserInterface = () => {
      field.value = parseInt(field.element.val().toString())
    }
    field.updateUserInterface = () => {
      field.element.val(field.value)
    }
    return this
  }

  /**
   * @param {string} name
   * @param {array} keyValuePairs
   * @param {string} helpText
   * @returns {BrazenConfigurationManager}
   */
  addRadiosGroup(name, keyValuePairs, helpText)
  {
    let field = this._createField(CONFIG_TYPE_RADIOS_GROUP, name, keyValuePairs[0][1], helpText)

    field.options = keyValuePairs

    field.createElement = () => {
      let inputGroup = this._uiGen.createFormRadiosGroupSection(field.title, field.options, field.helpText)
      field.element = inputGroup
      return inputGroup
    }
    field.setFromUserInterface = () => {
      field.value = field.element.find('input:checked').attr('data-value')
    }
    field.updateUserInterface = () => {
      field.element.find('input[data-value="' + field.value + '"]').prop('checked', true).trigger('change')
    }
    return this
  }

  /**
   * @param {string} name
   * @param {int} minimum
   * @param {int} maximum
   * @param {string} helpText
   * @returns {BrazenConfigurationManager}
   */
  addRangeField(name, minimum, maximum, helpText)
  {
    let field = this._createField(CONFIG_TYPE_RANGE, name, {minimum: minimum, maximum: minimum}, helpText)

    field.minimum = minimum
    field.maximum = maximum

    field.createElement = () => {
      let inputGroup = this._uiGen.createFormRangeInputGroup(field.title, 'number', field.minimum, field.maximum,
          field.helpText)
      field.element = inputGroup.find('input')
      return inputGroup
    }
    field.setFromUserInterface = () => {
      field.value = {
        minimum: field.element.first().val(),
        maximum: field.element.last().val(),
      }
    }
    field.updateUserInterface = () => {
      field.element.first().val(field.value.minimum)
      field.element.last().val(field.value.maximum)
    }
    return this
  }

  /**
   * @param {string} name
   * @param {number} rows
   * @param {string|null} helpText
   * @param {ConfigurationManagerRulesetCallback} onTranslateFromUI
   * @param {ConfigurationManagerRulesetCallback} onFormatForUI
   * @param {ConfigurationManagerRulesetCallback} onOptimize
   * @return {BrazenConfigurationManager}
   */
  addRulesetField(name, rows, helpText, onTranslateFromUI = null, onFormatForUI = null, onOptimize = null)
  {
    let field = this._createField(CONFIG_TYPE_RULESET, name, [], helpText)

    field.optimized = null
    field.onTranslateFromUI = onTranslateFromUI ?? field.onTranslateFromUI
    field.onFormatForUI = onFormatForUI ?? field.onFormatForUI
    field.onOptimize = onOptimize ?? field.onOptimize

    field.createElement = () => {
      let inputGroup = this._uiGen.createFormTextAreaGroup(field.title, rows, field.helpText)
      field.element = inputGroup.find('textarea')
      return inputGroup
    }
    field.setFromUserInterface = () => {
      let value = Utilities.trimAndKeepNonEmptyStrings(field.element.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.val(Utilities.callEventHandler(field.onFormatForUI, [field.value], field.value).join('\n'))
    }
    return this
  }

  /**
   * @param {string} name
   * @param {array} keyValuePairs
   * @param {string} helpText
   * @returns {BrazenConfigurationManager}
   */
  addSelectField(name, keyValuePairs, helpText)
  {
    let field = this._createField(CONFIG_TYPE_SELECT, name, keyValuePairs[0][1], helpText)

    field.options = keyValuePairs

    field.createElement = () => {
      let inputGroup = this._uiGen.createFormRadiosGroupSection(field.title, field.options, field.helpText)
      field.element = inputGroup.find('select')
      return inputGroup
    }
    field.setFromUserInterface = () => {
      field.value = field.element.val()
    }
    field.updateUserInterface = () => {
      field.element.val(field.value).trigger('change')
    }
    return this
  }

  /**
   * @param {string} name
   * @param {string} helpText
   * @param {string} defaultValue
   * @returns {BrazenConfigurationManager}
   */
  addTextField(name, helpText, defaultValue = '')
  {
    let field = this._createField(CONFIG_TYPE_TEXT, name, defaultValue, helpText)

    field.createElement = () => {
      let inputGroup = this._uiGen.createFormInputGroup(field.title, 'text', field.helpText)
      field.element = inputGroup.find('input')
      return inputGroup
    }
    field.setFromUserInterface = () => {
      let value = field.element.val()
      field.value = value === '' ? defaultValue : value
    }
    field.updateUserInterface = () => {
      field.element.val(field.value)
    }
    return this
  }

  /**
   * @returns {string}
   */
  backup()
  {
    let backupConfig = this._toStoreObject()
    backupConfig.id = this._syncedLocalStoreId
    return Utilities.objectToJSON(backupConfig)
  }

  /**
   * @param {string} name
   * @returns {JQuery}
   */
  createElement(name)
  {
    return this.getFieldOrFail(name).createElement()
  }

  /**
   * @param {string} configKey
   * @returns {function(*): boolean}
   */
  generateValidationCallback(configKey)
  {
    let validationCallback
    switch (this.getField(configKey).type) {
      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.')
    }
    return validationCallback
  }

  /**
   * @param {string} name
   * @return {ConfigurationField|null}
   */
  getField(name)
  {
    return this._config[this._formatFieldKey(name)]
  }

  /**
   * @param {string} name
   * @return {ConfigurationField}
   */
  getFieldOrFail(name)
  {
    let field = this._config[this._formatFieldKey(name)]
    if (field) {
      return field
    }
    throw new Error('Field named "' + name + '" could not be found')
  }

  /**
   * @param {string} name
   * @returns {*}
   */
  getValue(name)
  {
    return this.getFieldOrFail(name).value
  }

  /**
   * @param {string} name
   * @return {boolean}
   */
  hasField(name)
  {
    return typeof this.getField(name) !== 'undefined'
  }

  /**
   * @param scriptPrefix
   * @return {BrazenConfigurationManager}
   */
  initialize(scriptPrefix)
  {
    this._localStore = new LocalStore(scriptPrefix + 'settings', this._toStoreObject())
    this._localStore.onChange(() => this.updateInterface())

    this._localStoreId = new LocalStore(scriptPrefix + 'settings-id', {id: Utilities.generateId()})
    this._syncedLocalStoreId = this._localStoreId.get().id

    $(document).on('visibilitychange', () => {
      if (!document.hidden && this._syncedLocalStoreId !== this._localStoreId.get().id) {
        this._syncLocalStore(true)
        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)
    let id = backupConfig.id
    delete backupConfig.id

    this._localStore.save(backupConfig)
    this._syncLocalStore(false)
    this._updateLocalStoreId(id)

    return this
  }

  /**
   * @return {BrazenConfigurationManager}
   */
  revertChanges()
  {
    return this._syncLocalStore(false)
  }

  /**
   * @return {BrazenConfigurationManager}
   */
  save()
  {
    this.update()._localStore.save(this._toStoreObject())
    this._updateLocalStoreId()

    return this
  }

  /**
   * @return {BrazenConfigurationManager}
   */
  update()
  {
    let field
    for (let fieldName in this._config) {
      field = this._config[fieldName]
      if (field.element) {
        field.setFromUserInterface()
      }
    }
    return this
  }

  /**
   * @return {BrazenConfigurationManager}
   */
  updateInterface()
  {
    let field
    for (let fieldName in this._config) {
      field = this._config[fieldName]
      if (field.element) {
        field.updateUserInterface()
      }
    }
    return this
  }
}