added recoding and settings tab
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"polling_interval": 500,
|
||||
"webserver": {
|
||||
"port": 3001
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "mic",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
export interface Config {
|
||||
webserver: any
|
||||
polling_interval: number
|
||||
inputs: any
|
||||
bus_controls: any
|
||||
|
||||
Reference in New Issue
Block a user