From e8404c803967b7275ac0182a8b6f5b21b5db5aa1 Mon Sep 17 00:00:00 2001 From: Rox Squires Date: Fri, 13 Mar 2026 00:02:11 +0000 Subject: [PATCH] Removed Some junk ready for linux fix --- bridge-nw/app.js | 74 ++++ bridge-nw/index.html | 16 + bridge-nw/scripts/local-bridge.js | 98 +++++- do_not_edit/fluxer | 1 + nw/plugins/DiscordRPCEmu/index.js | 324 ++++++++++++++++- nw/plugins/DisplaySourceFix/index.js | 407 ---------------------- nw/plugins/DisplaySourceFix/manifest.json | 7 - scripts/lib/fluxer-injector-utils.js | 66 +++- 8 files changed, 563 insertions(+), 430 deletions(-) create mode 160000 do_not_edit/fluxer delete mode 100644 nw/plugins/DisplaySourceFix/index.js delete mode 100644 nw/plugins/DisplaySourceFix/manifest.json diff --git a/bridge-nw/app.js b/bridge-nw/app.js index bcb4474..6366091 100644 --- a/bridge-nw/app.js +++ b/bridge-nw/app.js @@ -12,6 +12,8 @@ const startBtn = document.getElementById("startBtn"); const stopBtn = document.getElementById("stopBtn"); const healthBtn = document.getElementById("healthBtn"); const probeBtn = document.getElementById("probeBtn"); +const nerdModeToggleEl = document.getElementById("nerdModeToggle"); +const updateIntervalInputEl = document.getElementById("updateIntervalInput"); const statusEl = document.getElementById("status"); const logEl = document.getElementById("log"); @@ -19,6 +21,52 @@ let bridgeProc = null; let tray = null; let isQuitting = false; let healthMonitorTimer = null; +const SETTINGS_FILE = path.join(getAppDataHome(), APP_NAME, "data", "bridge-settings.json"); +const DEFAULT_UPDATE_INTERVAL_MS = 10000; + +function normalizeUpdateIntervalMs(value) { + const n = Number.parseInt(String(value || ""), 10); + if (!Number.isFinite(n)) return DEFAULT_UPDATE_INTERVAL_MS; + return Math.max(1000, Math.min(60000, Math.round(n))); +} + +function ensureBridgeDataDir() { + fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true }); +} + +function loadBridgeSettings() { + try { + if (!fs.existsSync(SETTINGS_FILE)) { + return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS }; + } + const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8")); + return { + nerdModeEnabled: Boolean(parsed && parsed.nerdModeEnabled), + updateIntervalMs: normalizeUpdateIntervalMs(parsed && parsed.updateIntervalMs) + }; + } catch (_) { + return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS }; + } +} + +function saveBridgeSettings(nextSettings) { + ensureBridgeDataDir(); + const payload = { + nerdModeEnabled: Boolean(nextSettings && nextSettings.nerdModeEnabled), + updateIntervalMs: normalizeUpdateIntervalMs(nextSettings && nextSettings.updateIntervalMs) + }; + fs.writeFileSync(SETTINGS_FILE, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); + return payload; +} + +function applyBridgeSettingsToUi(settings) { + if (nerdModeToggleEl) { + nerdModeToggleEl.checked = Boolean(settings && settings.nerdModeEnabled); + } + if (updateIntervalInputEl) { + updateIntervalInputEl.value = String(normalizeUpdateIntervalMs(settings && settings.updateIntervalMs)); + } +} function getBundleRoot() { try { @@ -343,6 +391,12 @@ async function checkHealth() { try { const health = await httpJson(`${BRIDGE_URL}/health`); setStatus(health && health.ok ? "running" : "degraded"); + if (health && (typeof health.nerdModeEnabled === "boolean" || health.updateIntervalMs != null)) { + applyBridgeSettingsToUi({ + nerdModeEnabled: health.nerdModeEnabled, + updateIntervalMs: health.updateIntervalMs + }); + } log(`Health: ${JSON.stringify(health)}`); } catch (err) { setStatus("offline"); @@ -368,6 +422,25 @@ startBtn.addEventListener("click", startBridge); stopBtn.addEventListener("click", stopBridge); healthBtn.addEventListener("click", checkHealth); probeBtn.addEventListener("click", probeNowPlaying); +if (nerdModeToggleEl) { + nerdModeToggleEl.addEventListener("change", () => { + const settings = saveBridgeSettings({ + nerdModeEnabled: Boolean(nerdModeToggleEl.checked), + updateIntervalMs: updateIntervalInputEl ? updateIntervalInputEl.value : DEFAULT_UPDATE_INTERVAL_MS + }); + log(`Nerd Mode ${settings.nerdModeEnabled ? "enabled" : "disabled"}.`); + }); +} +if (updateIntervalInputEl) { + updateIntervalInputEl.addEventListener("change", () => { + const settings = saveBridgeSettings({ + nerdModeEnabled: nerdModeToggleEl ? Boolean(nerdModeToggleEl.checked) : false, + updateIntervalMs: updateIntervalInputEl.value + }); + applyBridgeSettingsToUi(settings); + log(`Update interval set to ${settings.updateIntervalMs}ms.`); + }); +} window.addEventListener("beforeunload", () => { stopHealthMonitor(); @@ -463,5 +536,6 @@ function setupTrayBehavior() { } setStatus("idle"); +applyBridgeSettingsToUi(loadBridgeSettings()); log("Bridge NW app ready."); setupTrayBehavior(); diff --git a/bridge-nw/index.html b/bridge-nw/index.html index a30edb8..d62e74c 100644 --- a/bridge-nw/index.html +++ b/bridge-nw/index.html @@ -11,6 +11,10 @@ button { background: #1f2430; color: #e7e9ee; border: 1px solid #2e3544; border-radius: 8px; padding: 8px 12px; cursor: pointer; } button:hover { background: #2a3142; } .status { background: #121622; border: 1px solid #2e3544; border-radius: 8px; padding: 10px; margin-bottom: 12px; } + .toggleRow { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding: 10px 12px; background: #121622; border: 1px solid #2e3544; border-radius: 8px; } + .toggleRow label { display: flex; align-items: center; gap: 8px; cursor: pointer; } + .toggleHint { color: #9aa4b4; font-size: 12px; } + .settingInput { background: #0b0d14; color: #e7e9ee; border: 1px solid #2e3544; border-radius: 6px; padding: 6px 8px; width: 120px; } pre { background: #0b0d14; border: 1px solid #2e3544; border-radius: 8px; padding: 12px; height: 460px; overflow: auto; white-space: pre-wrap; } code { color: #8ad1ff; } @@ -24,6 +28,18 @@

BetterFluxer Bridge (NW.js)

+
+ + When nothing is playing, publish system stats instead. +
+
+ + + Suggested poll interval for status sync. +
Status: idle

   
diff --git a/bridge-nw/scripts/local-bridge.js b/bridge-nw/scripts/local-bridge.js
index 79b686b..94a04f4 100644
--- a/bridge-nw/scripts/local-bridge.js
+++ b/bridge-nw/scripts/local-bridge.js
@@ -31,6 +31,7 @@ const BRIDGE_BASE_DIR = getBridgeBaseDir();
 const DATA_DIR = path.join(BRIDGE_BASE_DIR, "data");
 const TOKEN_FILE = path.join(DATA_DIR, "bridge-token.txt");
 const CACHE_FILE = path.join(DATA_DIR, "bridge-cache.json");
+const SETTINGS_FILE = path.join(DATA_DIR, "bridge-settings.json");
 
 const HOST = "127.0.0.1";
 const PORT = Number.parseInt(process.env.BF_BRIDGE_PORT || "21864", 10);
@@ -43,6 +44,7 @@ const BRIDGE_VERSION = BRIDGE_VERSION_BY_OS[process.platform] || "2026-03-10-rpc
 const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.BF_BRIDGE_TIMEOUT_MS || "12000", 10);
 const DEFAULT_TTL_SECONDS = Number.parseInt(process.env.BF_BRIDGE_DEFAULT_TTL || "120", 10);
 const MAX_TTL_SECONDS = Number.parseInt(process.env.BF_BRIDGE_MAX_TTL || "1800", 10);
