feat: sidebar allows recall, rename and display custom names
This commit is contained in:
109
src/App.tsx
109
src/App.tsx
@ -1,8 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { useState } from 'react';
|
||||
import './App.css';
|
||||
import { DSP, DSPStatus, AppPage } from './types/types';
|
||||
import { DSP, AppPage } from './types/types';
|
||||
import TopBar from './components/Topbar/Topbar';
|
||||
import DSPPage from './pages/DspPage/DspPage';
|
||||
import AddDSPForm from './pages/AddDspPage/AddDspPage';
|
||||
@ -10,8 +8,7 @@ import { useNotifications } from './hooks/useNotifications';
|
||||
import Notifications from './components/Notifications/Notifications';
|
||||
import CreditsPage from './pages/CreditsPage/CreditsPage';
|
||||
import HomePage from './pages/HomePage/HomePage';
|
||||
import { getDSP408State, connectDSP408, disconnectDSP408 } from './api/dsp408';
|
||||
import { useDSPPolling } from './hooks/useDSPPolling';
|
||||
import { useDSP408 } from './hooks/useDSP408';
|
||||
|
||||
function App() {
|
||||
const [dsps, setDsps] = useState<DSP[]>([]);
|
||||
@ -21,92 +18,14 @@ function App() {
|
||||
const activeDsp = dsps.find((d) => d.id === selected) ?? null;
|
||||
const connectedDspCount = dsps.filter((dsp) => dsp.status === 'connected').length;
|
||||
|
||||
useDSPPolling(dsps, selected, setDsps);
|
||||
|
||||
function setStatus(id: number, status: DSPStatus) {
|
||||
setDsps((prev) => prev.map((d) => (d.id === id ? { ...d, status } : d)));
|
||||
}
|
||||
|
||||
async function addDSP(dsp: Omit<DSP, 'id' | 'status' | 'state' | 'meters'>) {
|
||||
try {
|
||||
const id = await invoke<number>('create_dsp408', {
|
||||
ip: dsp.ip,
|
||||
port: dsp.port,
|
||||
deviceId: Number(dsp.deviceId),
|
||||
});
|
||||
|
||||
const newDsp: DSP = {
|
||||
id,
|
||||
...dsp,
|
||||
status: 'disconnected',
|
||||
state: null,
|
||||
meters: null,
|
||||
};
|
||||
|
||||
setDsps((prev) => [...prev, newDsp]);
|
||||
setSelected(id);
|
||||
setPage('view-dsp');
|
||||
|
||||
notify('Added successfully', 'success', dsp.name);
|
||||
} catch (err) {
|
||||
notify(`Connection failed: ${String(err)}`, 'error', dsp.name);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDSP(dsp: DSP) {
|
||||
try {
|
||||
let id = dsp.id;
|
||||
await invoke('remove_dsp408', {
|
||||
id,
|
||||
});
|
||||
|
||||
setDsps((prev) => {
|
||||
const next = prev.filter((d) => d.id !== id);
|
||||
|
||||
if (selected === id) {
|
||||
setSelected(next.length ? next[0].id : null);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
notify('Removed successfully', 'success', dsp?.name);
|
||||
} catch (err) {
|
||||
notify(`Remove failed: ${String(err)}`, 'error', dsp?.name);
|
||||
}
|
||||
}
|
||||
|
||||
async function connectDSP(dsp: DSP) {
|
||||
setStatus(dsp.id, 'connecting');
|
||||
|
||||
try {
|
||||
await connectDSP408(dsp.id);
|
||||
|
||||
setStatus(dsp.id, 'connected');
|
||||
|
||||
const state = await getDSP408State(dsp.id);
|
||||
|
||||
setDsps((prev) => prev.map((d) => (d.id === dsp.id ? { ...d, state } : d)));
|
||||
|
||||
notify('Connected', 'success', dsp.name);
|
||||
} catch (err) {
|
||||
notify(`Connection failed: ${String(err)}`, 'error', dsp.name);
|
||||
|
||||
setStatus(dsp.id, 'disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectDSP(dsp: DSP) {
|
||||
try {
|
||||
await disconnectDSP408(dsp.id);
|
||||
|
||||
setStatus(dsp.id, 'disconnected');
|
||||
|
||||
notify('Disconnected', 'success', dsp.name);
|
||||
} catch (err) {
|
||||
notify(`Disconnect failed: ${String(err)}`, 'error', dsp.name);
|
||||
}
|
||||
}
|
||||
const dsp408 = useDSP408(
|
||||
dsps,
|
||||
setDsps,
|
||||
selected,
|
||||
setSelected,
|
||||
setPage,
|
||||
notify
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
@ -128,7 +47,7 @@ function App() {
|
||||
setPage('view-dsp');
|
||||
}}
|
||||
|
||||
onRemove={removeDSP}
|
||||
onRemove={dsp408.removeDSP}
|
||||
|
||||
onAddClick={() => {
|
||||
setPage('add-dsp');
|
||||
@ -139,13 +58,11 @@ function App() {
|
||||
{page === 'credits' ? (
|
||||
<CreditsPage />
|
||||
) : page === 'add-dsp' ? (
|
||||
<AddDSPForm onCancel={() => setPage('home')} onAdd={addDSP} />
|
||||
<AddDSPForm onCancel={() => setPage('home')} onAdd={dsp408.addDSP} />
|
||||
) : activeDsp ? (
|
||||
<DSPPage
|
||||
dsp={activeDsp}
|
||||
onConnect={() => connectDSP(activeDsp)}
|
||||
onDisconnect={() => disconnectDSP(activeDsp)}
|
||||
notify={notify}
|
||||
dsp408={dsp408.forDSP(activeDsp)}
|
||||
/>
|
||||
) : (
|
||||
<HomePage
|
||||
|
||||
@ -16,10 +16,16 @@ export const disconnectDSP408 = (id: number) => invoke('disconnect_dsp408', { id
|
||||
|
||||
export const getDSP408State = (id: number) => invoke<DSPState>('get_dsp_state', { id });
|
||||
|
||||
export const recallPreset = (id: number, presetIndex: number) =>
|
||||
export const recallPresetDSP408 = (id: number, presetIndex: number) =>
|
||||
invoke<boolean>('recall_preset', {
|
||||
id,
|
||||
presetIndex,
|
||||
});
|
||||
|
||||
export const DSP408SetCurrentPresetName = (id: number, name: string) =>
|
||||
invoke<boolean>('set_current_preset_name', {
|
||||
id,
|
||||
name,
|
||||
});
|
||||
|
||||
export const getDSP408MeterLevels = (id: number) => invoke<Meters>('get_meter_levels', { id });
|
||||
|
||||
@ -50,6 +50,7 @@ function AppCombobox({
|
||||
onAppRename?: (name: string) => void;
|
||||
}) {
|
||||
const currentOption = appOptions.find((o) => o.id === selectedAppId) ?? appOptions[0];
|
||||
const canRename = currentOption?.id !== 'f00';
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
@ -91,8 +92,12 @@ function AppCombobox({
|
||||
|
||||
function startEditing(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!canRename) return;
|
||||
|
||||
setEditing(true);
|
||||
setOpen(false);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
@ -100,17 +105,30 @@ function AppCombobox({
|
||||
}
|
||||
|
||||
function commitRename() {
|
||||
if (!canRename) {
|
||||
setInputValue(currentOption?.name ?? appName);
|
||||
setEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = inputValue.trim();
|
||||
|
||||
setEditing(false);
|
||||
|
||||
if (!trimmed) {
|
||||
setInputValue(currentOption?.name ?? appName);
|
||||
return;
|
||||
}
|
||||
|
||||
const matched = appOptions.find((o) => o.name === trimmed);
|
||||
|
||||
if (matched) {
|
||||
if (matched.id !== selectedAppId) onAppChange?.(matched.id);
|
||||
if (matched.id !== selectedAppId) {
|
||||
onAppChange?.(matched.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed !== currentOption?.name) {
|
||||
onAppRename?.(trimmed);
|
||||
}
|
||||
@ -153,7 +171,8 @@ function AppCombobox({
|
||||
ref={inputRef}
|
||||
className="sidebar-app-input"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
maxLength={14}
|
||||
onChange={(e) => setInputValue(e.target.value.slice(0, 14))}
|
||||
onBlur={commitRename}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label="Rename app"
|
||||
|
||||
@ -57,7 +57,7 @@ function ConnectionPanel({
|
||||
|
||||
<div className="connection-panel__spec">
|
||||
<dt>Device ID</dt>
|
||||
<dd className="connection-panel__mono" title={dsp.deviceId || undefined}>
|
||||
<dd className="connection-panel__mono" title={String(dsp.deviceId) || undefined}>
|
||||
{dsp.deviceId || '—'}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
553
src/hooks/useDSP408.ts
Normal file
553
src/hooks/useDSP408.ts
Normal file
@ -0,0 +1,553 @@
|
||||
// hooks/useDSP408.ts
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { DSP, DSPStatus, AppPage } from '../types/types';
|
||||
|
||||
import {
|
||||
createDSP408,
|
||||
removeDSP408,
|
||||
connectDSP408,
|
||||
disconnectDSP408,
|
||||
getDSP408State,
|
||||
recallPresetDSP408,
|
||||
DSP408SetCurrentPresetName,
|
||||
getDSP408MeterLevels,
|
||||
} from '../api/dsp408';
|
||||
|
||||
type NotifyFunction = (
|
||||
message: string,
|
||||
type: 'success' | 'error',
|
||||
dspName?: string
|
||||
) => void;
|
||||
|
||||
type SetDsps = React.Dispatch<React.SetStateAction<DSP[]>>;
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
|
||||
function updateDSP(
|
||||
setDsps: SetDsps,
|
||||
dspId: number,
|
||||
updater: (dsp: DSP) => DSP
|
||||
) {
|
||||
setDsps((prev) =>
|
||||
prev.map((dsp) => (dsp.id === dspId ? updater(dsp) : dsp))
|
||||
);
|
||||
}
|
||||
|
||||
export type DSP408Device = {
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
recallPreset: (index: number) => Promise<boolean>;
|
||||
setCurrentPresetName: (name: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Polling
|
||||
// ============================================================
|
||||
|
||||
function useDSPPolling(
|
||||
dsps: DSP[],
|
||||
selected: number | null,
|
||||
setDsps: React.Dispatch<React.SetStateAction<DSP[]>>
|
||||
) {
|
||||
const dspsRef = useRef(dsps);
|
||||
const selectedRef = useRef(selected);
|
||||
|
||||
useEffect(() => {
|
||||
dspsRef.current = dsps;
|
||||
}, [dsps]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedRef.current = selected;
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
let stopped = false;
|
||||
let activeTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
let inactiveTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const pollDSP = async (dsp: DSP) => {
|
||||
// Don't poll paused DSPs
|
||||
if (dsp.pollingPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const meters = await getDSP408MeterLevels(dsp.id);
|
||||
|
||||
if (stopped) return;
|
||||
|
||||
setDsps((prev) => prev.map((d) => (d.id === dsp.id ? { ...d, meters } : d)));
|
||||
} catch (err) {
|
||||
if (!stopped) {
|
||||
console.error(`Meter polling failed for ${dsp.name}:`, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const activeLoop = async () => {
|
||||
if (stopped) return;
|
||||
|
||||
const activeId = selectedRef.current;
|
||||
|
||||
if (activeId !== null) {
|
||||
const dsp = dspsRef.current.find(
|
||||
(d) => d.id === activeId && d.status === 'connected' && !d.pollingPaused
|
||||
);
|
||||
|
||||
if (dsp) {
|
||||
await pollDSP(dsp);
|
||||
}
|
||||
}
|
||||
|
||||
if (!stopped) {
|
||||
activeTimeout = setTimeout(activeLoop, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const inactiveLoop = async () => {
|
||||
if (stopped) return;
|
||||
|
||||
const activeId = selectedRef.current;
|
||||
|
||||
const inactiveDsps = dspsRef.current.filter(
|
||||
(dsp) => dsp.status === 'connected' && dsp.id !== activeId && !dsp.pollingPaused
|
||||
);
|
||||
|
||||
await Promise.all(inactiveDsps.map((dsp) => pollDSP(dsp)));
|
||||
|
||||
if (!stopped) {
|
||||
inactiveTimeout = setTimeout(inactiveLoop, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling
|
||||
activeLoop();
|
||||
inactiveLoop();
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
|
||||
if (activeTimeout !== undefined) {
|
||||
clearTimeout(activeTimeout);
|
||||
}
|
||||
|
||||
if (inactiveTimeout !== undefined) {
|
||||
clearTimeout(inactiveTimeout);
|
||||
}
|
||||
};
|
||||
}, [setDsps]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main hook
|
||||
// ============================================================
|
||||
|
||||
export function useDSP408(
|
||||
dsps: DSP[],
|
||||
setDsps: SetDsps,
|
||||
selected: number | null,
|
||||
setSelected: React.Dispatch<React.SetStateAction<number | null>>,
|
||||
setPage: React.Dispatch<React.SetStateAction<AppPage>>,
|
||||
notify: NotifyFunction
|
||||
) {
|
||||
// ----------------------------------------------------------
|
||||
// Polling control
|
||||
// ----------------------------------------------------------
|
||||
|
||||
const setPollingPaused = (id: number, paused: boolean) => {
|
||||
updateDSP(setDsps, id, (dsp) => ({
|
||||
...dsp,
|
||||
pollingPaused: paused,
|
||||
}));
|
||||
};
|
||||
|
||||
// Polling is part of the hook.
|
||||
useDSPPolling(dsps, selected, setDsps);
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Generic operation wrapper
|
||||
// ----------------------------------------------------------
|
||||
|
||||
async function withPollingPaused<T>(
|
||||
dsp: DSP,
|
||||
operation: () => Promise<T>
|
||||
): Promise<T> {
|
||||
setPollingPaused(dsp.id, true);
|
||||
|
||||
try {
|
||||
return await operation();
|
||||
} finally {
|
||||
setPollingPaused(dsp.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Connection
|
||||
// ==========================================================
|
||||
|
||||
async function connectDSP(dsp: DSP) {
|
||||
setPollingPaused(dsp.id, true);
|
||||
|
||||
updateDSP(setDsps, dsp.id, (d) => ({
|
||||
...d,
|
||||
status: 'connecting',
|
||||
}));
|
||||
|
||||
try {
|
||||
await connectDSP408(dsp.id);
|
||||
|
||||
const state = await getDSP408State(dsp.id);
|
||||
|
||||
updateDSP(setDsps, dsp.id, (d) => ({
|
||||
...d,
|
||||
status: 'connected',
|
||||
state,
|
||||
}));
|
||||
|
||||
notify('Connected', 'success', dsp.name);
|
||||
} catch (err) {
|
||||
updateDSP(setDsps, dsp.id, (d) => ({
|
||||
...d,
|
||||
status: 'disconnected',
|
||||
}));
|
||||
|
||||
notify(
|
||||
`Connection failed: ${String(err)}`,
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
} finally {
|
||||
setPollingPaused(dsp.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectDSP(dsp: DSP) {
|
||||
setPollingPaused(dsp.id, true);
|
||||
|
||||
try {
|
||||
await disconnectDSP408(dsp.id);
|
||||
|
||||
updateDSP(setDsps, dsp.id, (d) => ({
|
||||
...d,
|
||||
status: 'disconnected',
|
||||
}));
|
||||
|
||||
notify('Disconnected', 'success', dsp.name);
|
||||
} catch (err) {
|
||||
// If disconnect failed, it is probably still connected.
|
||||
setPollingPaused(dsp.id, false);
|
||||
|
||||
notify(
|
||||
`Disconnect failed: ${String(err)}`,
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Add / Remove
|
||||
// ==========================================================
|
||||
|
||||
async function addDSP(
|
||||
dsp: Omit<
|
||||
DSP,
|
||||
'id' | 'status' | 'state' | 'meters' | 'pollingPaused'
|
||||
>
|
||||
) {
|
||||
try {
|
||||
const id = await createDSP408(
|
||||
dsp.ip,
|
||||
dsp.port,
|
||||
dsp.deviceId
|
||||
);
|
||||
|
||||
const newDsp: DSP = {
|
||||
id,
|
||||
...dsp,
|
||||
status: 'disconnected',
|
||||
state: null,
|
||||
meters: null,
|
||||
pollingPaused: true,
|
||||
};
|
||||
|
||||
setDsps((prev) => [...prev, newDsp]);
|
||||
setSelected(id);
|
||||
setPage('view-dsp');
|
||||
|
||||
notify('Added successfully', 'success', dsp.name);
|
||||
} catch (err) {
|
||||
notify(
|
||||
`Connection failed: ${String(err)}`,
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDSP(dsp: DSP) {
|
||||
setPollingPaused(dsp.id, true);
|
||||
|
||||
try {
|
||||
await removeDSP408(dsp.id);
|
||||
|
||||
setDsps((prev) => {
|
||||
const next = prev.filter(
|
||||
(d) => d.id !== dsp.id
|
||||
);
|
||||
|
||||
if (selected === dsp.id) {
|
||||
setSelected(
|
||||
next.length ? next[0].id : null
|
||||
);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
notify(
|
||||
'Removed successfully',
|
||||
'success',
|
||||
dsp.name
|
||||
);
|
||||
} catch (err) {
|
||||
setPollingPaused(dsp.id, false);
|
||||
|
||||
notify(
|
||||
`Remove failed: ${String(err)}`,
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Presets
|
||||
// ==========================================================
|
||||
|
||||
async function recallPreset(
|
||||
dsp: DSP,
|
||||
presetIndex: number
|
||||
) {
|
||||
return withPollingPaused(dsp, async () => {
|
||||
try {
|
||||
const success = await recallPresetDSP408(
|
||||
dsp.id,
|
||||
presetIndex
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
notify(
|
||||
'Failed to recall preset',
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
updateDSP(setDsps, dsp.id, (d) => {
|
||||
if (!d.state) return d;
|
||||
|
||||
return {
|
||||
...d,
|
||||
state: {
|
||||
...d.state,
|
||||
presets: {
|
||||
...d.state.presets,
|
||||
current_index: presetIndex - 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
notify(
|
||||
`Preset ${presetIndex} recalled`,
|
||||
'success',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
notify(
|
||||
`Preset recall failed: ${String(err)}`,
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function setCurrentPresetName(
|
||||
dsp: DSP,
|
||||
name: string
|
||||
) {
|
||||
return withPollingPaused(dsp, async () => {
|
||||
try {
|
||||
const paddedName = name
|
||||
.slice(0, 14)
|
||||
.padEnd(14, ' ');
|
||||
|
||||
const success =
|
||||
await DSP408SetCurrentPresetName(
|
||||
dsp.id,
|
||||
paddedName
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
notify(
|
||||
'Failed to rename preset',
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
updateDSP(setDsps, dsp.id, (d) => {
|
||||
if (!d.state) return d;
|
||||
|
||||
const presets = d.state.presets;
|
||||
const index = presets.current_index;
|
||||
|
||||
return {
|
||||
...d,
|
||||
state: {
|
||||
...d.state,
|
||||
presets: {
|
||||
...presets,
|
||||
names: presets.names.map(
|
||||
(presetName, i) =>
|
||||
i === index
|
||||
? paddedName
|
||||
: presetName
|
||||
),
|
||||
modified: presets.modified.map(
|
||||
(modified, i) =>
|
||||
i === index
|
||||
? true
|
||||
: modified
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
notify(
|
||||
'Preset renamed',
|
||||
'success',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
notify(
|
||||
`Preset rename failed: ${String(err)}`,
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Gain
|
||||
// ==========================================================
|
||||
|
||||
async function setGain(
|
||||
dsp: DSP,
|
||||
channel: number,
|
||||
gain: number
|
||||
) {
|
||||
return withPollingPaused(dsp, async () => {
|
||||
try {
|
||||
const success = await invoke<boolean>(
|
||||
'set_gain',
|
||||
{
|
||||
id: dsp.id,
|
||||
channel,
|
||||
gain,
|
||||
}
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
notify(
|
||||
'Failed to set gain',
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
updateDSP(setDsps, dsp.id, (d) => {
|
||||
if (!d.state) return d;
|
||||
|
||||
return {
|
||||
...d,
|
||||
state: {
|
||||
...d.state,
|
||||
// Update gain state here
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
notify(
|
||||
'Gain updated',
|
||||
'success',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
notify(
|
||||
`Gain update failed: ${String(err)}`,
|
||||
'error',
|
||||
dsp.name
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Return API
|
||||
// ==========================================================
|
||||
|
||||
function forDSP(dsp: DSP): DSP408Device {
|
||||
return {
|
||||
connect: () => connectDSP(dsp),
|
||||
disconnect: () => disconnectDSP(dsp),
|
||||
recallPreset: (index: number) => recallPreset(dsp, index),
|
||||
setCurrentPresetName: (name: string) =>
|
||||
setCurrentPresetName(dsp, name),
|
||||
// setGain: (channel, gain) =>
|
||||
// setGain(dsp, channel, gain),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
forDSP,
|
||||
|
||||
addDSP,
|
||||
removeDSP,
|
||||
|
||||
connectDSP,
|
||||
disconnectDSP,
|
||||
|
||||
recallPreset,
|
||||
setCurrentPresetName,
|
||||
|
||||
setGain,
|
||||
};
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DSP } from '../types/types';
|
||||
import { getDSP408MeterLevels } from '../api/dsp408';
|
||||
|
||||
export function useDSPPolling(
|
||||
dsps: DSP[],
|
||||
selected: number | null,
|
||||
setDsps: React.Dispatch<React.SetStateAction<DSP[]>>
|
||||
) {
|
||||
const dspsRef = useRef(dsps);
|
||||
const selectedRef = useRef(selected);
|
||||
|
||||
// Keep refs up to date without restarting the polling loops
|
||||
useEffect(() => {
|
||||
dspsRef.current = dsps;
|
||||
}, [dsps]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedRef.current = selected;
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
let stopped = false;
|
||||
|
||||
const pollDSP = async (dsp: DSP) => {
|
||||
try {
|
||||
const meters = await getDSP408MeterLevels(dsp.id);
|
||||
|
||||
if (stopped) return;
|
||||
|
||||
setDsps((prev) => prev.map((d) => (d.id === dsp.id ? { ...d, meters } : d)));
|
||||
} catch (err) {
|
||||
if (!stopped) {
|
||||
console.error(`Meter polling failed for ${dsp.name}:`, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const activeLoop = async () => {
|
||||
if (stopped) return;
|
||||
|
||||
const activeId = selectedRef.current;
|
||||
|
||||
if (activeId !== null) {
|
||||
const dsp = dspsRef.current.find((d) => d.id === activeId && d.status === 'connected');
|
||||
|
||||
if (dsp) {
|
||||
await pollDSP(dsp);
|
||||
}
|
||||
}
|
||||
|
||||
if (!stopped) {
|
||||
setTimeout(activeLoop, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const inactiveLoop = async () => {
|
||||
if (stopped) return;
|
||||
|
||||
const activeId = selectedRef.current;
|
||||
|
||||
const inactiveDsps = dspsRef.current.filter(
|
||||
(dsp) => dsp.status === 'connected' && dsp.id !== activeId
|
||||
);
|
||||
|
||||
// Poll inactive connected DSPs
|
||||
await Promise.all(inactiveDsps.map((dsp) => pollDSP(dsp)));
|
||||
|
||||
if (!stopped) {
|
||||
setTimeout(inactiveLoop, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
// Start both loops
|
||||
activeLoop();
|
||||
inactiveLoop();
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
};
|
||||
}, [setDsps]);
|
||||
}
|
||||
@ -10,18 +10,22 @@ function AddDSPForm({
|
||||
onAdd,
|
||||
}: {
|
||||
onCancel: () => void;
|
||||
onAdd: (d: Omit<DSP, 'status' | 'id' | 'state' | 'meters'>) => void;
|
||||
onAdd: (d: Omit<DSP, 'status' | 'id' | 'state' | 'meters' | 'pollingPaused'>) => void;
|
||||
}) {
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
type: DSP_TYPES[0].name,
|
||||
ip: '192.168.1.100',
|
||||
port: DSP_TYPES[0].defaultPort,
|
||||
deviceId: '1',
|
||||
deviceId: 1,
|
||||
});
|
||||
|
||||
const canSubmit =
|
||||
form.name.trim().length > 0 && form.ip.trim().length > 0 && form.deviceId.length > 0;
|
||||
form.name.trim().length > 0 &&
|
||||
form.ip.trim().length > 0 &&
|
||||
Number.isInteger(form.deviceId) &&
|
||||
form.deviceId >= 1 &&
|
||||
form.deviceId <= 255;
|
||||
|
||||
function submit() {
|
||||
if (!canSubmit) return;
|
||||
@ -82,7 +86,7 @@ function AddDSPForm({
|
||||
step={1}
|
||||
placeholder="1"
|
||||
value={form.deviceId}
|
||||
onChange={(e) => setForm({ ...form, deviceId: e.target.value })}
|
||||
onChange={(e) => setForm({ ...form, deviceId: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,27 +4,25 @@ import Sidebar from '../../components/Sidebar/Sidebar';
|
||||
import { InfoIcon, KnobGainIcon } from '../../assets/icons/AudioIcons';
|
||||
import ConnectionPanel from '../../components/dsp408/ConnectionSection/ConnectionSection';
|
||||
import GainSection from '../../components/dsp408/GainSection/GainSection';
|
||||
import { NotificationType } from '../../types/types';
|
||||
import { DSP408Device } from '../../hooks/useDSP408';
|
||||
import './DspPage.css';
|
||||
import GateSection from '../../components/dsp408/GateSection/GateSection';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
|
||||
import { InputChannel, OutputChannel } from '../../types/dsp408State';
|
||||
|
||||
function DSPPage({
|
||||
dsp,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
notify,
|
||||
dsp408,
|
||||
}: {
|
||||
dsp: DSP;
|
||||
onConnect: () => void;
|
||||
onDisconnect: () => void;
|
||||
notify: (message: string, type?: NotificationType, dspName?: string) => void;
|
||||
dsp408: DSP408Device;
|
||||
}) {
|
||||
const [activeSidebar, setActiveSidebar] = useState('overview');
|
||||
const [sidebarExpanded, setSidebarExpanded] = useState(false);
|
||||
const [selectedPreset, setSelectedPreset] = useState(dsp.state?.presets.current_index ?? 0);
|
||||
const [presetLoading, setPresetLoading] = useState(false);
|
||||
const inputStates = dsp.state?.current_config.input_states;
|
||||
const outputStates = dsp.state?.current_config.output_states;
|
||||
const sidebarItems = [
|
||||
{
|
||||
id: 'overview',
|
||||
@ -68,62 +66,63 @@ function DSPPage({
|
||||
},
|
||||
{
|
||||
id: 'ina',
|
||||
label: 'In A',
|
||||
label: `In A (${inputStates?.[InputChannel.InA]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'inb',
|
||||
label: 'In B',
|
||||
label: `In B (${inputStates?.[InputChannel.InB]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'inc',
|
||||
label: 'In C',
|
||||
label: `In C (${inputStates?.[InputChannel.InC]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'ind',
|
||||
label: 'In D (TESTTEST)',
|
||||
label: `In D (${inputStates?.[InputChannel.InD]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'out1',
|
||||
label: 'Out 1',
|
||||
label: `Out 1 (${outputStates?.[OutputChannel.Out1]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'out2',
|
||||
label: 'Out 2',
|
||||
label: `Out 2 (${outputStates?.[OutputChannel.Out2]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'out3',
|
||||
label: 'Out 3',
|
||||
label: `Out 3 (${outputStates?.[OutputChannel.Out3]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'out4',
|
||||
label: 'Out 4',
|
||||
label: `Out 4 (${outputStates?.[OutputChannel.Out4]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'out5',
|
||||
label: 'Out 5',
|
||||
label: `Out 5 (${outputStates?.[OutputChannel.Out5]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'out6',
|
||||
label: 'Out 6',
|
||||
label: `Out 6 (${outputStates?.[OutputChannel.Out6]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'out7',
|
||||
label: 'Out 7',
|
||||
label: `Out 7 (${outputStates?.[OutputChannel.Out7]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
id: 'out8',
|
||||
label: 'Out 8',
|
||||
label: `Out 8 (${outputStates?.[OutputChannel.Out8]?.name})`,
|
||||
icon: <KnobGainIcon />,
|
||||
},
|
||||
{
|
||||
@ -154,52 +153,14 @@ function DSPPage({
|
||||
})),
|
||||
];
|
||||
|
||||
const recallPreset = async (presetIndex: number) => {
|
||||
try {
|
||||
const success = await invoke<boolean>('recall_preset', {
|
||||
id: dsp.id,
|
||||
presetIndex,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
notify(`Preset ${presetIndex} recalled`, 'success', dsp.name);
|
||||
} else {
|
||||
notify('Failed to recall preset', 'error', dsp.name);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (err) {
|
||||
notify(`Preset recall failed: ${String(err)}`, 'error', dsp.name);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setCurrentPresetName = async (name: string) => {
|
||||
try {
|
||||
const success = await invoke<boolean>('set_current_preset_name', {
|
||||
id: dsp.id,
|
||||
name,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
notify(`Preset renamed`, 'success', dsp.name);
|
||||
} else {
|
||||
notify('Failed to rename preset', 'error', dsp.name);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (err) {
|
||||
notify(`Preset rename failed: ${String(err)}`, 'error', dsp.name);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// const { recallPreset, setCurrentPresetName } = useDSPPresets(dsp, setDsps, notify);
|
||||
|
||||
const onAppChange = async (id: string) => {
|
||||
const presetIndex = presets.findIndex((preset) => preset.id === id);
|
||||
if (presetIndex === -1) return;
|
||||
|
||||
setPresetLoading(true);
|
||||
const success = await recallPreset(presetIndex);
|
||||
const success = await dsp408.recallPreset(presetIndex);
|
||||
if (success) {
|
||||
setSelectedPreset(presetIndex);
|
||||
}
|
||||
@ -209,13 +170,13 @@ function DSPPage({
|
||||
const renderTab = () => {
|
||||
switch (activeSidebar) {
|
||||
case 'overview':
|
||||
return <ConnectionPanel dsp={dsp} onConnect={onConnect} onDisconnect={onDisconnect} />;
|
||||
return <ConnectionPanel dsp={dsp} onConnect={dsp408.connect} onDisconnect={dsp408.disconnect} />;
|
||||
|
||||
case 'gain':
|
||||
return <GainSection dsp={dsp} notify={notify} />;
|
||||
// case 'gain':
|
||||
// return <GainSection dsp={dsp} notify={notify} />;
|
||||
|
||||
case 'gate':
|
||||
return <GateSection dsp={dsp} notify={notify} />;
|
||||
// case 'gate':
|
||||
// return <GateSection dsp={dsp} notify={notify} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
@ -232,7 +193,7 @@ function DSPPage({
|
||||
appOptions={presets}
|
||||
selectedAppId={presets[selectedPreset]?.id}
|
||||
onAppChange={onAppChange}
|
||||
onAppRename={setCurrentPresetName}
|
||||
onAppRename={dsp408.setCurrentPresetName}
|
||||
expanded={sidebarExpanded}
|
||||
onToggle={() => setSidebarExpanded((v) => !v)}
|
||||
/>
|
||||
|
||||
@ -8,10 +8,11 @@ export type DSP = {
|
||||
type: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
deviceId: string;
|
||||
deviceId: number;
|
||||
status: DSPStatus;
|
||||
state: DSPState | null;
|
||||
meters: Meters | null;
|
||||
pollingPaused: boolean;
|
||||
};
|
||||
|
||||
export const DSP_TYPES = [
|
||||
|
||||
Reference in New Issue
Block a user