Configuration management for the Brazen user scripts framework
אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greasyfork.org/scripts/418665/1880213/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 — GM-backed duplicate ledger; reload() migrates legacy index-only blobs to per-id entry keys + normalized { [primaryField]: ids } |
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. Dirty textareas still import on Save as usual.
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 GM storage, rendered via the View Layer bookmarks panel.
**Typical use cases:**
- “Pages I visit often” lists, with optional “current page bookmarked?” UI signals.
```javascript
addBookmarksField('Bookmarks', 'Add pages you visit often.', {
storageKey: 'myscript-bookmarks', // default: {scriptPrefix}bookmarks
storage: 'gm', // default
normalizeBookmarks: (stored) => /* sanitize array */,
showAddButton: true,
pageMatch: { getCurrentUrl, normalizeUrl, onMatchChange, watchSelectors, isActive },
onAdd, onSort, onRemove(id), onNavigate(url),
getRowActions(bookmark), // optional → HTMLElement[] | JQuery[]
})
| Behaviour | Detail |
|---|---|
persist |
false — not in aggregate blob; dedicated GM key via GmStorageDriver |
storage |
'gm' by default |
| Field API | reload(), save(), serializeForBackup(), applyFromBackup() (replace on restore) |
widget |
BrazenBookmarksPanel from createBookmarksPanel |
setFromUserInterface |
No-op — mutations call field.save() from site handlers |
updateUserInterface |
reload() then widget.render(value) |
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:
field.element.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, 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 dedicated driver keys — 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