removed idea folder

This commit is contained in:
2025-01-12 22:25:24 +01:00
parent e8442ffde4
commit 79bdb7163f
27 changed files with 2844 additions and 273 deletions
+17
View File
@@ -0,0 +1,17 @@
import {NextResponse} from "next/server";
import fs from "fs";
import path from "path";
export async function GET() {
const configPath = process.env.CONFIG_PATH || "./config.json";
const absolutePath = path.resolve(configPath);
if (!fs.existsSync(absolutePath)) {
return NextResponse.json({error: "Config file not found"}, {status: 404});
}
const fileContent = fs.readFileSync(absolutePath, "utf-8");
const config = JSON.parse(fileContent);
return NextResponse.json(config);
}
+53
View File
@@ -0,0 +1,53 @@
"use client";
import {useEffect, useState} from "react";
import {fetchConfig} from "@/app/utils/fetchConfig";
import InputControl from "@/app/components/InputControl";
const BusControl = ({busControlConfig, mainConfig}) => {
const [appConfig, setAppConfig] = useState(null);
const busId = busControlConfig.id;
const busChannel = busControlConfig.bus
useEffect(() => {
if (mainConfig) {
setAppConfig(mainConfig);
} else {
fetchConfig().then(config => {
setAppConfig(config);
}
);
}
}, [mainConfig]);
return (
<div>
{!appConfig ? <p>Loading...</p> : (
<div className={"p-2 rounded-md border border-gray-250"}>
<div className={"mb-2 pb-3 border-b border-b-gray-250"}>
<InputControl
id={`bus-${busId}-main`}
name={`Main Level`}
basePath={`/bus/${busChannel.toString().padStart(2, "0")}/mix`}
appConfig={appConfig} faderControlSuffix={undefined} muteControlSuffix={undefined} />
</div>
<div className={"flex flex-col gap-3"}>
<h3>Inputs</h3>
{appConfig.inputs.map((input) => (
<InputControl
key={input.id}
id={`bus-${busId}-input-${input.id}`}
name={input.name}
faderControlSuffix={"level"}
basePath={`/ch/${input.channels[0].toString().padStart(2, "0")}/mix/${busChannel.toString().padStart(2, "0")}`}
appConfig={appConfig} muteControlSuffix={undefined} />
))}
</div>
</div>
)}
</div>
);
};
export default BusControl;
+85
View File
@@ -0,0 +1,85 @@
import React, {useEffect, useMemo, useState} from "react";
import {fetchConfig} from "@/app/utils/fetchConfig";
import BusControl from "@/app/components/BusControl";
import {Config} from "@/app/types/config";
const BusControlTabs = () => {
const [busControlConfigs, setBusControlConfigs] = useState([]);
const [activeTab, setActiveTab] = useState(null);
useEffect(() => {
fetchConfig().then((config: Config) => {
if (config) {
setBusControlConfigs(config.bus_controls);
// create tab components from bus control configs
const components = config.bus_controls.map((busControl) => {
return {
id: busControl.id,
label: busControl.name,
content: <BusControl busControlConfig={busControl} mainConfig={config}/>
};
});
// setTabComponents(components);
// set the active tab to the first tab
if (components.length > 0) {
setActiveTab(components[0].id);
}
}
}
);
}, []);
const tabComponents = useMemo(() => {
return busControlConfigs.map((busControl) => ({
id: busControl.id,
label: busControl.name,
content: (
<BusControl
key={busControl.id}
busControlConfig={busControl} mainConfig={undefined}
/>
),
}));
}, [busControlConfigs]);
if (!tabComponents.length) {
return <p>Loading...</p>;
}
return (
<div>
{/* Tabs Header */}
<div className="flex border-b mb-4">
{tabComponents.map((tab) => (
<button
key={tab.id}
className={`px-4 py-2 ${
activeTab === tab.id
? "border-b-2 border-blue-500 text-blue-500"
: "text-gray-500"
}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
{/* Tabs Content */}
<div>
{tabComponents.map((tab) => (
<div
key={tab.id}
style={{display: activeTab === tab.id ? "block" : "none"}}
>
{tab.content}
</div>
))}
</div>
</div>
);
};
export default BusControlTabs;
+64
View File
@@ -0,0 +1,64 @@
"use client";
import {useState, useEffect} from "react";
import {Slider} from "@/components/ui/slider"
import {fetchValue, sendValue} from "@/app/lib/x32CommunicationManager";
interface FaderProps {
id: string;
path: string;
pollingInterval: number;
}
const Fader: React.FC<FaderProps> = ({id, path, pollingInterval}) => {
const [faderValue, setFaderValue] = useState(0); // Values for each path
useEffect(() => {
const updateCurrentFaderValue = async () => {
fetchValue(path).then((value) => {
if (value !== faderValue) {
setFaderValue(value);
}
}
);
}
// Initial fetch and polling
updateCurrentFaderValue();
// add random interval to avoid all clients polling at the same time
const randomInterval = Math.random() * pollingInterval;
setTimeout(() => null, randomInterval);
const interval = setInterval(() => {
updateCurrentFaderValue();
}, pollingInterval);
return () => clearInterval(interval); // Cleanup interval
}, [path, pollingInterval]);
const handleFaderChange = async (path: string, newValue: number) => {
setFaderValue(newValue)
sendValue(path, newValue); // Send new value
};
return (
<Slider
id={`${id}-${path}-fader`}
min={0}
max={1}
step={0.01}
value={faderValue ? [faderValue,] : [0,]}
onValueChange={(newValue) => handleFaderChange(path, newValue[0])}
style={{
background: `linear-gradient(to right, #4ade80 75%, #ef4444 75%)`,
}}
/>
);
};
export default Fader;
+45
View File
@@ -0,0 +1,45 @@
import Fader from "@/app/components/Fader";
import MuteButton from "@/app/components/MuteButton";
const InputControl = ({
id,
name,
basePath,
faderControlSuffix,
muteControlSuffix,
appConfig
}) => {
if (faderControlSuffix === undefined) {
faderControlSuffix = "fader";
}
if (muteControlSuffix === undefined) {
muteControlSuffix = "on";
}
const faderControlPath = `${basePath}/${faderControlSuffix}`;
const muteControlPath = `${basePath}/${muteControlSuffix}`;
return (
<div className={"flex gap-5 w-auto"}>
<div className={"min-w-24"}>
<MuteButton
key={`inputControl-${id}-mute`}
id={`inputControl-${id}-mute`}
label={name}
path={muteControlPath}
pollingInterval={appConfig.polling_interval}
/>
</div>
<Fader
key={`inputControl-${id}`}
id={`inputControl-${id}`}
path={faderControlPath}
pollingInterval={appConfig.polling_interval}
/>
</div>
);
}
export default InputControl;
+42
View File
@@ -0,0 +1,42 @@
import {useEffect, useState} from "react";
import {fetchConfig} from "@/app/utils/fetchConfig";
import InputControl from "@/app/components/InputControl";
const MainMix = () => {
const [appConfig, setAppConfig] = useState(null);
useEffect(() => {
fetchConfig().then(config => {
setAppConfig(config);
});
}, []);
if (!appConfig) {
return <p> Loading...</p>;
}
return (
<div className={"p-2 rounded-md border border-gray-250"}>
<div className={"mb-3 pb-3 border-b border-b-gray-250"}>
<InputControl id={"mainMix"} name={"Main Level"} basePath={"/main/st/mix"} appConfig={appConfig}
faderControlSuffix={undefined} muteControlSuffix={undefined}/>
</div>
<div className={"flex flex-col gap-5"}>
{appConfig.inputs.map((input) => (
<InputControl
key={input.id}
id={input.id}
name={input.name}
basePath={`/ch/${input.channels[0].toString().padStart(2, "0")}/mix`}
appConfig={appConfig} faderControlSuffix={undefined} muteControlSuffix={undefined} />
))
}
</div>
</div>
);
}
export default MainMix;
+57
View File
@@ -0,0 +1,57 @@
import {useEffect, useState} from "react";
import {Button} from "@/components/ui/button";
import {fetchValue, sendValue} from "@/app/lib/x32CommunicationManager";
const MuteButton = ({id, label, path, pollingInterval}) => {
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
const updateCurrentMuteValue = async () => {
fetchValue(path).then((value) => {
if (value !== isMuted) {
setIsMuted(value);
}
}
);
}
// Initial fetch and polling
updateCurrentMuteValue();
// add random interval to avoid all clients polling at the same time
const randomInterval = Math.random() * pollingInterval;
setTimeout(() => null, randomInterval);
const interval = setInterval(() => {
updateCurrentMuteValue();
}, pollingInterval);
return () => clearInterval(interval); // Cleanup interval
}, [path, pollingInterval]);
const toggleMute = () => {
const newIsMuted = !isMuted;
// disable or enable the input for each channel
sendValue(
path,
newIsMuted ? 1 : 0,
);
setIsMuted(newIsMuted);
}
return (
<Button key={`${id}-${path}-mute`}
className={`w-24 ${isMuted ? 'bg-green-500' : 'bg-red-500'}`}
onClick={toggleMute}>
{label}
</Button>
)
}
export default MuteButton;
+61 -10
View File
@@ -2,20 +2,71 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
body {
font-family: Arial, Helvetica, sans-serif;
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+77
View File
@@ -0,0 +1,77 @@
import dotenv from "dotenv";
dotenv.config();
// A global manager (no React hooks)
let ws: WebSocket | null = null;
let isConnected = false;
// Your choice: store responses in memory, or just call callbacks
let messageCallbacks: Array<(msg: string) => void> = [];
export function initConnection(wsUrl: string) {
if (ws) return; // Already connected or connecting
console.log("Connecting to WebSocket:", wsUrl);
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log("Global WebSocket connected!");
isConnected = true;
};
ws.onclose = () => {
console.log("Global WebSocket disconnected.");
isConnected = false;
ws = null; // So we can reconnect if needed
};
ws.onerror = (err) => {
console.error("Global WebSocket error:", err);
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
// Notify any registered callbacks
messageCallbacks.forEach((cb) => cb(message));
} catch (err) {
console.error("Error parsing WebSocket message:", err);
}
};
}
export function registerMessageCallback(cb: (msg: any) => void) {
messageCallbacks.push(cb);
}
export function removeMessageCallback(cb: (response: any) => void) {
messageCallbacks = messageCallbacks.filter((f) => f !== cb);
}
// Send a message to the X32
export function sendMessage(msg: any) {
if (ws && isConnected) {
ws.send(JSON.stringify(msg));
} else {
console.error("WebSocket not connected. Message not sent:", msg);
}
}
export function fetchValue(path: string): Promise<any> {
return new Promise((resolve) => {
sendMessage({action: "getValue", path});
// We'll listen for the matching response just once
const callback = (response: any) => {
if (response.action === "response" && response.path === path) {
removeMessageCallback(callback);
resolve(response.value);
}
};
registerMessageCallback(callback);
});
}
export function sendValue(path: string, value: any) {
sendMessage({action: "sendValue", path, value});
}
+79 -98
View File
@@ -1,101 +1,82 @@
import Image from "next/image";
"use client";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
import {useEffect, useMemo, useState} from "react";
import {fetchConfig} from "@/app/utils/fetchConfig";
import BusControlTabs from "@/app/components/BusControlTabs";
import MainMix from "@/app/components/MainMix";
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
// initialize global websocket connection
import {initConnection} from "@/app/lib/x32CommunicationManager";
const Page = () => {
const [appConfig, setAppConfig] = useState(null);
const [activeTab, setActiveTab] = useState("mainMix");
useEffect(() => {
initConnection(`ws://${window.location.hostname}:8080`);
fetchConfig().then(config => {
setAppConfig(config);
});
}, []);
const tabComponents = useMemo(() => {
return [
{
id: "mainMix",
label: "Main Mix",
content: (
<MainMix/>
),
},
{
id: "busControl",
label: "Bus Control",
content: (
<BusControlTabs/>
),
}
]
}
, []);
if (!appConfig) {
return <p>Error fetching config</p>;
}
return (
<div>
<div className="p-4">
{/* Tabs Header */}
<div className="flex border-b mb-4">
{tabComponents.map((tab) => (
<button
key={tab.id}
className={`px-4 py-2 ${
activeTab === tab.id
? "border-b-2 border-blue-500 text-blue-500"
: "text-gray-500"
}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
{/* Tabs Content */}
<div>
{tabComponents.map((tab) => (
<div
key={tab.id}
style={{display: activeTab === tab.id ? "block" : "none"}}
>
{tab.content}
</div>
))}
</div>
</div>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}
);
};
export default Page;
+5
View File
@@ -0,0 +1,5 @@
export interface Config {
polling_interval: number
inputs: any
bus_controls: any
}
+14
View File
@@ -0,0 +1,14 @@
export const fetchConfig = async <T>(): Promise<T | null> => {
try {
const response = await fetch("/api/config");
if (!response.ok) {
console.error(`Failed to fetch config:`, response.statusText);
return null;
}
return await response.json();
} catch (error) {
console.error(`Error fetching config from:`, error);
return null;
}
};