Configuration management for the Brazen user scripts framework
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/418665/1909478/Brazen%20Framework%20-%20Configuration%20Manager.js
Typed settings schema, IndexedDB persistence via IndexedDB Storage, Reactor-backed cross-tab sync, backup/restore, and DOM factories bound to View Layer widgets.
Greasy Fork: Configuration Manager · Requires: Utilities, View Layer · Used by: Framework core
GM_setValue blobs for settings in apps. Prefer defining fields here so the framework can apply/save/reset consistently and include values in backups.Define fields in your app’s constructor, then mount them into the settings panel during UI composition.
// constructor (define schema)
this._configurationManager.registerFieldSeed('enable-feature', true)
this._configurationManager
.addFlagField('enable-feature')
.setTitle('Enable Feature')
.setHelp('Turns the feature on/off.')
this._configurationManager.registerFieldSeed('tag-blacklist', {
templateId: 'tag-blacklist',
config: {autoSort: true, sortMode: 'natural-asc', groupingEnabled: false, hideTagTypes: false},
})
this._configurationManager
.addRulesetField('tag-blacklist')
.setTitle('Tag Blacklist')
.setRows(15)
.setHelp('One rule per line.')
.setSortRules(true)
.setGroupingAvailable(true)
// UI composition (mount controls)
this._userInterface = [
this._uiGen.createTabsSection(['Filters'], [
this._uiGen.createTabPanel('Filters', true).append([
this._configurationManager.createElement('enable-feature'),
this._configurationManager.createElement('tag-blacklist'),
]),
]),
]
new BrazenConfigurationManager(scriptPrefix, uiGenerator, tagSelectorGenerator)
| Parameter | Role |
|---|---|
scriptPrefix |
Prefix for storage keys (per driver) |
uiGenerator |
BrazenViewLayer instance |
tagSelectorGenerator |
(tag: string) => string — optional site hook (legacy selector highlights removed) |
Framework exposes this as this._configurationManager. Register fields in the subclass constructor; call initialize() during init() before UI build.
Each field has:
field.key) used for persistence, lookups, docking relationships, and backup/restore.field.title) used only for UI labels.The value you pass as the first argument to add*Field(key) is normalized via Utilities.toKebabCase(...) and stored as field.key.
To decouple label text from persistence, use kebab-case keys in code and set the UI label via setTitle(...):
const FILTER_TAG_BLACKLIST = 'tag-blacklist'
this._configurationManager.addRulesetField(FILTER_TAG_BLACKLIST).
setTitle('Tag Blacklist').
setRows(15).
setHelp('One rule per line.')
What it does: You define a stable settings schema by adding typed fields. Fields become the single source of truth for UI binding, persistence, validation gating, and backup/restore.
How it works (developer-relevant):
add*Field(...) returns the field object so you can chain configuration (setHelp, setRows, setGroupingAvailable, etc.).registerFieldSeed(key, value) or registerFieldSeeds({...}) before or after field registration; _seedAllFields() (fresh install) and schema migration handlers write them to IndexedDB once. Reads return stored values verbatim — no runtime default merge.settings document; ruleset rows in rulesetEntries; ledger in ledgerEntries. Legacy GM/local import runs once during setup when present.| Constant | add* method |
Stored value type |
UI |
|---|---|---|---|
CONFIG_TYPE_FLAG |
addFlagField(name) |
boolean |
Checkbox |
CONFIG_TYPE_TEXT |
addTextField(name) |
string |
Text input; empty → seed default on read |
CONFIG_TYPE_NUMBER |
addNumberField(name, min, max) |
number |
Number input |
CONFIG_TYPE_COLOR |
addColorField(name) |
color string | Color input |
CONFIG_TYPE_RANGE |
addRangeField(name, min, max) |
{ minimum, maximum } |
Two number inputs |
CONFIG_TYPE_SELECT |
addSelectField(name, pairs) |
option value | See known limitation in spec |
CONFIG_TYPE_CHECKBOXES_GROUP |
addCheckboxesGroup(name, pairs) |
string[] of checked data-values |
Checkbox group |
CONFIG_TYPE_RADIOS_GROUP |
addRadiosGroup(name, pairs) |
selected data-value |
Radio group |
CONFIG_TYPE_RULESET |
addRulesetField(name) |
string[] lines / IDB rows |
Ruleset panel (createRulesetPanel); use .setTemplate('bookmarks') for bookmarks |
CONFIG_TYPE_ACTION |
addActionField(name) |
n/a (persist: false) |
Form button or dock-only action |
CONFIG_TYPE_LEDGER |
addLedgerField(name) |
self-managed id set (not in aggregate blob) | No UI — IndexedDB ledgerEntries |
All add*Field methods return the field object. Chain setHelp, setRows, bookmark/ledger/dock setters (setDockButton, applyDockTemplate, setDockSlideOut). Manager methods (onConfigurationChange, setDockActive, …) return the manager.
applyDockTemplate(name, args?) — named recipes (tagBlacklist, exploredTags, autoNextPage, defaultTags, resolutionFilter, hideOlderPosts, invertedFiltersMaster, skipDuplicates, hideDownloaded; plus escape-hatch flagToggle). Records field.dock.templateName.
this._configurationManager.registerFieldSeed('enable-feature', true)
this._configurationManager.addFlagField('enable-feature').setHelp('…')
this._configurationManager.addRulesetField('tag-blacklist').setRows(15).setHelp('…').setSortRules(true)
this._configurationManager.addRulesetField('filename-tag-substitutions').
setSubstitutionComposer({
normalize: (tag) => normalizeTag(tag),
subjectPlaceholder: 'Subject',
aliasPlaceholder: 'Alias',
})
setSubstitutionComposer implies setReadOnly(true). Adds call setTagSubstitution; Save does not re-import the textarea when IndexedDB is active.
What it does: Ruleset fields turn user-edited textarea lines into a cleaned list of rules, optionally sorted/deduplicated, and optionally optimized into a runtime-friendly structure.
Apply/Save pipeline (setFromUserInterface, used by Apply/Save):
On Apply/Save (setFromUserInterface):
REGEX_LINE_BREAK; trim empty lines.sortRules: true — locale-aware sort (string lines only).onTranslateFromUI(values) — normalize (optional; may return a non-array map, e.g. text sanitization).onOptimize(values) — store result in field.optimized (filters read this at runtime).Registry / ruleset fields: panel widgets (createRulesetPanel) persist rows in rulesetEntries; sidebar toggles and template APIs write the same store. Row mutations notify via notifyRulesetMutation() (Reactor ruleset-mutation Command on foreign tabs — RulesetMutationBus removed in 8.0.0). Header entry count comes from entryCountLabel + resolveEntryCount → rulesetEntries.countForField.
addRulesetField(key).setTemplate('tag-blacklist' | 'explored-tags' | …) registers a ruleset field with template-driven panel UI and compile hooks.
Ruleset subject copy:
Each ruleset field names what it stores with a singular subject noun. Default is rule; override via setRulesetSubject('bookmark') or a template subject on RulesetTemplateRegistry. Configuration Manager derives panel empty/search/sort strings, add-entry repeater, batch submit label, and detail pane titles from the subject (pluralized where needed).
| Setter | Role |
|---|---|
setRulesetSubject(text) |
Singular noun for entries (default rule; bookmarks template uses bookmark) |
setSortHelpText(text) |
Optional toolbar ⇅ tooltip override (default Sort {plural}) |
Derived examples (subject rule / bookmark):
| String | rule | bookmark |
|---|---|---|
| Empty state | No rules yet. | No bookmarks yet. |
| Add entry (repeater / + tooltip) | Add Rule | Add Bookmark |
| Batch submit | Add Rules | Add Bookmarks |
| Create detail title | {title} - Add Rules |
{title} - Add Bookmarks |
| Edit detail title | {title} - Edit Rule |
{title} - Edit Bookmark |
Detail pane CRUD: Create opens the multi-form shell (scrollable bordered cards + batch submit). Edit opens a single bordered form + Save. Empty create entries are skipped on submit.
Per-rule API (programmatic edits):
Every ruleset field exposes methods for programmatic add/remove without going through the textarea. Callers get the field via getField(name) and invoke these directly; side effects (save(), compliance refresh, UI refresh elsewhere) remain the caller's responsibility.
| Method | Role |
|---|---|
onMatchRule(storedRule, target) |
Optional matcher callback; default is strict equality (storedRule === target) |
setRules(lines) |
Canonical write: onTranslateFromUI → optional sortRules (arrays only) → value → onOptimize → updateUserInterface |
findRuleIndex(target) |
Index of first matching rule, or -1 |
hasRule(target) |
Membership test via onMatchRule |
addRule(rule, target?) |
Upsert: remove matches for target (defaults to rule), append rule, then setRules |
removeRule(target) |
Drop all matches, then setRules |
toggleRule(rule, target?) |
Add or remove via hasRule |
clearRules() |
setRules([]) |
setRules does not deduplicate (preserves programmatic edits). IDB-backed ruleset fields dedupe natively when rows are added (RulesetEntryRepository.add compares trimmed rawLine). Legacy array rulesets still dedupe via _deduplicateRulesetRules on Apply/Save.
Tag rule syntax (one rule per line):
& — AND (all tags in segment must match)| — OR within a segment// — comment suffix (ignored after split)Optimized output: array of arrays (each inner array = one conjunctive rule). Sorted shortest-first.
addRulesetField('bookmarks').setTemplate('bookmarks')What it does: Bookmarks are a ruleset template backed by IndexedDB rulesetEntries, rendered via View Layer createRulesetPanel. Legacy GM/local {prefix}bookmarks import once during setup via BrazenLegacyImporter when legacyScriptPrefix is set; migrateBookmarksToRuleset copies legacy bookmarks store rows.
Typical use cases:
this._configurationManager.registerFieldSeed('bookmarks', { autoSort: true, sortMode: 'date-desc', hideTagTypes: false })
this._configurationManager.addRulesetField('bookmarks').
setTemplate('bookmarks').
setTitle('Bookmarks').
setHelp('Add pages you visit often.').
setShowAddButton(true).
setPageMatch({ getCurrentUrl, normalizeUrl, onMatchChange, watchSelectors, isActive }).
setOnRemove((id) => { /* … */ }).
setAttributeActionsConfig({ host, tokenize, normalize, blacklist, explore, … })
Framework-owned blacklist/explore remediation on bookmark rows is enabled via setAttributeActionsConfig({ host: frameworkInstance, tokenize, … }). CM composes those buttons (and tag-cache prefetch) before any consumer getRowActions / onBeforeRender.
| Behaviour | Detail |
|---|---|
persist |
false — not in aggregate settings blob; IndexedDB rulesetEntries under the bookmarks fieldKey |
| Field API | reload(), add/edit/remove via ruleset panel or rulesetEntries repo |
widget |
Ruleset panel from createRulesetPanel (bookmarks template) |
setFromUserInterface |
No-op — panel CRUD writes IDB directly |
updateUserInterface |
Re-renders _rulesetRows + checkPageMatch |
reload() |
Loads ruleset page cache + rebuilds bookmark tag index |
createElement (mounting fields)What it does: Builds the View Layer DOM for a field, stores a reference on the field (field.element), and returns the mountable node.
this._configurationManager.createElement('My Setting') // display name
Invokes the field's createElement() which:
HTMLElement).field.element (HTMLElement|null).null when the dock is active and the field is docked (panel XOR dock — empty sentinel is null, not an empty jQuery set).createDockElement(name) similarly returns HTMLElement|null and stores on field.dockElement.
Fields without createElement called remain memory-only until mounted.
Each field implements:
| Callback | When |
|---|---|
createElement() |
First mount |
setFromUserInterface() |
Apply/Save — read DOM → field.value (+ ruleset optimize) |
updateUserInterface() |
Load/reset/tab switch — write field.value → DOM |
this._configurationManager.getValue('My Setting')
this._configurationManager.getField('My Setting') // ConfigurationField | null
this._configurationManager.getFieldOrFail('My Setting') // throws if missing
this._configurationManager.hasField('My Setting')
ConfigurationField properties: title, type, element (HTMLElement|null), dockElement (HTMLElement|null), value, optimized?, help, minimum, maximum, options, persist, widget, ruleset callbacks.
IndexedDB is mandatory for the current stack — initialize() opens the per-script database via BrazenStorageRepositories. Legacy local/GM aggregate blobs import once during setup when present.
| Store | Role |
|---|---|
settings |
Scalar config fields (camelCase properties) |
rulesetEntries |
Ruleset rows (bookmarks, blacklist, ignore, …) |
ledgerEntries |
Download duplicate ledger |
meta |
revisionId, domainConfigSeq / domainTagsSeq / domainLedgerSeq, setup flags |
Self-managed fields (persist: false, e.g. ledger, bookmarks ruleset) read/write their stores directly and are included in zip backup export.
initialize() (IndexedDB)brazen-{prefix}).getPendingMigrationPlan() first. When schemaTooNew is true, shows schema-too-new panel (Retry / Reset database). When consentRequired is true, shows consent panel (Start update / Reset database).createPreMigrationSafetyBackup) — zip manifest version 3 (legacy rollback shape; may include legacy tagRules / tagRuleSets).onMigrationProgress({ phase, label, detail?, current?, total?, indeterminate? }).hydrateSearchDefaultsFromStorage(fieldKeys?) (search URL merge) and hydrateBootFieldsFromStorage() (ruleset compile, TagRuntime warm, ruleset/ledger reload) after dock embed.getMigrationError()); Framework shows migration failure panel.| Method | Behaviour |
|---|---|
update() |
All mounted fields: setFromUserInterface() |
save() |
Persist mounted fields; when Reactor sender is wired, routes via config-save Command |
writeSetting(fieldKey, value) |
Direct IDB put; may route via write-setting Command cross-tab |
revertChanges() |
Re-read store into fields + updateInterface() |
updateInterface() |
All mounted fields: updateUserInterface() |
Framework button wiring:
update() + re-run compliance (no disk).save().revertChanges() + compliance.User export: backup() downloads {scriptPrefix}backup.zip with manifest CONFIG_BACKUP_VERSION (5) — one JSON file per store + meta.json / settings.json / ruleset stores. Large tables may use .partNNNN.json sidecars.
Import: v5 zip (primary); v3 IDB zip; v2 driver bundle; v1 flat JSON — via restore(response).
Ledger restore merges by postId; bookmarks/ruleset rows restore from rulesetEntries.json.
Domain revision cursors: meta.domainConfigSeq, domainTagsSeq, domainLedgerSeq — foreign sync reloads only domains that advanced (ledger-only bumps skip ruleset UI rebuild).
Reactor (8.0.0):
| API | Role |
|---|---|
getConfigBus() |
Shared BrazenEventBus |
setCommandSender |
Coordinator write-through Commands |
notifyConfigurationChange / notifyRulesetMutation() |
Publish config-change / ruleset-mutation Commands |
setConfigUiReactionDelegate |
Local config-ui repaint after foreign sync |
setRuntimePipelineChangeDelegate |
Read-only DM refresh on queue/state writes (not full config-ui fan-out) |
getRevisionAtoms() |
Public { config, tags, ledger } stamps for compliance UI effects |
onConfigurationChange(handler) — Framework registers (manager, source, local, detail) => void. Same-tab focus after your own writes is a no-op.
Tag discovery bulk reset: resetAllTagsDiscovered(onProgress?) — delegates to TagRepository; clears TagRuntime RAM; _commitTagFieldChange() on success. Framework Toolbox → Reset Tag Discovery.
generateValidationCallbackgenerateValidationCallback(configKey) → default enable checks:
| Type | Active when |
|---|---|
| flag / radios / select | truthy value |
| checkboxes | length > 0 |
| number | value > 0 |
| range | `minimum > 0 \ |
| ruleset / text | length > 0 |
Used by framework _addItemComplianceFilter when no custom validate callback is supplied.
init() builds UI.createElement inside tab panels.addFlagField + ruleset).addLedgerField) use IndexedDB stores — no ad-hoc GM_setValue in apps.registerFieldSeed(s); _seedAllFields() / schema migrations write IDB once; registration never upserts defaults; new defaults for existing installs need a schema version bump + handler. See spec Config defaults ownership + PR checklist.#bv-ui userScript reference if tabs need updateInterface from custom widgets.Next in stack: optional Tag Query Engine → optional Paginator / Subscriptions Loader → Framework core.
addFlagField, addTextField, addNumberField, addColorField, addRangeField, addSelectField, addCheckboxesGroup, addRadiosGroup, addRulesetField, addActionField, addLedgerFieldcreateElement(name)getValue, getField, getFieldOrFail, hasFieldinitialize, getPendingMigrationPlan, getSchemaVersionConflict, createPreMigrationSafetyBackup, getPreMigrationBackupFilenameHint, getMigrationError, hydrateSearchDefaultsFromStorage, hydrateBootFieldsFromStorage, update, save, writeSetting, revertChanges, updateInterface, refreshSettingCacheFromStoragegetConfigBus, setCommandSender, persistMountedSettingsCoordinator, setConfigUiReactionDelegate, setRuntimePipelineChangeDelegate, notifyConfigurationChange, notifyRulesetMutation, getRevisionAtoms, reloadBookmarkFields, onConfigurationChangebackup, restoregenerateValidationCallback