Greasy Fork 支持简体中文。

Brazen Configuration Manager

Configuration management and related UI creation module

目前為 2023-09-03 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/418665/1244654/Brazen%20Configuration%20Manager.js

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