Configuration management for the Brazen user scripts framework
Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta
// @require https://update.greasyfork.org/scripts/418665/1884358/Brazen%20Framework%20-%20Configuration%20Manager.js
Typed settings schema, pluggable storage drivers (localStorage + Tampermonkey GM), 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
.addFlagField('enable-feature')
.setTitle('Enable Feature')
.setDefault(true)
.setHelpText('Turns the feature on/off.')
this._configurationManager
.addRulesetField('tag-blacklist')
.setTitle('Tag Blacklist')
.setRows(15)
.setHelpText('One rule per line.')
.setSortRules(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 — used by addTagRulesetField / _optimizeTagRuleset |
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).
setHelpText('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 (setDefault, setHelpText, setRows, etc.).field.attachStorage('gm') when a value should live in GM storage.persist: false) and use driver primitives directly (bookmarks, ledger).| Constant | add* method |
Stored value type |
UI |
|---|---|---|---|
CONFIG_TYPE_FLAG |
addFlagField(name) |
boolean |
Checkbox |
CONFIG_TYPE_TEXT |
addTextField(name) |
string |
Text input; empty → default on read (setDefault) |
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 |
Textarea |
CONFIG_TYPE_ACTION |
addActionField(name) |
n/a (persist: false) |
Form button or dock-only action |
CONFIG_TYPE_BOOKMARKS |
addBookmarksField(name) |
array (self-managed on gm) |
Bookmarks panel |
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 setHelpText, setDefault, setRows, bookmark/ledger/dock setters (setDockButton, applyDockTemplate, setDockSlideOut), then field.attachStorage('local'|'gm') as needed (default driver: local). Manager methods (onExternalConfigurationChange, setDockActive, …) return the manager.
applyDockTemplate(name, args?) — named recipes (tagBlacklist, exploredTags, autoDownload, removeMediaOnDownload, singleDownload, autoNextPage, defaultTags, resolutionFilter, hideOlderPosts, invertedFiltersMaster, skipDuplicates, hideDownloaded; plus escape-hatch flagToggle). Records field.dock.templateName. Templates autoDownload, removeMediaOnDownload, and singleDownload set immediateDownload; when config key only-show-download-manager is on, _evaluateDockInclude hides them wherever the app placed them on the rail.
this._configurationManager.addFlagField('Enable Feature').setHelpText('…').setDefault(true)
this._configurationManager.addRulesetField('Tag Blacklist').setRows(15).setHelpText('…').setSortRules(true)
Filename substitutions can use a controlled composer (read-only list + Subject → Alias add row):
```javascript
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.
---
### Rulesets: the Apply/Save pipeline and optimized output
**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`):
1. Split textarea by `REGEX_LINE_BREAK`; trim empty lines.
2. Optional `sortRules: true` — locale-aware sort (string lines only).
3. `onTranslateFromUI(values)` — normalize (optional; may return a non-array map, e.g. text sanitization).
4. Deduplicate when the value is an array (case-sensitive; first occurrence kept). Non-arrays pass through. Refreshes the textarea when array duplicates are removed.
5. `onOptimize(values)` — store result in `field.optimized` (filters read this at runtime).
**Registry fields** (`TAG_FIELD_SPEC`): textarea edits set `uiDirty`. While clean, Apply keeps the IDB/cache projection (does not read a stale textarea), and Save skips re-import so sidebar toggles are not wiped. Save snapshots dirty keys before `update()` (which clears `uiDirty` after reading the textarea) so dirty textareas still import to IndexedDB.
On **interface refresh** (`updateUserInterface`):
- `onFormatForUI(value)` → join with `\n` for textarea display; clears `uiDirty`.
`addTagRulesetField(key, useSelectors, rows, helpText?, formatter?, sortRules?)` wraps `addRulesetField` with default `onOptimize` → `_optimizeTagRuleset`.
**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); `setFromUserInterface` (Apply/Save) still deduplicates array rulesets via `_deduplicateRulesetRules`.
**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.
---
### Bookmarks: `addBookmarksField`
**What it does:** A self-managed bookmarks field backed by IndexedDB `bookmarks`, rendered via the View Layer bookmarks panel. Legacy GM/local `{prefix}bookmarks` import once during setup via `BrazenLegacyImporter` when `legacyScriptPrefix` is set.
**Typical use cases:**
- “Pages I visit often” lists, with optional “current page bookmarked?” UI signals.
```javascript
this._configurationManager.addBookmarksField('bookmarks').
setTitle('Bookmarks').
setHelpText('Add pages you visit often.').
setNormalizeBookmarks((stored) => /* sanitize array */).
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 bookmarks store |
| Field API | reload(), repository add/remove/sort via Framework helpers |
widget |
BrazenBookmarksPanel from createBookmarksPanel |
setFromUserInterface |
No-op — mutations call field.save() from site handlers |
updateUserInterface |
Re-renders cached _bookmarkRows + checkPageMatch (no IDB round-trip) |
reload() |
Assigns _bookmarkRows after listAll; discards stale overlapping results via _reloadGeneration |
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?, helpText, minimum, maximum, options, persist, widget, ruleset callbacks.
Each field has a storage driver key (local by default). Simple fields persist in a per-driver aggregate blob; self-managed types (persist: false, e.g. ledger, bookmarks) use driver primitives directly and participate in backup v2 when they implement serializeForBackup.
| Store | Driver | Key | Content |
|---|---|---|---|
| Settings | local |
{scriptPrefix}settings |
JSON object: kebab keys → values (skips persist: false) |
| Sync id | local |
{scriptPrefix}settings-id |
{ id: number } — bumps on save |
| Settings | gm |
{scriptPrefix}settings |
Same shape in GM namespace (lazy; only when a field uses gm) |
| Sync id | gm |
{scriptPrefix}settings-id |
Same in GM namespace |
initialize()local driver stores (LocalStore)._syncDriverStores('local') — merge disk into fields; rulesets run onOptimize.onChange on settings store → updateInterface().visibilitychange / pageshow — if tab visible and meta.revisionId differs from the locally tracked cursor, tagRuntime.clearCache(), re-sync + notify (local: false). Local writes advance the cursor immediately so same-tab minimize/maximize does not wipe unsaved panel edits.| Method | Behaviour |
|---|---|
update() |
All mounted fields: setFromUserInterface() (rulesets: trim → optional sort → onTranslateFromUI → dedupe rules case-sensitively → onOptimize) |
save() |
update() → write store → bump sync id |
revertChanges() |
Re-read store into fields + updateInterface() |
updateInterface() |
All mounted fields: updateUserInterface() |
Framework button wiring:
update() + re-run compliance (no disk).save().revertChanges() + compliance.backup() downloads {scriptPrefix}backup.json:
{
"version": 2,
"id": 123456789,
"drivers": {
"local": {
"tag-blacklist": ["rule1", "rule2"],
"pagination-threshold": 20
},
"gm": {
"download-ledger": { "postIds": ["123", "456"] },
"bookmarks": [{ "id": "…", "label": "…", "tags": "…", "url": "…" }]
}
}
}
Legacy flat backups (no version / drivers) still import as local values.
restore(response) — fetch/Response with JSON body; each field's applyFromBackup handles semantics (ledger merges ids; bookmarks replace list); restores values + sync id; alerts success/failure. Legacy v1 backups without a bookmarks key leave local bookmarks untouched.
onExternalConfigurationChange(handler) — (manager) => void when another tab saved. Framework registers compliance re-run.
Revision cursor: BrazenStorageRepositories accepts optional onRevisionBump(revisionId). Configuration Manager wires it so every local meta.bumpRevision() advances _syncedRevisionId immediately. Same-tab visibilitychange after your own writes is then a no-op and does not wipe unsaved panel edits. Foreign revision sync clears TagRuntime RAM so sidebar / filters refetch attributes instead of reusing stale ensureNames cache entries.
Tag runtime warm cache: After tag registry writes, _commitTagFieldChange calls TagRuntime.warmCache() (marks ready) and relies on _cacheEntry / ensureNames — it does not reload the full tags table into RAM.
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, addBookmarksField) use IndexedDB stores — no ad-hoc GM_setValue in apps.#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, addBookmarksField, addLedgerField, addTagRulesetFieldcreateElement(name)getValue, getField, getFieldOrFail, hasFieldinitialize, update, save, revertChanges, updateInterfacebackup, restore, onExternalConfigurationChangegenerateValidationCallback