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