+const DEFAULT_UPDATE_INTERVAL_MS = Number.parseInt(process.env.BF_BRIDGE_UPDATE_INTERVAL_MS || "10000", 10);
 const WINDOWS_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
 const WINDOWS_RUN_VALUE = "BetterFluxerBridge";
 const LINUX_BIN_DIR = path.join(os.homedir(), ".local", "bin");
@@ -116,6 +118,12 @@ function normalizeTtlSeconds(input) {
   return Math.max(1, Math.min(MAX_TTL_SECONDS, n));
 }
 
+function normalizeUpdateIntervalMs(input) {
+  const n = Number.parseInt(String(input || ""), 10);
+  if (!Number.isFinite(n) || n <= 0) return DEFAULT_UPDATE_INTERVAL_MS;
+  return Math.max(1000, Math.min(60000, Math.round(n)));
+}
+
 function readJsonBody(req) {
   return new Promise((resolve) => {
     let raw = "";
@@ -153,6 +161,21 @@ function saveCache(cache) {
   fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
 }
 
+function loadBridgeSettings() {
+  if (!fs.existsSync(SETTINGS_FILE)) {
+    return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+  }
+  try {
+    const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8"));
+    return {
+      nerdModeEnabled: Boolean(parsed && parsed.nerdModeEnabled),
+      updateIntervalMs: normalizeUpdateIntervalMs(parsed && parsed.updateIntervalMs)
+    };
+  } catch (_) {
+    return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+  }
+}
+
 function parseArgv(argv) {
   const out = {};
   for (const item of argv || []) {
@@ -392,6 +415,41 @@ function normalizeNowPlayingPayload(payload, source) {
   };
 }
 
+function formatDurationShort(totalSeconds) {
+  const sec = Math.max(0, Math.floor(Number(totalSeconds) || 0));
+  const hours = Math.floor(sec / 3600);
+  const minutes = Math.floor((sec % 3600) / 60);
+  const seconds = sec % 60;
+  if (hours > 0) return `${hours}h ${minutes}m`;
+  if (minutes > 0) return `${minutes}m ${seconds}s`;
+  return `${seconds}s`;
+}
+
+function buildNerdModeNowPlaying() {
+  const totalMem = os.totalmem();
+  const freeMem = os.freemem();
+  const usedMem = Math.max(0, totalMem - freeMem);
+  const memPercent = totalMem > 0 ? Math.round((usedMem / totalMem) * 100) : 0;
+  const loadAvg = os.loadavg();
+  const cpuCount = Array.isArray(os.cpus()) ? os.cpus().length : 0;
+  const hostname = String(os.hostname() || "").trim();
+  const details = `CPU ${Number(loadAvg[0] || 0).toFixed(2)} | RAM ${memPercent}%`;
+  const state = `${hostname || process.platform} | up ${formatDurationShort(os.uptime())}`;
+  return normalizeNowPlayingPayload(
+    {
+      source: "nerd-mode",
+      kind: "system",
+      title: "Nerd Mode",
+      details,
+      state,
+      appId: hostname || process.platform,
+      playbackStatus: "idle",
+      name: `System Stats (${cpuCount} cores)`
+    },
+    "nerd-mode"
+  );
+}
+
 function makeRpcFrame(op, payloadObject) {
   const payload = Buffer.from(JSON.stringify(payloadObject || {}), "utf8");
   const frame = Buffer.allocUnsafe(8 + payload.length);
@@ -809,6 +867,7 @@ async function queryMacMedia() {
 
 async function queryUniversalNowPlaying(state) {
   const now = Date.now();
+  let rpcFallback = null;
   if (state && state.lastRpcActivity && now - Number(state.lastRpcActivityAt || 0) < 180000) {
     const raw = state.lastRpcActivity.raw && typeof state.lastRpcActivity.raw === "object" ? state.lastRpcActivity.raw : {};
     const base = state.lastRpcActivity.normalized && typeof state.lastRpcActivity.normalized === "object"
@@ -823,31 +882,49 @@ async function queryUniversalNowPlaying(state) {
     if (startMs != null && endMs != null && endMs > startMs) {
       base.durationMs = endMs - startMs;
     }
-    return base;
+    if (base && base.ok && base.hasSession) {
+      return base;
+    }
+    rpcFallback = {
+      ok: true,
+      hasSession: false,
+      source: String(base.source || "discord-rpc-pipe"),
+      error: String(base.error || "No active RPC session")
+    };
   }
 
+  let fallback = null;
   if (process.platform === "win32") {
     const tuna = await queryTunaNowPlaying(state);
     if (tuna && tuna.ok && tuna.hasSession) return tuna;
-    return tuna && tuna.ok
+    fallback = tuna && tuna.ok
       ? tuna
       : { ok: false, hasSession: false, source: "windows-now-playing", error: "No active RPC or Tuna session" };
-  }
-  if (process.platform === "linux") {
+  } else if (process.platform === "linux") {
     const tuna = await queryTunaNowPlaying(state);
     if (tuna && tuna.ok && tuna.hasSession) return tuna;
-    return tuna && tuna.ok
+    fallback = tuna && tuna.ok
       ? tuna
       : { ok: false, hasSession: false, source: "linux-now-playing", error: "No active Tuna session" };
-  }
-  if (process.platform === "darwin") {
+  } else if (process.platform === "darwin") {
     const mac = await queryMacMedia();
     if (mac.ok && mac.hasSession) return mac;
     const tuna = await queryTunaNowPlaying(state);
     if (tuna && tuna.ok && tuna.hasSession) return tuna;
-    return mac.ok ? mac : tuna.ok ? tuna : mac;
+    fallback = mac.ok ? mac : tuna.ok ? tuna : mac;
+  } else {
+    fallback = { ok: false, error: `Unsupported platform: ${process.platform}` };
+  }
+
+  const preferredFallback =
+    fallback && fallback.hasSession
+      ? fallback
+      : rpcFallback || fallback;
+
+  if (loadBridgeSettings().nerdModeEnabled && (!preferredFallback || !preferredFallback.hasSession)) {
+    return buildNerdModeNowPlaying();
   }
-  return { ok: false, error: `Unsupported platform: ${process.platform}` };
+  return preferredFallback;
 }
 
 function formatNowPlayingForLog(np) {
@@ -988,6 +1065,7 @@ async function main() {
 
     const url = new URL(req.url || "/", `http://${HOST}:${PORT}`);
     if (url.pathname === "/health") {
+      const settings = loadBridgeSettings();
       return sendJson(
         res,
         200,
@@ -999,6 +1077,8 @@ async function main() {
           rpcPipesListening: (bridgeState.rpcServers || []).length,
           rpcPipeErrors: (bridgeState.rpcBindErrors || []).length,
           tunaPath: bridgeState.tunaPath,
+          nerdModeEnabled: Boolean(settings.nerdModeEnabled),
+          updateIntervalMs: settings.updateIntervalMs,
           allowlist,
           uptimeSec: Math.floor(process.uptime())
         },
diff --git a/do_not_edit/fluxer b/do_not_edit/fluxer
new file mode 160000
index 0000000..03813bb
--- /dev/null
+++ b/do_not_edit/fluxer
@@ -0,0 +1 @@
+Subproject commit 03813bbe17db008452f0f1be3090a7d2970a5447
diff --git a/nw/plugins/DiscordRPCEmu/index.js b/nw/plugins/DiscordRPCEmu/index.js
index 917311f..d09ff8f 100644
--- a/nw/plugins/DiscordRPCEmu/index.js
+++ b/nw/plugins/DiscordRPCEmu/index.js
@@ -51,8 +51,10 @@ module.exports = class DiscordRPCEmuPlugin {
     this.bridgeSocket = null;
     this.bridgePort = null;
     this.bridgeReconnectTimer = null;
+    this.bridgeWatchdogTimer = null;
     this.bridgeStarted = false;
     this.bridgeNonce = 0;
+    this.lastBridgeSocketEventAt = 0;
     this.statusSyncEnabled = this.api.storage.get("statusSyncEnabled", true) !== false;
     this.statusPollTimer = null;
     this.lastBridgeActivity = null;
@@ -65,10 +67,14 @@ module.exports = class DiscordRPCEmuPlugin {
     this.localBridgeEnabled = this.api.storage.get("localBridgeEnabled", true) !== false;
     this.localBridgePort = Number.parseInt(String(this.api.storage.get("localBridgePort", "21864")), 10) || 21864;
     this.localBridgeToken = String(this.api.storage.get("localBridgeToken", "") || "");
+    this.statusSyncIntervalMs = Number.parseInt(String(this.api.storage.get("statusSyncIntervalMs", "10000")), 10) || 10000;
     this.lastWindowsMedia = null;
     this.lastWindowsMediaAt = 0;
     this.lastWindowsMediaError = "";
     this.debugDetection = this.api.storage.get("debugDetection", false) === true;
+    this.savedUserCustomStatus = this.normalizeCustomStatusValue(this.api.storage.get("savedUserCustomStatus", null));
+    this.statusSyncOwnsStatus = this.api.storage.get("statusSyncOwnsStatus", false) === true;
+    this.statusMutationDepth = 0;
   }
 
   start() {
@@ -182,9 +188,12 @@ module.exports = class DiscordRPCEmuPlugin {
       }),
       getStatusSyncState: () => ({
         enabled: Boolean(plugin.statusSyncEnabled),
+        intervalMs: plugin.statusSyncIntervalMs,
         lastAppliedStatusText: plugin.lastAppliedStatusText || "",
         lastStatusApplyAt: plugin.lastStatusApplyAt || 0,
         cachedEndpoint: plugin.cachedStatusEndpoint || null,
+        savedUserCustomStatus: plugin.savedUserCustomStatus,
+        statusSyncOwnsStatus: Boolean(plugin.statusSyncOwnsStatus),
         localBridgeEnabled: Boolean(plugin.localBridgeEnabled),
         localBridgePort: plugin.localBridgePort,
         hasRecentWindowsMedia: Boolean(plugin.lastWindowsMedia && Date.now() - plugin.lastWindowsMediaAt < 30000)
@@ -234,6 +243,7 @@ module.exports = class DiscordRPCEmuPlugin {
       description: "Bridge and status sync behavior.",
       controls: [
         { key: "statusSyncEnabled", type: "boolean", label: "Enable status sync", value: this.statusSyncEnabled },
+        { key: "statusSyncIntervalMs", type: "range", label: "Update interval (ms)", min: 1000, max: 60000, step: 1000, value: this.statusSyncIntervalMs },
         { 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) },
@@ -265,6 +275,16 @@ module.exports = class DiscordRPCEmuPlugin {
       if (this.statusSyncEnabled) this.startStatusSync();
       else this.stopStatusSync();
     }
+    if (k === "statusSyncIntervalMs") {
+      const n = Number(value);
+      if (Number.isFinite(n) && n >= 1000 && n <= 60000) {
+        this.statusSyncIntervalMs = Math.round(n);
+        if (this.statusSyncEnabled) {
+          this.stopStatusSync();
+          this.startStatusSync();
+        }
+      }
+    }
     if (k === "localBridgeEnabled") {
       this.localBridgeEnabled = Boolean(value);
       this.stopBridge();
@@ -286,6 +306,7 @@ module.exports = class DiscordRPCEmuPlugin {
     }
     try {
       this.api.storage.set("statusSyncEnabled", this.statusSyncEnabled);
+      this.api.storage.set("statusSyncIntervalMs", this.statusSyncIntervalMs);
       this.api.storage.set("localBridgeEnabled", this.localBridgeEnabled);
       this.api.storage.set("debugDetection", this.debugDetection);
       this.api.storage.set("localBridgePort", this.localBridgePort);
@@ -293,6 +314,7 @@ module.exports = class DiscordRPCEmuPlugin {
     } catch (_e) {}
     return {
       statusSyncEnabled: this.statusSyncEnabled,
+      statusSyncIntervalMs: this.statusSyncIntervalMs,
       localBridgeEnabled: this.localBridgeEnabled,
       debugDetection: this.debugDetection,
       localBridgePort: this.localBridgePort,
@@ -344,6 +366,7 @@ module.exports = class DiscordRPCEmuPlugin {
       }
 
       if (response && response.ok && bodyJson && typeof bodyJson === "object") {
+        const capturedCustomStatus = plugin.extractCustomStatusFromPayload(bodyJson);
         const hasCustomStatus =
           Object.prototype.hasOwnProperty.call(bodyJson, "custom_status") ||
           (bodyJson.status &&
@@ -357,6 +380,12 @@ module.exports = class DiscordRPCEmuPlugin {
             plugin.captureStatusEndpointArmed = false;
             plugin.api.logger.info(`DiscordRPCEmu: captured status endpoint ${method} ${url}`);
           }
+          if (plugin.statusMutationDepth <= 0) {
+            plugin.savedUserCustomStatus = capturedCustomStatus;
+            plugin.api.storage.set("savedUserCustomStatus", capturedCustomStatus);
+            plugin.statusSyncOwnsStatus = false;
+            plugin.api.storage.set("statusSyncOwnsStatus", false);
+          }
         }
       }
 
@@ -376,9 +405,12 @@ module.exports = class DiscordRPCEmuPlugin {
   startStatusSync() {
     if (!this.statusSyncEnabled) return;
     if (this.statusPollTimer) return;
+    const intervalMs = Number.isFinite(Number(this.statusSyncIntervalMs))
+      ? Math.max(1000, Math.min(60000, Math.round(Number(this.statusSyncIntervalMs))))
+      : 10000;
     this.statusPollTimer = setInterval(() => {
       this.applyNowPlayingFromSources().catch(() => {});
-    }, 10000);
+    }, intervalMs);
     this.applyNowPlayingFromSources().catch(() => {});
   }
 
@@ -402,6 +434,102 @@ module.exports = class DiscordRPCEmuPlugin {
     return token;
   }
 
+  normalizeCustomStatusValue(value) {
+    if (!value || typeof value !== "object") return null;
+    const text = String(value.text || "").trim();
+    const emojiName = String(value.emoji_name || value.emojiName || "").trim();
+    const emojiId =
+      value.emoji_id != null
+        ? String(value.emoji_id).trim()
+        : value.emojiId != null
+          ? String(value.emojiId).trim()
+          : "";
+    const expiresAt =
+      value.expires_at != null
+        ? String(value.expires_at).trim()
+        : value.expiresAt != null
+          ? String(value.expiresAt).trim()
+          : "";
+    if (!text && !emojiName && !emojiId) return null;
+    const out = {};
+    if (text) out.text = text;
+    if (emojiName) out.emoji_name = emojiName;
+    if (emojiId) out.emoji_id = emojiId;
+    if (expiresAt) out.expires_at = expiresAt;
+    return out;
+  }
+
+  extractCustomStatusFromPayload(payload) {
+    const body = payload && typeof payload === "object" ? payload : null;
+    if (!body) return null;
+    if (Object.prototype.hasOwnProperty.call(body, "custom_status")) {
+      return this.normalizeCustomStatusValue(body.custom_status);
+    }
+    if (body.status && typeof body.status === "object" && Object.prototype.hasOwnProperty.call(body.status, "custom_status")) {
+      return this.normalizeCustomStatusValue(body.status.custom_status);
+    }
+    return null;
+  }
+
+  withPluginStatusMutation(task) {
+    this.statusMutationDepth += 1;
+    return Promise.resolve()
+      .then(task)
+      .finally(() => {
+        this.statusMutationDepth = Math.max(0, this.statusMutationDepth - 1);
+      });
+  }
+
+  async readCurrentFluxerCustomStatus() {
+    const win = this.api.app.getWindow?.();
+    if (!win || typeof win.fetch !== "function") return null;
+    const token = this.getAuthToken();
+    const baseHeaders = { Accept: "application/json" };
+    const headerVariants = [{ ...baseHeaders }];
+    if (token) {
+      headerVariants.push({ ...baseHeaders, Authorization: token });
+      headerVariants.push({ ...baseHeaders, Authorization: `Bearer ${token}` });
+    }
+    const targets = [
+      "/api/v1/users/@me/settings",
+      "/api/v1/users/@me",
+      "/api/v1/users/@me/profile",
+      "https://web.fluxer.app/api/v1/users/@me/settings",
+      "https://web.fluxer.app/api/v1/users/@me",
+      "https://web.fluxer.app/api/v1/users/@me/profile"
+    ];
+    for (const url of targets) {
+      for (const headers of headerVariants) {
+        try {
+          const res = await win.fetch(url, { method: "GET", credentials: "include", headers });
+          if (!res || !res.ok) continue;
+          const payload = await res.json().catch(() => null);
+          const customStatus = this.extractCustomStatusFromPayload(payload);
+          const hasStatusField =
+            Boolean(payload && typeof payload === "object" && Object.prototype.hasOwnProperty.call(payload, "custom_status")) ||
+            Boolean(
+              payload &&
+              payload.status &&
+              typeof payload.status === "object" &&
+              Object.prototype.hasOwnProperty.call(payload.status, "custom_status")
+            );
+          if (customStatus || hasStatusField) {
+            return customStatus;
+          }
+        } catch (_) {}
+      }
+    }
+    return null;
+  }
+
+  async ensureSavedUserCustomStatus() {
+    if (this.statusSyncOwnsStatus) return this.savedUserCustomStatus;
+    const currentStatus = await this.readCurrentFluxerCustomStatus();
+    this.savedUserCustomStatus = currentStatus;
+    this.api.storage.set("savedUserCustomStatus", currentStatus);
+    return currentStatus;
+  }
+
   formatClock(ms) {
     const n = Number(ms);
     if (!Number.isFinite(n) || n < 0) return "";
@@ -466,13 +594,14 @@ module.exports = class DiscordRPCEmuPlugin {
     if (!m || !m.ok || !m.hasSession) return "";
     const kind = String(m.kind || "").toLowerCase();
     const source = String(m.source || "").toLowerCase();
+    const isNerdMode = source === "nerd-mode" || kind === "system";
     const hasTrackMetadata = Boolean(String(m.title || "").trim() || String(m.artist || "").trim() || String(m.albumTitle || "").trim());
     const hasRpcGameMarkers = kind === "game";
     const hasExplicitGameType =
       (typeof m.activityType === "number" && Number.isFinite(m.activityType) && m.activityType === 0) ||
       (typeof m.activityType === "string" && m.activityType.trim() !== "" && Number(m.activityType) === 0);
     const isGame = hasRpcGameMarkers || (hasExplicitGameType && !hasTrackMetadata);
-    const prefix = isGame ? "🎮 Playing " : "🎵 Listening to ";
+    const prefix = isNerdMode ? "⚙️ " : isGame ? "🎮 Playing " : "🎵 Listening to ";
 
     const name = String(m.name || "").trim();
     const details = String(m.details || "").trim();
@@ -603,12 +732,29 @@ module.exports = class DiscordRPCEmuPlugin {
   async applyNowPlayingFromSources() {
     if (!this.statusSyncEnabled) return false;
     const text = await this.getPreferredNowPlayingText();
-    if (!text) return false;
+    if (!text) {
+      if (!this.statusSyncOwnsStatus && !this.lastAppliedStatusText) return false;
+      const restored = this.savedUserCustomStatus
+        ? await this.restoreFluxerCustomStatus(this.savedUserCustomStatus)
+        : await this.clearFluxerCustomStatus();
+      if (restored) {
+        this.lastAppliedStatusText = "";
+        this.lastStatusApplyAt = Date.now();
+        this.statusSyncOwnsStatus = false;
+        this.api.storage.set("statusSyncOwnsStatus", false);
+      }
+      return restored;
+    }
     if (text === this.lastAppliedStatusText) return true;
+    if (!this.statusSyncOwnsStatus) {
+      await this.ensureSavedUserCustomStatus();
+    }
     const ok = await this.setFluxerCustomStatus(text);
     if (ok) {
       this.lastAppliedStatusText = text;
       this.lastStatusApplyAt = Date.now();
+      this.statusSyncOwnsStatus = true;
+      this.api.storage.set("statusSyncOwnsStatus", true);
     }
     return ok;
   }
@@ -675,12 +821,12 @@ module.exports = class DiscordRPCEmuPlugin {
       seen.add(key);
       for (const headers of headerVariants) {
         try {
-          const res = await win.fetch(def.url, {
+          const res = await this.withPluginStatusMutation(() => win.fetch(def.url, {
             method: def.method,
             credentials: "include",
             headers,
             body: JSON.stringify(def.body || {})
-          });
+          }));
           if (!res) continue;
           if (res.ok) {
             this.cachedStatusEndpoint = { method: def.method, url: def.url, body: def.body };
@@ -693,6 +839,144 @@ module.exports = class DiscordRPCEmuPlugin {
     return false;
   }
 
+  async clearFluxerCustomStatus() {
+    const win = this.api.app.getWindow?.();
+    if (!win || typeof win.fetch !== "function") return false;
+    const token = this.getAuthToken();
+    const baseHeaders = {
+      Accept: "application/json",
+      "Content-Type": "application/json"
+    };
+    const headerVariants = [];
+    headerVariants.push({ ...baseHeaders });
+    if (token) {
+      headerVariants.push({ ...baseHeaders, Authorization: token });
+      headerVariants.push({ ...baseHeaders, Authorization: `Bearer ${token}` });
+    }
+
+    const requestDefs = [];
+    if (this.cachedStatusEndpoint && this.cachedStatusEndpoint.url && this.cachedStatusEndpoint.method) {
+      const cachedBody = this.cachedStatusEndpoint.body && typeof this.cachedStatusEndpoint.body === "object"
+        ? JSON.parse(JSON.stringify(this.cachedStatusEndpoint.body))
+        : {};
+      if (cachedBody.custom_status && typeof cachedBody.custom_status === "object") {
+        cachedBody.custom_status = null;
+      } else if (cachedBody.status && typeof cachedBody.status === "object") {
+        cachedBody.status.custom_status = null;
+      } else {
+        cachedBody.custom_status = null;
+      }
+      requestDefs.push({
+        method: this.cachedStatusEndpoint.method,
+        url: this.cachedStatusEndpoint.url,
+        body: cachedBody
+      });
+    }
+    requestDefs.push(
+      { method: "PATCH", url: "/api/v1/users/@me/settings", body: { custom_status: null } },
+      { method: "PATCH", url: "/api/v1/users/@me", body: { custom_status: null } },
+      { method: "PATCH", url: "/api/v1/users/@me/profile", body: { custom_status: null } },
+      { method: "PATCH", url: "https://web.fluxer.app/api/v1/users/@me/settings", body: { custom_status: null } },
+      { method: "PATCH", url: "https://web.fluxer.app/api/v1/users/@me", body: { custom_status: null } },
+      { method: "PATCH", url: "https://web.fluxer.app/api/v1/users/@me/profile", body: { custom_status: null } },
+      { method: "PATCH", url: "/api/v1/users/@me/settings", body: { status: { custom_status: null } } }
+    );
+
+    const seen = new Set();
+    for (const def of requestDefs) {
+      if (!def || !def.url || !def.method) continue;
+      const key = `${def.method}|${def.url}|${JSON.stringify(def.body || {})}`;
+      if (seen.has(key)) continue;
+      seen.add(key);
+      for (const headers of headerVariants) {
+        try {
+          const res = await this.withPluginStatusMutation(() => win.fetch(def.url, {
+            method: def.method,
+            credentials: "include",
+            headers,
+            body: JSON.stringify(def.body || {})
+          }));
+          if (res && res.ok) {
+            return true;
+          }
+        } catch (_) {}
+      }
+    }
+    return false;
+  }
+
+  async restoreFluxerCustomStatus(customStatus) {
+    const restored = this.normalizeCustomStatusValue(customStatus);
+    if (!restored) return this.clearFluxerCustomStatus();
+    const win = this.api.app.getWindow?.();
+    if (!win || typeof win.fetch !== "function") return false;
+    const token = this.getAuthToken();
+    const baseHeaders = {
+      Accept: "application/json",
+      "Content-Type": "application/json"
+    };
+    const headerVariants = [{ ...baseHeaders }];
+    if (token) {
+      headerVariants.push({ ...baseHeaders, Authorization: token });
+      headerVariants.push({ ...baseHeaders, Authorization: `Bearer ${token}` });
+    }
+
+    const applyCustomStatus = (body, value) => {
+      const input = body && typeof body === "object" ? JSON.parse(JSON.stringify(body)) : {};
+      const out = input && typeof input === "object" ? input : {};
+      if (Object.prototype.hasOwnProperty.call(out, "custom_status")) {
+        out.custom_status = value;
+        return out;
+      }
+      if (out.status && typeof out.status === "object") {
+        out.status.custom_status = value;
+        return out;
+      }
+      out.custom_status = value;
+      return out;
+    };
+
+    const requestDefs = [];
+    if (this.cachedStatusEndpoint && this.cachedStatusEndpoint.url && this.cachedStatusEndpoint.method) {
+      requestDefs.push({
+        method: this.cachedStatusEndpoint.method,
+        url: this.cachedStatusEndpoint.url,
+        body: applyCustomStatus(this.cachedStatusEndpoint.body, restored)
+      });
+    }
+    requestDefs.push(
+      { method: "PATCH", url: "/api/v1/users/@me/settings", body: { custom_status: restored } },
+      { method: "PATCH", url: "/api/v1/users/@me", body: { custom_status: restored } },
+      { method: "PATCH", url: "/api/v1/users/@me/profile", body: { custom_status: restored } },
+      { method: "PATCH", url: "https://web.fluxer.app/api/v1/users/@me/settings", body: { custom_status: restored } },
+      { method: "PATCH", url: "https://web.fluxer.app/api/v1/users/@me", body: { custom_status: restored } },
+      { method: "PATCH", url: "https://web.fluxer.app/api/v1/users/@me/profile", body: { custom_status: restored } },
+      { method: "PATCH", url: "/api/v1/users/@me/settings", body: { status: { custom_status: restored } } }
+    );
+
+    const seen = new Set();
+    for (const def of requestDefs) {
+      if (!def || !def.url || !def.method) continue;
+      const key = `${def.method}|${def.url}|${JSON.stringify(def.body || {})}`;
+      if (seen.has(key)) continue;
+      seen.add(key);
+      for (const headers of headerVariants) {
+        try {
+          const res = await this.withPluginStatusMutation(() => win.fetch(def.url, {
+            method: def.method,
+            credentials: "include",
+            headers,
+            body: JSON.stringify(def.body || {})
+          }));
+          if (res && res.ok) {
+            return true;
+          }
+        } catch (_) {}
+      }
+    }
+    return false;
+  }
+
   getBridgeState() {
     return {
       connected: Boolean(this.bridgeSocket && this.bridgeSocket.readyState === 1),
@@ -707,6 +991,8 @@ module.exports = class DiscordRPCEmuPlugin {
       lastAppliedStatusText: this.lastAppliedStatusText || "",
       lastStatusApplyAt: this.lastStatusApplyAt || 0,
       cachedEndpoint: this.cachedStatusEndpoint || null,
+      savedUserCustomStatus: this.savedUserCustomStatus,
+      statusSyncOwnsStatus: Boolean(this.statusSyncOwnsStatus),
       hasRecentBridgeActivity: Boolean(this.lastBridgeActivity && Date.now() - this.lastBridgeActivityAt < 120000),
       captureArmed: Boolean(this.captureStatusEndpointArmed)
     };
@@ -810,6 +1096,7 @@ module.exports = class DiscordRPCEmuPlugin {
 
   startBridge() {
     this.bridgeStarted = true;
+    this.startBridgeWatchdog();
     this.connectBridge();
   }
 
@@ -819,6 +1106,10 @@ module.exports = class DiscordRPCEmuPlugin {
       clearTimeout(this.bridgeReconnectTimer);
       this.bridgeReconnectTimer = null;
     }
+    if (this.bridgeWatchdogTimer) {
+      clearInterval(this.bridgeWatchdogTimer);
+      this.bridgeWatchdogTimer = null;
+    }
     if (this.bridgeSocket) {
       try {
         this.bridgeSocket.close();
@@ -826,6 +1117,26 @@ module.exports = class DiscordRPCEmuPlugin {
       this.bridgeSocket = null;
     }
     this.bridgePort = null;
+    this.lastBridgeSocketEventAt = 0;
+  }
+
+  startBridgeWatchdog() {
+    if (this.bridgeWatchdogTimer) return;
+    this.bridgeWatchdogTimer = setInterval(() => {
+      if (!this.bridgeStarted) return;
+      const ws = this.bridgeSocket;
+      if (!ws || ws.readyState !== 1) return;
+      const lastEventAt = Number(this.lastBridgeSocketEventAt || 0);
+      if (!lastEventAt) return;
+      if (Date.now() - lastEventAt < 90000) return;
+      this.api.logger.warn("DiscordRPCEmu: Discord RPC bridge went stale, reconnecting.");
+      try {
+        ws.close();
+      } catch (_) {
+        this.bridgeSocket = null;
+      }
+      this.scheduleBridgeReconnect(1000);
+    }, 15000);
   }
 
   scheduleBridgeReconnect(delayMs) {
@@ -920,6 +1231,7 @@ module.exports = class DiscordRPCEmuPlugin {
         settled = true;
         this.bridgeSocket = ws;
         this.bridgePort = candidate.port;
+        this.lastBridgeSocketEventAt = Date.now();
         this.api.logger.info(`DiscordRPCEmu: connected Discord RPC bridge on port ${candidate.port}`);
         try {
           ws.send(
@@ -935,6 +1247,7 @@ module.exports = class DiscordRPCEmuPlugin {
 
       ws.addEventListener("message", (event) => {
         try {
+          this.lastBridgeSocketEventAt = Date.now();
           const raw = typeof event.data === "string" ? event.data : "";
           if (!raw) return;
           const msg = JSON.parse(raw);
@@ -956,6 +1269,7 @@ module.exports = class DiscordRPCEmuPlugin {
           this.bridgeSocket = null;
           this.bridgePort = null;
         }
+        this.lastBridgeSocketEventAt = 0;
         this.scheduleBridgeReconnect(5000);
       });
 
diff --git a/nw/plugins/DisplaySourceFix/index.js b/nw/plugins/DisplaySourceFix/index.js
deleted file mode 100644
index e11f608..0000000
--- a/nw/plugins/DisplaySourceFix/index.js
+++ /dev/null
@@ -1,407 +0,0 @@
-module.exports = class DisplaySourceFixPlugin {
-  constructor(api) {
-    this.api = api;
-    this.originalSelect = null;
-    this.originalGetDisplayMedia = null;
-    this.originalGetUserMedia = null;
-    this.onDisplayMediaRequestedUnsub = null;
-    this.lastGoodSourceId = null;
-    this.pendingRequest = null;
-    this.pendingPickerResolve = null;
-    this.overlayId = "betterfluxer-display-picker-overlay";
-    this.styleId = "betterfluxer-display-picker-style";
-    this.cache = {
-      types: ["screen", "window"],
-      sources: [],
-      at: 0
-    };
-    this.includeScreens = true;
-    this.includeWindows = true;
-  }
-
-  getElectronApi() {
-    return this.api.app.getWindow?.()?.electron;
-  }
-
-  async refreshSources() {
-    const electronApi = this.getElectronApi();
-    if (!electronApi || typeof electronApi.getDesktopSources !== "function") {
-      return [];
-    }
-    try {
-      this.cache.types = this.getSourceTypes();
-      const sources = await electronApi.getDesktopSources(this.cache.types);
-      if (Array.isArray(sources)) {
-        this.cache.sources = sources;
-        this.cache.at = Date.now();
-      }
-    } catch (error) {
-      this.api.logger.warn("Failed to refresh desktop sources:", error?.message || error);
-    }
-    return this.cache.sources;
-  }
-
-  findFallbackSourceId(requestedId) {
-    const id = String(requestedId || "");
-    const sources = this.cache.sources || [];
-    if (sources.length === 0) {
-      if (this.lastGoodSourceId) return this.lastGoodSourceId;
-      if (id.startsWith("window:")) return "screen:0:0";
-      return null;
-    }
-
-    const exact = sources.find((item) => String(item?.id || "") === id);
-    if (exact) return exact.id;
-
-    const prefix = id.includes(":") ? id.split(":")[0] : "";
-    if (prefix) {
-      const samePrefix = sources.find((item) => String(item?.id || "").startsWith(`${prefix}:`));
-      if (samePrefix) return samePrefix.id;
-    }
-
-    const preferredScreen = sources.find((item) => String(item?.id || "").startsWith("screen:"));
-    if (preferredScreen) return String(preferredScreen.id || "");
-
-    return String(sources[0]?.id || this.lastGoodSourceId || "");
-  }
-
-  ensurePickerStyle(doc) {
-    if (doc.getElementById(this.styleId)) return;
-    const style = doc.createElement("style");
-    style.id = this.styleId;
-    style.textContent = [
-      ".bf-dsp-overlay{position:fixed;inset:0;background:rgba(0,0,0,.62);z-index:2147483647;display:flex;align-items:center;justify-content:center;}",
-      ".bf-dsp-modal{width:min(860px,96vw);max-height:88vh;overflow:auto;background:#15181d;border:1px solid #2b3139;border-radius:12px;color:#f4f6f8;padding:14px;box-shadow:0 28px 70px rgba(0,0,0,.5);font-family:Segoe UI,Tahoma,sans-serif;}",
-      ".bf-dsp-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;}",
-      ".bf-dsp-head h3{margin:0;font-size:16px;}",
-      ".bf-dsp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;}",
-      ".bf-dsp-card{background:#1b2027;border:1px solid #2b3139;border-radius:10px;padding:10px;display:grid;gap:8px;}",
-      ".bf-dsp-name{font-size:13px;color:#d7dee7;word-break:break-word;}",
-      ".bf-dsp-id{font-size:11px;color:#9cacbe;word-break:break-word;}",
-      ".bf-dsp-btn{border:1px solid #3c4754;background:#26313d;color:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;}",
-      ".bf-dsp-btn:hover{background:#304050;}",
-      ".bf-dsp-foot{display:flex;justify-content:space-between;align-items:center;margin-top:10px;gap:10px;}",
-      ".bf-dsp-muted{font-size:12px;color:#9cacbe;}"
-    ].join("");
-    doc.head.appendChild(style);
-  }
-
-  closePicker() {
-    const doc = this.api.app.getDocument?.();
-    if (!doc) return;
-    const overlay = doc.getElementById(this.overlayId);
-    if (overlay) overlay.remove();
-    if (typeof this.pendingPickerResolve === "function") {
-      const resolve = this.pendingPickerResolve;
-      this.pendingPickerResolve = null;
-      resolve(null);
-    }
-    this.pendingRequest = null;
-  }
-
-  selectForRequest(requestId, sourceId, withAudio) {
-    const chosen = String(sourceId || "");
-    if (!chosen) return;
-
-    if (requestId && this.originalSelect) {
-      this.originalSelect(requestId, chosen, withAudio);
-    }
-
-    if (typeof this.pendingPickerResolve === "function") {
-      const resolve = this.pendingPickerResolve;
-      this.pendingPickerResolve = null;
-      const selected = (this.cache.sources || []).find((item) => String(item?.id || "") === chosen) || { id: chosen };
-      resolve(selected);
-    }
-
-    this.lastGoodSourceId = chosen;
-    const doc = this.api.app.getDocument?.();
-    const overlay = doc?.getElementById(this.overlayId);
-    if (overlay) overlay.remove();
-    this.pendingRequest = null;
-  }
-
-  async showPicker(requestId, info) {
-    const doc = this.api.app.getDocument?.();
-    if (!doc) return;
-
-    await this.refreshSources();
-    const sources = this.cache.sources || [];
-    if (!Array.isArray(sources) || sources.length === 0) {
-      this.api.logger.warn("No desktop sources available for custom picker.");
-      return;
-    }
-
-    this.closePicker();
-    this.ensurePickerStyle(doc);
-
-    const overlay = doc.createElement("div");
-    overlay.id = this.overlayId;
-    overlay.className = "bf-dsp-overlay";
-
-    const modal = doc.createElement("div");
-    modal.className = "bf-dsp-modal";
-    overlay.appendChild(modal);
-
-    const head = doc.createElement("div");
-    head.className = "bf-dsp-head";
-    head.innerHTML = "

Select Window Or Screen

"; - const closeBtn = doc.createElement("button"); - closeBtn.className = "bf-dsp-btn"; - closeBtn.textContent = "Cancel"; - closeBtn.addEventListener("click", () => { - this.closePicker(); - }); - head.appendChild(closeBtn); - modal.appendChild(head); - - const grid = doc.createElement("div"); - grid.className = "bf-dsp-grid"; - modal.appendChild(grid); - - for (const source of sources) { - const id = String(source?.id || ""); - if (!id) continue; - const name = String(source?.name || id); - const card = doc.createElement("div"); - card.className = "bf-dsp-card"; - - const title = doc.createElement("div"); - title.className = "bf-dsp-name"; - title.textContent = name; - card.appendChild(title); - - const sub = doc.createElement("div"); - sub.className = "bf-dsp-id"; - sub.textContent = id; - card.appendChild(sub); - - const selectBtn = doc.createElement("button"); - selectBtn.className = "bf-dsp-btn"; - selectBtn.textContent = "Share This"; - selectBtn.addEventListener("click", () => { - this.selectForRequest(requestId, id, Boolean(info?.withAudio)); - }); - card.appendChild(selectBtn); - - grid.appendChild(card); - } - - const foot = doc.createElement("div"); - foot.className = "bf-dsp-foot"; - const muted = doc.createElement("div"); - muted.className = "bf-dsp-muted"; - muted.textContent = "Custom picker by BetterFluxer"; - foot.appendChild(muted); - const fallbackBtn = doc.createElement("button"); - fallbackBtn.className = "bf-dsp-btn"; - fallbackBtn.textContent = "Use Fallback"; - fallbackBtn.addEventListener("click", () => { - const fallback = this.findFallbackSourceId("screen:0:0") || "screen:0:0"; - this.selectForRequest(requestId, fallback, Boolean(info?.withAudio)); - }); - foot.appendChild(fallbackBtn); - modal.appendChild(foot); - - overlay.addEventListener("click", (event) => { - if (event.target === overlay) this.closePicker(); - }); - doc.body.appendChild(overlay); - - this.pendingRequest = { requestId, withAudio: Boolean(info?.withAudio) }; - } - - async pickSourceFromOverlay() { - await this.showPicker(null, { withAudio: false }); - return new Promise((resolve) => { - this.pendingPickerResolve = resolve; - }); - } - - async captureViaCustomPicker(constraints) { - const win = this.api.app.getWindow?.(); - const mediaDevices = win?.navigator?.mediaDevices; - if (!mediaDevices || typeof mediaDevices.getUserMedia !== "function") { - throw new Error("mediaDevices.getUserMedia unavailable"); - } - - const selected = await this.pickSourceFromOverlay(); - if (!selected || !selected.id) { - throw new Error("Display capture canceled"); - } - - const selectedId = String(selected.id); - const wantsAudio = Boolean(constraints && constraints.audio); - const videoTrack = { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: selectedId - } - }; - - const userMediaConstraints = { - video: videoTrack, - audio: false - }; - - // System audio capture support varies across Linux setups; keep it optional. - if (wantsAudio) { - userMediaConstraints.audio = { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: selectedId - } - }; - } - - return mediaDevices.getUserMedia(userMediaConstraints); - } - - 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"); - const hasGetDisplayMedia = Boolean(mediaDevices && typeof mediaDevices.getDisplayMedia === "function"); - const hasGetDesktopSources = Boolean(electronApi && typeof electronApi.getDesktopSources === "function"); - - if (!hasGetDisplayMedia) { - this.api.logger.warn("navigator.mediaDevices.getDisplayMedia is unavailable; plugin inactive."); - return; - } - - if (!hasGetDesktopSources) { - this.api.logger.warn("electron.getDesktopSources is unavailable; custom picker cannot list sources."); - } - - if (hasSelectDisplayMediaSource) { - this.originalSelect = electronApi.selectDisplayMediaSource.bind(electronApi); - } - if (mediaDevices && typeof mediaDevices.getDisplayMedia === "function") { - this.originalGetDisplayMedia = mediaDevices.getDisplayMedia.bind(mediaDevices); - } - if (mediaDevices && typeof mediaDevices.getUserMedia === "function") { - this.originalGetUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); - } - - this.refreshSources(); - - if (mediaDevices && this.originalGetDisplayMedia) { - mediaDevices.getDisplayMedia = async (constraints = { video: true, audio: false }) => { - try { - return await this.captureViaCustomPicker(constraints); - } catch (error) { - this.api.logger.warn("Custom getDisplayMedia failed, falling back:", error?.message || error); - return this.originalGetDisplayMedia(constraints); - } - }; - this.api.logger.info("Custom display source selector enabled (getDisplayMedia override)."); - } - - if (electronApi && typeof electronApi.onDisplayMediaRequested === "function" && this.originalSelect) { - this.onDisplayMediaRequestedUnsub = electronApi.onDisplayMediaRequested(async (requestId, info) => { - try { - await this.showPicker(requestId, info || {}); - } catch (error) { - this.api.logger.warn("Custom picker failed, falling back:", error?.message || error); - const fallback = this.findFallbackSourceId("screen:0:0") || "screen:0:0"; - this.selectForRequest(requestId, fallback, Boolean(info?.withAudio)); - } - }); - this.api.logger.info("Custom display source selector enabled."); - return; - } - - if (electronApi && this.originalSelect) { - this.api.logger.warn("onDisplayMediaRequested not available; using fallback-only mode."); - const patchedSelect = async (requestId, sourceId, withAudio) => { - const requested = String(sourceId || ""); - if (!requested) { - this.originalSelect(requestId, sourceId, withAudio); - return; - } - await this.refreshSources(); - const chosenId = this.findFallbackSourceId(requested) || requested; - this.originalSelect(requestId, chosenId, withAudio); - this.lastGoodSourceId = chosenId; - }; - electronApi.selectDisplayMediaSource = patchedSelect; - } - } - - stop() { - this.closePicker(); - const win = this.api.app.getWindow?.(); - const mediaDevices = win?.navigator?.mediaDevices; - const electronApi = this.getElectronApi(); - if (mediaDevices && this.originalGetDisplayMedia) { - try { - mediaDevices.getDisplayMedia = this.originalGetDisplayMedia; - } catch (_) {} - } - if (electronApi && this.originalSelect) { - try { - electronApi.selectDisplayMediaSource = this.originalSelect; - } catch (_) {} - try { - Object.defineProperty(electronApi, "selectDisplayMediaSource", { - configurable: true, - enumerable: true, - writable: true, - value: this.originalSelect - }); - } catch (_) {} - } - if (typeof this.onDisplayMediaRequestedUnsub === "function") { - this.onDisplayMediaRequestedUnsub(); - } - this.onDisplayMediaRequestedUnsub = null; - this.originalSelect = null; - this.originalGetDisplayMedia = null; - 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 - }; - } -}; diff --git a/nw/plugins/DisplaySourceFix/manifest.json b/nw/plugins/DisplaySourceFix/manifest.json deleted file mode 100644 index c032000..0000000 --- a/nw/plugins/DisplaySourceFix/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "DisplaySourceFix", - "version": "1.0.0", - "description": "Retries display source selection with a valid fallback when stale source IDs are used.", - "author": "BetterFluxer", - "main": "index.js" -} diff --git a/scripts/lib/fluxer-injector-utils.js b/scripts/lib/fluxer-injector-utils.js index a5bb912..73a4011 100644 --- a/scripts/lib/fluxer-injector-utils.js +++ b/scripts/lib/fluxer-injector-utils.js @@ -1659,6 +1659,59 @@ try { } } + class MembersClass extends BaseDOMClass { + getMemberItems() { + const nodes = this.queryAll( + "span[class*='MemberListItem'][class*='name'], [data-user-id] span[class*='name'], [data-user-id]" + ); + return nodes + .map((node) => { + const carrier = node.closest && typeof node.closest === "function" ? node.closest("[data-user-id]") : null; + const userId = String((carrier && carrier.getAttribute && carrier.getAttribute("data-user-id")) || (node.getAttribute && node.getAttribute("data-user-id")) || ""); + return { + id: userId, + label: this.text(node), + element: node + }; + }) + .filter((item) => item.label || item.id); + } + + clickMemberByName(name) { + const needle = String(name || "").trim().toLowerCase(); + if (!needle) return false; + const item = this.getMemberItems().find((it) => String(it.label || "").toLowerCase().includes(needle)); + if (!item || !item.element || typeof item.element.click !== "function") return false; + item.element.click(); + return true; + } + + getVisibleMemberIds() { + const out = []; + const seen = new Set(); + for (const item of this.getMemberItems()) { + const id = String(item.id || ""); + if (!id || seen.has(id)) continue; + seen.add(id); + out.push(id); + } + return out; + } + + getMemberById(userId) { + const id = String(userId || "").trim(); + if (!id) return null; + return this.getMemberItems().find((it) => String(it.id) === id) || null; + } + + clickMemberById(userId) { + const item = this.getMemberById(userId); + if (!item || !item.element || typeof item.element.click !== "function") return false; + item.element.click(); + return true; + } + } + function createUIClasses() { const settingsSidebar = new SettingsSidebarClass(); const userProfile = new UserProfileClass(settingsSidebar); @@ -1669,7 +1722,8 @@ try { userProfile, messages: new MessagesClass(), guildList: new GuildListClass(), - channels: new ChannelsClass() + channels: new ChannelsClass(), + members: new MembersClass() }; } @@ -1679,7 +1733,8 @@ try { UserProfileClass, MessagesClass, GuildListClass, - ChannelsClass + ChannelsClass, + MembersClass }; runtime.uiClasses = createUIClasses(); @@ -3088,6 +3143,13 @@ try { getChannelItems: () => runtime.uiClasses.channels.getChannelItems(), clickChannelByName: (name) => runtime.uiClasses.channels.clickChannelByName(name) }, + members: { + getMemberItems: () => runtime.uiClasses.members.getMemberItems(), + getVisibleMemberIds: () => runtime.uiClasses.members.getVisibleMemberIds(), + getMemberById: (userId) => runtime.uiClasses.members.getMemberById(userId), + clickMemberById: (userId) => runtime.uiClasses.members.clickMemberById(userId), + clickMemberByName: (name) => runtime.uiClasses.members.clickMemberByName(name) + }, settingsSidebar: { getItems: () => runtime.uiClasses.settingsSidebar.getItems(), clickById: (tabId) => runtime.uiClasses.settingsSidebar.clickById(tabId)