diff --git a/config.json b/config.json
index 2c81989..1d97861 100644
--- a/config.json
+++ b/config.json
@@ -1,5 +1,8 @@
{
"polling_interval": 500,
+ "webserver": {
+ "port": 3001
+ },
"inputs": [
{
"id": "mic",
diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts
index a3c6017..fb79584 100644
--- a/src/app/api/config/route.ts
+++ b/src/app/api/config/route.ts
@@ -2,6 +2,8 @@ import {NextResponse} from "next/server";
import fs from "fs";
import path from "path";
+let writeLock: boolean = false;
+
export async function GET() {
const configPath = process.env.CONFIG_PATH || "./config.json";
const absolutePath = path.resolve(configPath);
@@ -14,4 +16,29 @@ export async function GET() {
const config = JSON.parse(fileContent);
return NextResponse.json(config);
+}
+
+// post function for updating the config
+export async function POST({body}) {
+
+ if (writeLock) {
+ return NextResponse.json({error: "Write lock is active"}, {status: 400});
+ }
+
+ writeLock = true;
+
+ try {
+ const configPath = process.env.CONFIG_PATH || "./config.json";
+ const absolutePath = path.resolve(configPath);
+
+ fs.writeFileSync(absolutePath, JSON.stringify(body, null, 2));
+
+ return NextResponse.json({success: true});
+ }
+ catch (e) {
+ return NextResponse.json({error: e.message}, {status: 500});
+ }
+ finally {
+ writeLock = false;
+ }
}
\ No newline at end of file
diff --git a/src/app/components/ConfigEditor.tsx b/src/app/components/ConfigEditor.tsx
new file mode 100644
index 0000000..3a4f1bf
--- /dev/null
+++ b/src/app/components/ConfigEditor.tsx
@@ -0,0 +1,127 @@
+import React from "react";
+
+function ConfigEditor({config, setConfig}) {
+ const handleValueChange = (key, value) => {
+ setConfig((prev) => ({...prev, [key]: value}));
+ };
+
+ const handleAddToList = (key) => {
+ setConfig((prev) => ({
+ ...prev,
+ [key]: [...prev[key], {}], // Add an empty object
+ }));
+ };
+
+ const handleRemoveFromList = (key, index) => {
+ setConfig((prev) => ({
+ ...prev,
+ [key]: prev[key].filter((_, i) => i !== index),
+ }));
+ };
+
+ const handleNestedChange = (key, nestedKey, value) => {
+ setConfig((prev) => ({
+ ...prev,
+ [key]: {...prev[key], [nestedKey]: value},
+ }));
+ };
+
+ const handleObjectInListChange = (listKey, index, fieldKey, value) => {
+ setConfig((prev) => ({
+ ...prev,
+ [listKey]: prev[listKey].map((item, i) =>
+ i === index ? {...item, [fieldKey]: value} : item
+ ),
+ }));
+ };
+
+ const renderField = (key, value) => {
+ if (Array.isArray(value)) {
+ // Render a list editor
+ return (
+
+
+ {value.map((item, index) => (
+
+ {typeof item === "object" && item !== null ? (
+ // Render object fields
+
+ {Object.entries(item).map(([fieldKey, fieldValue]: [string, any]) => (
+
+
+
+ handleObjectInListChange(
+ key,
+ index,
+ fieldKey,
+ e.target.value
+ )
+ }
+ />
+
+ ))}
+
+
+ ) : (
+ // Render primitive values in the list
+
+
+ handleValueChange(key, [
+ ...value.slice(0, index),
+ e.target.value,
+ ...value.slice(index + 1),
+ ])
+ }
+ />
+
+
+ )}
+
+ ))}
+
+
+ );
+ } else if (typeof value === "object" && value !== null) {
+ // Render nested objects
+ return (
+
+
+
+ {Object.entries(value).map(([nestedKey, nestedValue]) =>
+ renderField(nestedKey, nestedValue)
+ )}
+
+
+ );
+ } else {
+ // Render a single value editor
+ return (
+
+
+ handleValueChange(key, e.target.value)}
+ />
+
+ );
+ }
+ };
+
+ return {Object.entries(config).map(([key, value]) => renderField(key, value))}
;
+}
+
+export default ConfigEditor;
diff --git a/src/app/components/Settings.tsx b/src/app/components/Settings.tsx
new file mode 100644
index 0000000..5f2c0ec
--- /dev/null
+++ b/src/app/components/Settings.tsx
@@ -0,0 +1,32 @@
+import ConfigEditor from "@/app/components/ConfigEditor";
+import {useEffect, useState} from "react";
+import {fetchConfig} from "@/app/utils/fetchConfig";
+
+const Settings = () => {
+
+ const [appConfig, setAppConfig] = useState(null);
+
+ useEffect(() => {
+ fetchConfig().then(config => {
+ setAppConfig(config);
+ });
+ }, []);
+
+ const updateConfig = (newConfig) => {
+ setAppConfig(newConfig);
+ }
+
+
+ if (!appConfig) {
+ return Loading...
;
+ }
+
+ return (
+
+
Settings
+
+
+ );
+}
+
+export default Settings;
\ No newline at end of file
diff --git a/src/app/lib/x32CommunicationManager.ts b/src/app/lib/x32CommunicationManager.ts
index adb53fd..856fade 100644
--- a/src/app/lib/x32CommunicationManager.ts
+++ b/src/app/lib/x32CommunicationManager.ts
@@ -58,6 +58,12 @@ export function sendMessage(msg: any) {
export function fetchValue(path: string): Promise {
return new Promise((resolve) => {
+
+ // raise an error if the websocket is not connected
+ if (!ws || !isConnected) {
+ throw new Error("WebSocket not connected. Message not sent");
+ }
+
sendMessage({action: "getValue", path});
// We'll listen for the matching response just once
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 2bc7278..3b0269d 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -4,35 +4,78 @@ import {useEffect, useMemo, useState} from "react";
import {fetchConfig} from "@/app/utils/fetchConfig";
import BusControlTabs from "@/app/components/BusControlTabs";
import MainMix from "@/app/components/MainMix";
+import Settings from "@/app/components/Settings";
+import {Config} from "@/app/types/config";
// initialize global websocket connection
-import {initConnection, isConnected} from "@/app/lib/x32CommunicationManager";
+import {fetchValue, initConnection, isConnected} from "@/app/lib/x32CommunicationManager";
-const WEBSOCKET_PORT = 3001;
+
+function withTimeout(promise: Promise, timeoutMs: number): Promise {
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ reject(new Error("Request timed out"));
+ }, timeoutMs);
+
+ promise
+ .then((result) => {
+ clearTimeout(timeout);
+ resolve(result);
+ })
+ .catch((error) => {
+ clearTimeout(timeout);
+ reject(error);
+ });
+ });
+}
const Page = () => {
const [appConfig, setAppConfig] = useState(null);
const [activeTab, setActiveTab] = useState("mainMix");
const [webSocketConnected, setWebSocketConnected] = useState(false);
+ const [mixerConnected, setMixerConnected] = useState(false);
+
useEffect(() => {
- initConnection(`ws://${window.location.hostname}:${WEBSOCKET_PORT}`);
- fetchConfig().then(config => {
+ fetchConfig().then((config: Config) => {
setAppConfig(config);
+ initConnection(`ws://${window.location.hostname}:${config.webserver.port}`);
});
}, []);
// periodically check if the websocket connection is still alive
useEffect(() => {
const interval = setInterval(() => {
- if (!isConnected) {
- initConnection(`ws://${window.location.hostname}:${WEBSOCKET_PORT}`);
+ // wait for appConfig to be fetched
+ if (!appConfig) {
+ console.log("AppConfig not fetched yet");
+ return;
}
setWebSocketConnected(isConnected);
- }, 1000);
+ if (!isConnected) {
+ console.log("Reconnecting to WebSocket");
+ initConnection(`ws://${window.location.hostname}:${appConfig.webserver.port}`);
+ } else {
+ // check if mixer is connected
+ withTimeout(fetchValue("/info"), 1500)
+ .then((response) => {
+ // response is a string starting with "V", which is the API version, if the mixer is connected
+ if (response.startsWith("V")) {
+ setMixerConnected(true);
+ } else {
+ setMixerConnected(false);
+ }
+ })
+ .catch((error) => {
+ console.log("Mixer connection check failed:", error.message);
+ setMixerConnected(false);
+ });
+
+ }
+ }, 2000);
return () => clearInterval(interval);
- }, []);
+ }, [appConfig]);
const tabComponents = useMemo(() => {
@@ -50,6 +93,23 @@ const Page = () => {
content: (
),
+ },
+ {
+ id: "recording",
+ label: "Recording",
+ content: (
+
+
Recording
+
Coming soon...
+
+ ),
+ },
+ {
+ id: "settings",
+ label: "Settings",
+ content: (
+
+ ),
}
]
}
@@ -60,7 +120,11 @@ const Page = () => {
}
if (!webSocketConnected) {
- return WebSocket or Mixer not connected
;
+ return WebSocket not connected
;
+ }
+
+ if (!mixerConnected) {
+ return Mixer not connected
;
}
return (
diff --git a/src/app/types/config.d.ts b/src/app/types/config.d.ts
index d341331..a9b43f9 100644
--- a/src/app/types/config.d.ts
+++ b/src/app/types/config.d.ts
@@ -1,4 +1,5 @@
export interface Config {
+ webserver: any
polling_interval: number
inputs: any
bus_controls: any