Brazen Configuration Manager

Configuration management and related UI creation module

目前为 2024-11-10 提交的版本。查看 最新版本

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

  1. // ==UserScript==
  2. // @name Brazen Configuration Manager
  3. // @namespace brazenvoid
  4. // @version 1.6.0
  5. // @author brazenvoid
  6. // @license GPL-3.0-only
  7. // @description Configuration management and related UI creation module
  8. // ==/UserScript==
  9.  
  10. const CONFIG_TYPE_CHECKBOXES_GROUP = 'checkboxes'
  11. const CONFIG_TYPE_FLAG = 'flag'
  12. const CONFIG_TYPE_NUMBER = 'number'
  13. const CONFIG_TYPE_RADIOS_GROUP = 'radios'
  14. const CONFIG_TYPE_RANGE = 'range'
  15. const CONFIG_TYPE_RULESET = 'ruleset'
  16. const CONFIG_TYPE_SELECT = 'select'
  17. const CONFIG_TYPE_TEXT = 'text'
  18.  
  19. class BrazenConfigurationManager
  20. {
  21. /**
  22. * @typedef {{title: string, type: string, element: null|JQuery, value: *, maximum: int, minimum: int, options: string[], helpText: string,
  23. * onFormatForUI: ConfigurationManagerRulesetCallback, onTranslateFromUI: ConfigurationManagerRulesetCallback,
  24. * onOptimize: ConfigurationManagerRulesetCallback, createElement: Function, setFromUserInterface: Function, updateUserInterface: Function,
  25. * optimized?: *}} ConfigurationField
  26. */
  27.  
  28. /**
  29. * @callback ConfigurationManagerRulesetCallback
  30. * @param {*} values
  31. */
  32.  
  33. /**
  34. * @callback ExternalConfigurationChangeCallback
  35. * @param {BrazenConfigurationManager} manager
  36. */
  37.  
  38. /**
  39. * @param {BrazenUIGenerator} uiGenerator
  40. */
  41. constructor(uiGenerator)
  42. {
  43. /**
  44. * @type {{}}
  45. * @private
  46. */
  47. this._config = {}
  48.  
  49. /**
  50. * @type {ExternalConfigurationChangeCallback|null}
  51. * @private
  52. */
  53. this._onExternalConfigurationChange = null
  54.  
  55. /**
  56. * @type {LocalStore}
  57. * @private
  58. */
  59. this._localStore = null
  60.  
  61. /**
  62. * @type {LocalStore}
  63. * @private
  64. */
  65. this._localStoreId = null
  66.  
  67. /**
  68. * @type {number}
  69. * @private
  70. */
  71. this._syncedLocalStoreId = 0
  72.  
  73. /**
  74. * @type BrazenUIGenerator
  75. * @private
  76. */
  77. this._uiGen = uiGenerator
  78. }
  79.  
  80. /**
  81. * @param {BrazenUIGenerator} uiGenerator
  82. * @return {BrazenConfigurationManager}
  83. */
  84. static create(uiGenerator)
  85. {
  86. return new BrazenConfigurationManager(uiGenerator)
  87. }
  88.  
  89. /**
  90. * @param {string} type
  91. * @param {string} name
  92. * @param {*} value
  93. * @param {string|null} helpText
  94. * @return ConfigurationField
  95. * @private
  96. */
  97. _createField(type, name, value, helpText)
  98. {
  99. let fieldKey = this._formatFieldKey(name)
  100. let field = this._config[fieldKey]
  101. if (!field) {
  102. field = {
  103. element: null,
  104. helpText: helpText,
  105. title: name,
  106. type: type,
  107. value: value,
  108. createElement: null,
  109. setFromUserInterface: null,
  110. updateUserInterface: null,
  111. }
  112. this._config[fieldKey] = field
  113. } else {
  114. if (helpText) {
  115. field.helpText = helpText
  116. }
  117. field.value = value
  118. }
  119. return field
  120. }
  121.  
  122. /**
  123. * @param {string} name
  124. * @return {string}
  125. * @private
  126. */
  127. _formatFieldKey(name)
  128. {
  129. return Utilities.toKebabCase(name)
  130. }
  131.  
  132. /**
  133. * @param {boolean} ignoreIfDefaultsSet
  134. * @private
  135. */
  136. _syncLocalStore(ignoreIfDefaultsSet)
  137. {
  138. let field
  139. let storeObject = this._localStore.get()
  140.  
  141. if (!ignoreIfDefaultsSet || !this._localStore.wereDefaultsSet()) {
  142. for (let key in this._config) {
  143.  
  144. field = this._config[key]
  145. if (typeof storeObject[key] !== 'undefined') {
  146.  
  147. field.value = storeObject[key]
  148. if (field.type === CONFIG_TYPE_RULESET) {
  149. field.optimized = Utilities.callEventHandler(field.onOptimize, [field.value])
  150. }
  151. }
  152. }
  153. this.updateInterface()
  154. }
  155. return this
  156. }
  157.  
  158. /**
  159. * @return {{}}
  160. * @private
  161. */
  162. _toStoreObject()
  163. {
  164. let storeObject = {}
  165. for (let key in this._config) {
  166. storeObject[key] = this._config[key].value
  167. }
  168. return storeObject
  169. }
  170.  
  171. /**
  172. * @param id
  173. * @private
  174. */
  175. _updateLocalStoreId(id = null)
  176. {
  177. if (id === null) {
  178. id = Utilities.generateId()
  179. }
  180. this._localStoreId.save({id: id})
  181. this._syncedLocalStoreId = id
  182. }
  183.  
  184. /**
  185. * @param {string} name
  186. * @param {array} keyValuePairs
  187. * @param {string} helpText
  188. * @returns {BrazenConfigurationManager}
  189. */
  190. addCheckboxesGroup(name, keyValuePairs, helpText)
  191. {
  192. let field = this._createField(CONFIG_TYPE_CHECKBOXES_GROUP, name, [], helpText)
  193.  
  194. field.options = keyValuePairs
  195.  
  196. field.createElement = () => {
  197. field.element = this._uiGen.createFormCheckBoxesGroupSection(field.title, field.options, field.helpText)
  198. return field.element
  199. }
  200. field.setFromUserInterface = () => {
  201. field.value = []
  202. field.element.find('input:checked').each((index, element) => {
  203. field.value.push($(element).attr('data-value'))
  204. })
  205. }
  206. field.updateUserInterface = () => {
  207. let elements = field.element.find('input')
  208. for (let key of field.value) {
  209. elements.filter('[data-value="' + key + '"]').prop('checked', true)
  210. }
  211. }
  212. return this
  213. }
  214.  
  215. /**
  216. * @param {string} name
  217. * @param {string} helpText
  218. * @returns {BrazenConfigurationManager}
  219. */
  220. addFlagField(name, helpText)
  221. {
  222. let field = this._createField(CONFIG_TYPE_FLAG, name, false, helpText)
  223.  
  224. field.createElement = () => {
  225. let inputGroup = this._uiGen.createFormInputGroup(field.title, 'checkbox', field.helpText)
  226. field.element = inputGroup.find('input')
  227. return inputGroup
  228. }
  229. field.setFromUserInterface = () => {
  230. field.value = field.element.prop('checked')
  231. }
  232. field.updateUserInterface = () => {
  233. field.element.prop('checked', field.value)
  234. }
  235. return this
  236. }
  237.  
  238. /**
  239. * @param {string} name
  240. * @param {int} minimum
  241. * @param {int} maximum
  242. * @param {string} helpText
  243. * @returns {BrazenConfigurationManager}
  244. */
  245. addNumberField(name, minimum, maximum, helpText)
  246. {
  247. let field = this._createField(CONFIG_TYPE_NUMBER, name, minimum, helpText)
  248.  
  249. field.minimum = minimum
  250. field.maximum = maximum
  251.  
  252. field.createElement = () => {
  253. let inputGroup = this._uiGen.createFormInputGroup(field.title, 'number', field.helpText).
  254. attr('min', field.minimum).
  255. attr('max', field.maximum)
  256. field.element = inputGroup.find('input')
  257. return inputGroup
  258. }
  259. field.setFromUserInterface = () => {
  260. field.value = parseInt(field.element.val().toString())
  261. }
  262. field.updateUserInterface = () => {
  263. field.element.val(field.value)
  264. }
  265. return this
  266. }
  267.  
  268. /**
  269. * @param {string} name
  270. * @param {array} keyValuePairs
  271. * @param {string} helpText
  272. * @returns {BrazenConfigurationManager}
  273. */
  274. addRadiosGroup(name, keyValuePairs, helpText)
  275. {
  276. let field = this._createField(CONFIG_TYPE_RADIOS_GROUP, name, keyValuePairs[0][1], helpText)
  277.  
  278. field.options = keyValuePairs
  279.  
  280. field.createElement = () => {
  281. let inputGroup = this._uiGen.createFormRadiosGroupSection(field.title, field.options, field.helpText)
  282. field.element = inputGroup
  283. return inputGroup
  284. }
  285. field.setFromUserInterface = () => {
  286. field.value = field.element.find('input:checked').attr('data-value')
  287. }
  288. field.updateUserInterface = () => {
  289. field.element.find('input[data-value="' + field.value + '"]').prop('checked', true).trigger('change')
  290. }
  291. return this
  292. }
  293.  
  294. /**
  295. * @param {string} name
  296. * @param {int} minimum
  297. * @param {int} maximum
  298. * @param {string} helpText
  299. * @returns {BrazenConfigurationManager}
  300. */
  301. addRangeField(name, minimum, maximum, helpText)
  302. {
  303. let field = this._createField(CONFIG_TYPE_RANGE, name, {minimum: minimum, maximum: minimum}, helpText)
  304.  
  305. field.minimum = minimum
  306. field.maximum = maximum
  307.  
  308. field.createElement = () => {
  309. let inputGroup = this._uiGen.createFormRangeInputGroup(field.title, 'number', field.minimum, field.maximum,
  310. field.helpText)
  311. field.element = inputGroup.find('input')
  312. return inputGroup
  313. }
  314. field.setFromUserInterface = () => {
  315. field.value = {
  316. minimum: field.element.first().val(),
  317. maximum: field.element.last().val(),
  318. }
  319. }
  320. field.updateUserInterface = () => {
  321. field.element.first().val(field.value.minimum)
  322. field.element.last().val(field.value.maximum)
  323. }
  324. return this
  325. }
  326.  
  327. /**
  328. * @param {string} name
  329. * @param {number} rows
  330. * @param {string|null} helpText
  331. * @param {ConfigurationManagerRulesetCallback} onTranslateFromUI
  332. * @param {ConfigurationManagerRulesetCallback} onFormatForUI
  333. * @param {ConfigurationManagerRulesetCallback} onOptimize
  334. * @return {BrazenConfigurationManager}
  335. */
  336. addRulesetField(name, rows, helpText, onTranslateFromUI = null, onFormatForUI = null, onOptimize = null)
  337. {
  338. let field = this._createField(CONFIG_TYPE_RULESET, name, [], helpText)
  339.  
  340. field.optimized = null
  341. field.onTranslateFromUI = onTranslateFromUI ?? field.onTranslateFromUI
  342. field.onFormatForUI = onFormatForUI ?? field.onFormatForUI
  343. field.onOptimize = onOptimize ?? field.onOptimize
  344.  
  345. field.createElement = () => {
  346. let inputGroup = this._uiGen.createFormTextAreaGroup(field.title, rows, field.helpText)
  347. field.element = inputGroup.find('textarea')
  348. return inputGroup
  349. }
  350. field.setFromUserInterface = () => {
  351. let value = Utilities.trimAndKeepNonEmptyStrings(field.element.val().split(REGEX_LINE_BREAK))
  352. field.value = Utilities.callEventHandler(field.onTranslateFromUI, [value], value)
  353. field.optimized = Utilities.callEventHandler(field.onOptimize, [field.value])
  354. }
  355. field.updateUserInterface = () => {
  356. field.element.val(Utilities.callEventHandler(field.onFormatForUI, [field.value], field.value).join('\n'))
  357. }
  358. return this
  359. }
  360.  
  361. /**
  362. * @param {string} name
  363. * @param {array} keyValuePairs
  364. * @param {string} helpText
  365. * @returns {BrazenConfigurationManager}
  366. */
  367. addSelectField(name, keyValuePairs, helpText)
  368. {
  369. let field = this._createField(CONFIG_TYPE_SELECT, name, keyValuePairs[0][1], helpText)
  370.  
  371. field.options = keyValuePairs
  372.  
  373. field.createElement = () => {
  374. let inputGroup = this._uiGen.createFormRadiosGroupSection(field.title, field.options, field.helpText)
  375. field.element = inputGroup.find('select')
  376. return inputGroup
  377. }
  378. field.setFromUserInterface = () => {
  379. field.value = field.element.val()
  380. }
  381. field.updateUserInterface = () => {
  382. field.element.val(field.value).trigger('change')
  383. }
  384. return this
  385. }
  386.  
  387. /**
  388. * @param {string} name
  389. * @param {string} helpText
  390. * @param {string} defaultValue
  391. * @returns {BrazenConfigurationManager}
  392. */
  393. addTextField(name, helpText, defaultValue = '')
  394. {
  395. let field = this._createField(CONFIG_TYPE_TEXT, name, defaultValue, helpText)
  396.  
  397. field.createElement = () => {
  398. let inputGroup = this._uiGen.createFormInputGroup(field.title, 'text', field.helpText)
  399. field.element = inputGroup.find('input')
  400. return inputGroup
  401. }
  402. field.setFromUserInterface = () => {
  403. let value = field.element.val()
  404. field.value = value === '' ? defaultValue : value
  405. }
  406. field.updateUserInterface = () => {
  407. field.element.val(field.value)
  408. }
  409. return this
  410. }
  411.  
  412. /**
  413. * @returns {string}
  414. */
  415. backup()
  416. {
  417. let backupConfig = this._toStoreObject()
  418. backupConfig.id = this._syncedLocalStoreId
  419. return Utilities.objectToJSON(backupConfig)
  420. }
  421.  
  422. /**
  423. * @param {string} name
  424. * @returns {JQuery}
  425. */
  426. createElement(name)
  427. {
  428. return this.getFieldOrFail(name).createElement()
  429. }
  430.  
  431. /**
  432. * @param {string} configKey
  433. * @returns {function(*): boolean}
  434. */
  435. generateValidationCallback(configKey)
  436. {
  437. let validationCallback
  438. switch (this.getField(configKey).type) {
  439. case CONFIG_TYPE_FLAG:
  440. case CONFIG_TYPE_RADIOS_GROUP:
  441. case CONFIG_TYPE_SELECT:
  442. validationCallback = (value) => value
  443. break
  444. case CONFIG_TYPE_CHECKBOXES_GROUP:
  445. validationCallback = (valueKeys) => valueKeys.length
  446. break
  447. case CONFIG_TYPE_NUMBER:
  448. validationCallback = (value) => value > 0
  449. break
  450. case CONFIG_TYPE_RANGE:
  451. validationCallback = (range) => range.minimum > 0 || range.maximum > 0
  452. break
  453. case CONFIG_TYPE_RULESET:
  454. validationCallback = (rules) => rules.length
  455. break
  456. case CONFIG_TYPE_TEXT:
  457. validationCallback = (value) => value.length
  458. break
  459. default:
  460. throw new Error('Associated config type requires explicit validation callback definition.')
  461. }
  462. return validationCallback
  463. }
  464.  
  465. /**
  466. * @param {string} name
  467. * @return {ConfigurationField|null}
  468. */
  469. getField(name)
  470. {
  471. return this._config[this._formatFieldKey(name)]
  472. }
  473.  
  474. /**
  475. * @param {string} name
  476. * @return {ConfigurationField}
  477. */
  478. getFieldOrFail(name)
  479. {
  480. let field = this._config[this._formatFieldKey(name)]
  481. if (field) {
  482. return field
  483. }
  484. throw new Error('Field named "' + name + '" could not be found')
  485. }
  486.  
  487. /**
  488. * @param {string} name
  489. * @returns {*}
  490. */
  491. getValue(name)
  492. {
  493. return this.getFieldOrFail(name).value
  494. }
  495.  
  496. /**
  497. * @param {string} name
  498. * @return {boolean}
  499. */
  500. hasField(name)
  501. {
  502. return typeof this.getField(name) !== 'undefined'
  503. }
  504.  
  505. /**
  506. * @param scriptPrefix
  507. * @return {BrazenConfigurationManager}
  508. */
  509. initialize(scriptPrefix)
  510. {
  511. this._localStore = new LocalStore(scriptPrefix + 'settings', this._toStoreObject())
  512. this._localStore.onChange(() => this.updateInterface())
  513.  
  514. this._localStoreId = new LocalStore(scriptPrefix + 'settings-id', {id: Utilities.generateId()})
  515. this._syncedLocalStoreId = this._localStoreId.get().id
  516.  
  517. $(document).on('visibilitychange', () => {
  518. if (!document.hidden && this._syncedLocalStoreId !== this._localStoreId.get().id) {
  519. this._syncLocalStore(true)
  520. Utilities.callEventHandler(this._onExternalConfigurationChange, [this])
  521. }
  522. })
  523. return this._syncLocalStore(true)
  524. }
  525.  
  526. /**
  527. * @param {ExternalConfigurationChangeCallback} eventHandler
  528. * @return {BrazenConfigurationManager}
  529. */
  530. onExternalConfigurationChange(eventHandler)
  531. {
  532. this._onExternalConfigurationChange = eventHandler
  533. return this
  534. }
  535.  
  536. /**
  537. * @param {string} backedUpConfiguration
  538. */
  539. restore(backedUpConfiguration)
  540. {
  541. let backupConfig = Utilities.objectFromJSON(backedUpConfiguration)
  542. let id = backupConfig.id
  543. delete backupConfig.id
  544.  
  545. this._localStore.save(backupConfig)
  546. this._syncLocalStore(false)
  547. this._updateLocalStoreId(id)
  548.  
  549. return this
  550. }
  551.  
  552. /**
  553. * @return {BrazenConfigurationManager}
  554. */
  555. revertChanges()
  556. {
  557. return this._syncLocalStore(false)
  558. }
  559.  
  560. /**
  561. * @return {BrazenConfigurationManager}
  562. */
  563. save()
  564. {
  565. this.update()._localStore.save(this._toStoreObject())
  566. this._updateLocalStoreId()
  567.  
  568. return this
  569. }
  570.  
  571. /**
  572. * @return {BrazenConfigurationManager}
  573. */
  574. update()
  575. {
  576. let field
  577. for (let fieldName in this._config) {
  578. field = this._config[fieldName]
  579. if (field.element) {
  580. field.setFromUserInterface()
  581. }
  582. }
  583. return this
  584. }
  585.  
  586. /**
  587. * @return {BrazenConfigurationManager}
  588. */
  589. updateInterface()
  590. {
  591. let field
  592. for (let fieldName in this._config) {
  593. field = this._config[fieldName]
  594. if (field.element) {
  595. field.updateUserInterface()
  596. }
  597. }
  598. return this
  599. }
  600. }