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: 3 additions & 0 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Prepare Linux desktop patch bundle
run: npm run prepare:linux-desktop-bundle

- name: Build injector linux package
run: npm run dist:linux

Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ cache/

# Local app copy / extraction workspace
app_do_not_edit/
do_not_edit/*
!do_not_edit/fluxer/
do_not_edit/fluxer/*
!do_not_edit/fluxer/fluxer_desktop/
!do_not_edit/fluxer/fluxer_desktop/**
do_not_edit/fluxer/.git/
do_not_edit/fluxer/fluxer_desktop/node_modules/
do_not_edit/fluxer/fluxer_desktop/dist/*
!do_not_edit/fluxer/fluxer_desktop/dist/main/
do_not_edit/fluxer/fluxer_desktop/dist/main/*
!do_not_edit/fluxer/fluxer_desktop/dist/main/index.js
MyPlugins/
MainFluxerApp/
# Logs
Expand Down
183 changes: 182 additions & 1 deletion bridge-nw/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,61 @@ 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");

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 {
Expand All @@ -44,6 +93,12 @@ function getTrayIconPath() {
function resolveNodeExec() {
const npmNode = String(process.env.npm_node_execpath || "").trim();
if (npmNode && fs.existsSync(npmNode)) return npmNode;
const runtimeNodeCandidates = [
path.join(getBridgeRuntimeRoot(), "node"),
path.join(getBridgeRuntimeRoot(), "node.exe")
];
const runtimeNode = runtimeNodeCandidates.find((p) => fs.existsSync(p));
if (runtimeNode) return runtimeNode;
if (process.platform === "win32") {
const localNodeCandidates = [
path.join(path.resolve(process.cwd(), ".."), "node.exe"),
Expand Down Expand Up @@ -112,6 +167,26 @@ function getBundledBridgeExePath() {
return candidates.find((p) => fs.existsSync(p)) || "";
}

function getBundledNodePath() {
const bundleRoot = getBundleRoot();
const candidates = process.platform === "win32"
? [
path.join(bundleRoot, "node.exe"),
path.join(bundleRoot, "bridge-nw", "node.exe"),
path.join(bundleRoot, "package.nw", "node.exe"),
path.join(path.dirname(process.execPath), "node.exe"),
path.join(path.dirname(process.execPath), "package.nw", "node.exe")
]
: [
path.join(bundleRoot, "node"),
path.join(bundleRoot, "bridge-nw", "node"),
path.join(bundleRoot, "package.nw", "node"),
path.join(path.dirname(process.execPath), "node"),
path.join(path.dirname(process.execPath), "package.nw", "node")
];
return candidates.find((p) => fs.existsSync(p)) || "";
}

function materializeBridgeRuntimeFiles() {
const sourceBridgeScript = getBundledBridgeScriptPath();
if (!sourceBridgeScript) return "";
Expand All @@ -138,6 +213,16 @@ function materializeBridgeRuntimeFiles() {
fs.copyFileSync(bundledEnv, path.join(runtimeBridgeEnvDir, ".env"));
}

const bundledNode = getBundledNodePath();
if (bundledNode) {
const runtimeNodeName = process.platform === "win32" ? "node.exe" : "node";
const runtimeNodePath = path.join(runtimeRoot, runtimeNodeName);
fs.copyFileSync(bundledNode, runtimeNodePath);
try {
if (process.platform !== "win32") fs.chmodSync(runtimeNodePath, 0o755);
} catch (_) {}
}

return runtimeBridgeScript;
}

Expand All @@ -151,6 +236,10 @@ function log(line) {
logEl.scrollTop = logEl.scrollHeight;
}

function statusIsRunning() {
return String(statusEl.textContent || "").toLowerCase().includes("running");
}

function httpJson(url) {
return new Promise((resolve, reject) => {
const req = http.get(url, (res) => {
Expand All @@ -170,6 +259,51 @@ function httpJson(url) {
});
}

async function isBridgeHealthy() {
try {
const health = await httpJson(`${BRIDGE_URL}/health`);
return Boolean(health && health.ok);
} catch (_) {
return false;
}
}

function stopHealthMonitor() {
if (healthMonitorTimer) {
clearInterval(healthMonitorTimer);
healthMonitorTimer = null;
}
}

function startHealthMonitor() {
stopHealthMonitor();
healthMonitorTimer = setInterval(async () => {
if (!bridgeProc) {
stopHealthMonitor();
return;
}
const healthy = await isBridgeHealthy();
if (healthy) {
setStatus("running");
return;
}
if (!statusIsRunning()) {
setStatus("starting...");
}
}, 3000);
}

async function waitForBridgeReady(maxWaitMs = 12000) {
const startAt = Date.now();
while (Date.now() - startAt < maxWaitMs) {
if (!bridgeProc) return false;
const healthy = await isBridgeHealthy();
if (healthy) return true;
await new Promise((resolve) => setTimeout(resolve, 300));
}
return false;
}

function startBridge() {
if (bridgeProc) {
log("Bridge already running in this app session.");
Expand Down Expand Up @@ -201,20 +335,39 @@ function startBridge() {
windowsHide: false
});
setStatus("starting...");
bridgeProc.stdout.on("data", (chunk) => log(String(chunk).trimEnd()));
bridgeProc.stdout.on("data", (chunk) => {
const text = String(chunk || "").trimEnd();
log(text);
if (/Listening on http:\/\/127\.0\.0\.1:\d+/i.test(text)) {
setStatus("running");
}
});
bridgeProc.stderr.on("data", (chunk) => log(String(chunk).trimEnd()));
bridgeProc.on("error", (err) => {
stopHealthMonitor();
log(`Bridge launch failed: ${err && err.message ? err.message : String(err)}`);
bridgeProc = null;
setStatus("failed");
});
bridgeProc.on("exit", (code) => {
stopHealthMonitor();
log(`Bridge exited with code ${code}`);
bridgeProc = null;
setStatus("stopped");
});
log("Bridge process started.");
startHealthMonitor();
waitForBridgeReady().then((ready) => {
if (!bridgeProc) return;
if (ready) {
setStatus("running");
return;
}
setStatus("starting...");
log("Bridge did not report healthy within 12s. Use Check Health for details.");
});
} catch (err) {
stopHealthMonitor();
log(`Start handler error: ${err && err.message ? err.message : String(err)}`);
setStatus("failed");
}
Expand All @@ -227,6 +380,7 @@ function stopBridge() {
}
try {
bridgeProc.kill();
stopHealthMonitor();
log("Sent stop signal to bridge process.");
} catch (err) {
log(`Failed to stop bridge: ${err.message}`);
Expand All @@ -237,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");
Expand All @@ -262,8 +422,28 @@ 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();
if (bridgeProc) {
try {
bridgeProc.kill();
Expand Down Expand Up @@ -356,5 +536,6 @@ function setupTrayBehavior() {
}

setStatus("idle");
applyBridgeSettingsToUi(loadBridgeSettings());
log("Bridge NW app ready.");
setupTrayBehavior();
16 changes: 16 additions & 0 deletions bridge-nw/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
</style>
Expand All @@ -24,6 +28,18 @@ <h2>BetterFluxer Bridge (NW.js)</h2>
<button id="healthBtn">Check Health</button>
<button id="probeBtn">Probe Now Playing</button>
</div>
<div class="toggleRow">
<label>
<input type="checkbox" id="nerdModeToggle" />
<span>Enable Nerd Mode</span>
</label>
<span class="toggleHint">When nothing is playing, publish system stats instead.</span>
</div>
<div class="toggleRow">
<label for="updateIntervalInput">Update interval (ms)</label>
<input class="settingInput" type="number" id="updateIntervalInput" min="1000" max="60000" step="1000" />
<span class="toggleHint">Suggested poll interval for status sync.</span>
</div>
<div class="status" id="status">Status: idle</div>
<pre id="log"></pre>
</div>
Expand Down
Loading
Loading