added recoding and settings tab

This commit is contained in:
2025-01-13 12:37:08 +01:00
parent b2267813c0
commit 6918534b95
7 changed files with 269 additions and 9 deletions
+3
View File
@@ -1,5 +1,8 @@
{
"polling_interval": 500,
"webserver": {
"port": 3001
},
"inputs": [
{
"id": "mic",
+27
View File
@@ -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;
}
}
+127
View File
@@ -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 (
<div key={key}>
<label>{key}</label>
{value.map((item, index) => (
<div key={index} style={{marginLeft: "20px", marginBottom: "10px"}}>
{typeof item === "object" && item !== null ? (
// Render object fields
<div>
{Object.entries(item).map(([fieldKey, fieldValue]: [string, any]) => (
<div key={fieldKey}>
<label>{fieldKey}</label>
<input
type="text"
value={fieldValue}
onChange={(e) =>
handleObjectInListChange(
key,
index,
fieldKey,
e.target.value
)
}
/>
</div>
))}
<button
onClick={() => handleRemoveFromList(key, index)}
style={{marginTop: "5px"}}
>
Remove Object
</button>
</div>
) : (
// Render primitive values in the list
<div>
<input
type="text"
value={item}
onChange={(e) =>
handleValueChange(key, [
...value.slice(0, index),
e.target.value,
...value.slice(index + 1),
])
}
/>
<button onClick={() => handleRemoveFromList(key, index)}>
Remove
</button>
</div>
)}
</div>
))}
<button onClick={() => handleAddToList(key)}>Add Item</button>
</div>
);
} else if (typeof value === "object" && value !== null) {
// Render nested objects
return (
<div key={key}>
<label>{key}</label>
<div style={{marginLeft: "20px"}}>
{Object.entries(value).map(([nestedKey, nestedValue]) =>
renderField(nestedKey, nestedValue)
)}
</div>
</div>
);
} else {
// Render a single value editor
return (
<div key={key}>
<label>{key}</label>
<input
type="text"
value={value}
onChange={(e) => handleValueChange(key, e.target.value)}
/>
</div>
);
}
};
return <div>{Object.entries(config).map(([key, value]) => renderField(key, value))}</div>;
}
export default ConfigEditor;
+32
View File
@@ -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 <p> Loading...</p>;
}
return (
<div>
<h2>Settings</h2>
<ConfigEditor config={appConfig} setConfig={updateConfig}/>
</div>
);
}
export default Settings;
+6
View File
@@ -58,6 +58,12 @@ export function sendMessage(msg: any) {
export function fetchValue(path: string): Promise<any> {
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
+73 -9
View File
@@ -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<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return new Promise<T>((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: (
<BusControlTabs/>
),
},
{
id: "recording",
label: "Recording",
content: (
<div>
<h2>Recording</h2>
<p>Coming soon...</p>
</div>
),
},
{
id: "settings",
label: "Settings",
content: (
<Settings/>
),
}
]
}
@@ -60,7 +120,11 @@ const Page = () => {
}
if (!webSocketConnected) {
return <p>WebSocket or Mixer not connected</p>;
return <p>WebSocket not connected</p>;
}
if (!mixerConnected) {
return <p>Mixer not connected</p>;
}
return (
+1
View File
@@ -1,4 +1,5 @@
export interface Config {
webserver: any
polling_interval: number
inputs: any
bus_controls: any