Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Main features:
- Safe patching API (`before`, `after`, `instead`, `unpatchAll`)
- Per-plugin persistent storage
- Injected settings UI inside Fluxer settings panel
- Dynamic plugin settings integration (`getSettingsSchema` + `setSettingValue`) with live apply
- Plugin store support (remote index + install/remove)

Docs index: [`docs/README.md`](./docs/README.md)
Expand Down Expand Up @@ -92,4 +93,4 @@ If Fluxer only has packed `app.asar` without `app.asar.unpacked` preload, inject
- `bridge-nw/`: Bridge app and local bridge script
- `scripts/lib/fluxer-injector-utils.js`: Core patch/inject logic
- `src/`: Runtime core used by injected BetterFluxer
- `plugins/` and/or `nw/plugins/`: Default plugins loaded by injector
- `MyPlugins/`, `plugins/`, and/or `nw/plugins/`: Plugin sources loaded by injector/runtime
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [Runtime API](./runtime-api.md)
- [Plugin Layouts](./plugin-layouts.md)
- [Plugin Settings Integration](./plugin-settings.md)
- [Plugin Store](./plugin-store.md)
- [Core Classes](./core-classes.md)
- [UI Classes](./ui-classes.md)
Expand Down
9 changes: 9 additions & 0 deletions docs/plugin-layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,12 @@ export default {
```

`ctx` receives the BetterFluxer API (`logger`, `storage`, `patcher`, `settings`, `ui`, `classes`, `app`).

## Dynamic Settings (Recommended)

Plugins can expose settings in BetterFluxer settings UI by implementing:

- `getSettingsSchema()`
- `setSettingValue(key, value)`

See full guide: [Plugin Settings Integration](./plugin-settings.md)
78 changes: 78 additions & 0 deletions docs/plugin-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Plugin Settings Integration

BetterFluxer can render plugin-defined settings directly inside the built-in **Better Fluxer > Settings** panel.

This supports live updates without reinjecting or switching chats.

## Supported plugin methods

Implement these in your plugin class/object:

```js
getSettingsSchema() {
return {
title: "My Plugin",
description: "Optional description",
controls: [
// range
{ key: "size", type: "range", label: "Size", min: 1, max: 10, step: 1, value: 4, suffix: "x" },
// boolean
{ key: "enabled", type: "boolean", label: "Enabled", value: true },
// text (fallback type)
{ key: "tag", type: "text", label: "Tag", value: "default" }
]
};
}

setSettingValue(key, value) {
// update in-memory
// persist using this.api.storage.set(...)
// re-apply changes immediately
return { ok: true };
}
```

## Control types

- `range`
- uses slider UI
- fields: `min`, `max`, `step`, `value`, optional `suffix`
- `boolean`
- uses checkbox/toggle UI
- fields: `value`
- `text` (or unknown type)
- uses text input UI
- fields: `value`

## Live update behavior

When a control changes, BetterFluxer will:

1. Call `setSettingValue(key, value)`
2. Try optional plugin hooks (if implemented):
- `onSettingChanged(key, value, result)`
- `refresh()`
- `processDocument(document)`
3. Emit runtime/browser events:
- runtime: `plugin:setting:changed`
- window: `betterfluxer:plugin-setting-changed`

## Persistence recommendation

Use per-plugin storage:

```js
// load
const value = this.api.storage.get("myKey", defaultValue);
// save
this.api.storage.set("myKey", value);
```

Load persisted values in `start()` (or a `loadConfig()` helper), and return those current values from `getSettingsSchema()`.

## Where this works

- `MyPlugins/*`
- `nw/plugins/*`

Any enabled plugin that exposes `getSettingsSchema()` appears automatically in BetterFluxer settings.
18 changes: 18 additions & 0 deletions docs/runtime-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ Class constructors for building custom modules.
### `openSettings(tabName?: "plugins" | "settings"): void`
Opens BetterFluxer settings content inside Fluxer's settings right pane.

### Plugin-defined settings UI
Enabled plugins that implement:
- `getSettingsSchema()`
- `setSettingValue(key, value)`

are auto-rendered in BetterFluxer settings.

On change, BetterFluxer calls:
1. `setSettingValue(key, value)`
2. optional hooks if present:
- `onSettingChanged(key, value, result)`
- `refresh()`
- `processDocument(document)`

and emits:
- runtime event: `plugin:setting:changed`
- window event: `betterfluxer:plugin-setting-changed`

### `loadStoreIndex(): Promise<Array<{ id: string, name: string, url: string }>>`
Fetches remote plugin index from store URL.

Expand Down
72 changes: 72 additions & 0 deletions nw/plugins/DiscordRPCEmu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,78 @@ module.exports = class DiscordRPCEmuPlugin {
this.api.logger.info("DiscordRPCEmu disabled.");
}

getSettingsSchema() {
return {
title: "Discord RPC Emu",
description: "Bridge and status sync behavior.",
controls: [
{ key: "statusSyncEnabled", type: "boolean", label: "Enable status sync", value: this.statusSyncEnabled },
{ key: "localBridgeEnabled", type: "boolean", label: "Enable local bridge", value: this.localBridgeEnabled },
{ key: "debugDetection", type: "boolean", label: "Enable debug detection logs", value: this.debugDetection },
{ key: "localBridgePort", type: "text", label: "Local bridge port (1024-65535)", value: String(this.localBridgePort) },
{ key: "localBridgeToken", type: "text", label: "Local bridge token", value: this.localBridgeToken || "" },
{ key: "testBridgeReconnect", type: "button", label: "Restart local bridge", note: "Reconnects bridge using current settings." },
{ key: "testSyncNow", type: "button", label: "Sync now-playing now", note: "Runs an immediate status sync." },
{ key: "testStatusState", type: "button", label: "Get status sync state", note: "Returns current status sync diagnostics." }
]
};
}

setSettingValue(key, value) {
const k = String(key || "");
if (k === "testBridgeReconnect") {
this.stopBridge();
if (this.localBridgeEnabled) this.startBridge();
return this.getBridgeState();
}
if (k === "testSyncNow") {
return this.syncNowPlayingNow()
.then(() => this.getStatusSyncState())
.catch((err) => ({ ok: false, error: err && err.message ? err.message : "sync failed" }));
}
if (k === "testStatusState") {
return this.getStatusSyncState();
}
if (k === "statusSyncEnabled") {
this.statusSyncEnabled = Boolean(value);
if (this.statusSyncEnabled) this.startStatusSync();
else this.stopStatusSync();
}
if (k === "localBridgeEnabled") {
this.localBridgeEnabled = Boolean(value);
this.stopBridge();
if (this.localBridgeEnabled) this.startBridge();
}
if (k === "debugDetection") {
this.debugDetection = Boolean(value);
}
if (k === "localBridgePort") {
const n = Number(value);
if (Number.isFinite(n) && n >= 1024 && n <= 65535) {
this.localBridgePort = Math.round(n);
this.stopBridge();
if (this.localBridgeEnabled) this.startBridge();
}
}
if (k === "localBridgeToken") {
this.localBridgeToken = String(value || "");
}
try {
this.api.storage.set("statusSyncEnabled", this.statusSyncEnabled);
this.api.storage.set("localBridgeEnabled", this.localBridgeEnabled);
this.api.storage.set("debugDetection", this.debugDetection);
this.api.storage.set("localBridgePort", this.localBridgePort);
this.api.storage.set("localBridgeToken", this.localBridgeToken);
} catch (_e) {}
return {
statusSyncEnabled: this.statusSyncEnabled,
localBridgeEnabled: this.localBridgeEnabled,
debugDetection: this.debugDetection,
localBridgePort: this.localBridgePort,
localBridgeToken: this.localBridgeToken
};
}

debugLog(message, extra) {
if (!this.debugDetection) return;
if (typeof extra !== "undefined") {
Expand Down
46 changes: 46 additions & 0 deletions nw/plugins/DisplaySourceFix/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ module.exports = class DisplaySourceFixPlugin {
sources: [],
at: 0
};
this.includeScreens = true;
this.includeWindows = true;
}

getElectronApi() {
Expand All @@ -27,6 +29,7 @@ module.exports = class DisplaySourceFixPlugin {
return [];
}
try {
this.cache.types = this.getSourceTypes();
const sources = await electronApi.getDesktopSources(this.cache.types);
if (Array.isArray(sources)) {
this.cache.sources = sources;
Expand Down Expand Up @@ -256,6 +259,7 @@ module.exports = class DisplaySourceFixPlugin {

start() {
const win = this.api.app.getWindow?.();
this.loadConfig();
const electronApi = this.getElectronApi();
const mediaDevices = win?.navigator?.mediaDevices;
const hasSelectDisplayMediaSource = Boolean(electronApi && typeof electronApi.selectDisplayMediaSource === "function");
Expand Down Expand Up @@ -358,4 +362,46 @@ module.exports = class DisplaySourceFixPlugin {
this.originalGetUserMedia = null;
this.api.logger.info("Display source fallback patch disabled.");
}

getSourceTypes() {
const out = [];
if (this.includeScreens) out.push("screen");
if (this.includeWindows) out.push("window");
return out.length ? out : ["screen", "window"];
}

loadConfig() {
try {
this.includeScreens = this.api.storage.get("includeScreens", this.includeScreens) !== false;
this.includeWindows = this.api.storage.get("includeWindows", this.includeWindows) !== false;
this.cache.types = this.getSourceTypes();
} catch (_e) {}
}

getSettingsSchema() {
return {
title: "Display Source Fix",
description: "Desktop source picker and fallback source types.",
controls: [
{ key: "includeScreens", type: "boolean", label: "Include screens", value: this.includeScreens },
{ key: "includeWindows", type: "boolean", label: "Include windows", value: this.includeWindows }
]
};
}

setSettingValue(key, value) {
const k = String(key || "");
if (k === "includeScreens") this.includeScreens = Boolean(value);
if (k === "includeWindows") this.includeWindows = Boolean(value);
this.cache.types = this.getSourceTypes();
try {
this.api.storage.set("includeScreens", this.includeScreens);
this.api.storage.set("includeWindows", this.includeWindows);
} catch (_e) {}
this.refreshSources();
return {
includeScreens: this.includeScreens,
includeWindows: this.includeWindows
};
}
};
74 changes: 74 additions & 0 deletions nw/plugins/PronounsInChat/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ module.exports = class PronounsInChatPlugin {
start() {
const doc = this.api.app.getDocument?.();
if (!doc) return;
this.loadConfig();
this.loadPersistentCaches();
this.injectStyle(doc);
this.exposeGlobalAccessor();
Expand Down Expand Up @@ -630,4 +631,77 @@ module.exports = class PronounsInChatPlugin {
this.retryTimers.delete(span);
}
}

loadConfig() {
try {
const retry = Number(this.api.storage.get("retryDelayMs", this.retryDelayMs));
const maxPronouns = Number(this.api.storage.get("maxCacheEntries", this.maxCacheEntries));
const maxProfiles = Number(this.api.storage.get("maxProfileEntries", this.maxProfileEntries));
if (Number.isFinite(retry) && retry >= 1000) this.retryDelayMs = Math.round(retry);
if (Number.isFinite(maxPronouns) && maxPronouns >= 100) this.maxCacheEntries = Math.round(maxPronouns);
if (Number.isFinite(maxProfiles) && maxProfiles >= 100) this.maxProfileEntries = Math.round(maxProfiles);
} catch (_e) {}
}

getSettingsSchema() {
return {
title: "Pronouns In Chat",
description: "Caching and retry settings for pronoun badges.",
controls: [
{
key: "retryDelayMs",
type: "range",
label: "Retry delay (ms)",
min: 1000,
max: 120000,
step: 1000,
value: this.retryDelayMs
},
{
key: "maxCacheEntries",
type: "range",
label: "Pronoun cache size",
min: 100,
max: 10000,
step: 100,
value: this.maxCacheEntries
},
{
key: "maxProfileEntries",
type: "range",
label: "Profile cache size",
min: 100,
max: 5000,
step: 100,
value: this.maxProfileEntries
}
]
};
}

setSettingValue(key, value) {
const k = String(key || "");
if (k === "retryDelayMs") {
const n = Number(value);
if (Number.isFinite(n) && n >= 1000) this.retryDelayMs = Math.round(n);
}
if (k === "maxCacheEntries") {
const n = Number(value);
if (Number.isFinite(n) && n >= 100) this.maxCacheEntries = Math.round(n);
}
if (k === "maxProfileEntries") {
const n = Number(value);
if (Number.isFinite(n) && n >= 100) this.maxProfileEntries = Math.round(n);
}
try {
this.api.storage.set("retryDelayMs", this.retryDelayMs);
this.api.storage.set("maxCacheEntries", this.maxCacheEntries);
this.api.storage.set("maxProfileEntries", this.maxProfileEntries);
} catch (_e) {}
return {
retryDelayMs: this.retryDelayMs,
maxCacheEntries: this.maxCacheEntries,
maxProfileEntries: this.maxProfileEntries
};
}
};
Loading