added recoding and settings tab
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"polling_interval": 500,
|
"polling_interval": 500,
|
||||||
|
"webserver": {
|
||||||
|
"port": 3001
|
||||||
|
},
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
"id": "mic",
|
"id": "mic",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import {NextResponse} from "next/server";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
let writeLock: boolean = false;
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const configPath = process.env.CONFIG_PATH || "./config.json";
|
const configPath = process.env.CONFIG_PATH || "./config.json";
|
||||||
const absolutePath = path.resolve(configPath);
|
const absolutePath = path.resolve(configPath);
|
||||||
@@ -14,4 +16,29 @@ export async function GET() {
|
|||||||
const config = JSON.parse(fileContent);
|
const config = JSON.parse(fileContent);
|
||||||
|
|
||||||
return NextResponse.json(config);
|
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> {
|
export function fetchValue(path: string): Promise<any> {
|
||||||
return new Promise((resolve) => {
|
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});
|
sendMessage({action: "getValue", path});
|
||||||
|
|
||||||
// We'll listen for the matching response just once
|
// 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 {fetchConfig} from "@/app/utils/fetchConfig";
|
||||||
import BusControlTabs from "@/app/components/BusControlTabs";
|
import BusControlTabs from "@/app/components/BusControlTabs";
|
||||||
import MainMix from "@/app/components/MainMix";
|
import MainMix from "@/app/components/MainMix";
|
||||||
|
import Settings from "@/app/components/Settings";
|
||||||
|
import {Config} from "@/app/types/config";
|
||||||
|
|
||||||
// initialize global websocket connection
|
// 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 Page = () => {
|
||||||
const [appConfig, setAppConfig] = useState(null);
|
const [appConfig, setAppConfig] = useState(null);
|
||||||
const [activeTab, setActiveTab] = useState("mainMix");
|
const [activeTab, setActiveTab] = useState("mainMix");
|
||||||
|
|
||||||
const [webSocketConnected, setWebSocketConnected] = useState(false);
|
const [webSocketConnected, setWebSocketConnected] = useState(false);
|
||||||
|
const [mixerConnected, setMixerConnected] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initConnection(`ws://${window.location.hostname}:${WEBSOCKET_PORT}`);
|
fetchConfig().then((config: Config) => {
|
||||||
fetchConfig().then(config => {
|
|
||||||
setAppConfig(config);
|
setAppConfig(config);
|
||||||
|
initConnection(`ws://${window.location.hostname}:${config.webserver.port}`);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// periodically check if the websocket connection is still alive
|
// periodically check if the websocket connection is still alive
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (!isConnected) {
|
// wait for appConfig to be fetched
|
||||||
initConnection(`ws://${window.location.hostname}:${WEBSOCKET_PORT}`);
|
if (!appConfig) {
|
||||||
|
console.log("AppConfig not fetched yet");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
setWebSocketConnected(isConnected);
|
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);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, [appConfig]);
|
||||||
|
|
||||||
|
|
||||||
const tabComponents = useMemo(() => {
|
const tabComponents = useMemo(() => {
|
||||||
@@ -50,6 +93,23 @@ const Page = () => {
|
|||||||
content: (
|
content: (
|
||||||
<BusControlTabs/>
|
<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) {
|
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 (
|
return (
|
||||||
|
|||||||
Vendored
+1
@@ -1,4 +1,5 @@
|
|||||||
export interface Config {
|
export interface Config {
|
||||||
|
webserver: any
|
||||||
polling_interval: number
|
polling_interval: number
|
||||||
inputs: any
|
inputs: any
|
||||||
bus_controls: any
|
bus_controls: any
|
||||||
|
|||||||
Reference in New Issue
Block a user