feat: add MiscellaneousPanel - need to implement UI

This commit is contained in:
2026-09-13 19:09:47 +02:00
parent 4f2bce1f5f
commit 00b6721c42
5 changed files with 366 additions and 58 deletions
+27
View File
@@ -224,7 +224,34 @@ pub async fn copy_channel(
}
// Command `0x2D` -- TODO
#[tauri::command]
pub async fn unlock_device(
state: State<'_, AppState>,
id: u64,
password: String,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.unlock_device(&password)
.map_err(|e| e.to_string())
})
}
// Command `0x2F` -- TODO
#[tauri::command]
pub async fn lock_device(
state: State<'_, AppState>,
id: u64,
password: String,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.lock_device(&password)
.map_err(|e| e.to_string())
})
}
// Command `0x30`
#[tauri::command]
@@ -16,6 +16,7 @@ import { DSP408Api } from '../../dsp408';
import { DSP408 } from '../../../../types';
import { SidebarId } from '../../constants';
import './DSP408Panel.css';
import MiscellaneousPanel from '../MiscellaneousSection/MiscellaneousSection';
function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
const [activeSidebar, setActiveSidebar] = useState<SidebarId>('overview');
@@ -81,7 +82,7 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
{ id: 'miscellaneous', label: 'Miscellaneous', icon: <TextIcon>Out 1</TextIcon> },
];
const presets = dsp.state?.presets.names
const presets = dsp.state.presets.names
? [
{ id: 'f00', name: 'Factory Preset' },
...dsp.state.presets.names.map((name, index) => ({
@@ -107,6 +108,35 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
setPresetLoading(false);
};
const renderChannel = (channel: { Input: InputChannel } | { Output: OutputChannel }) => (
<ChannelSection
dsp={dsp}
channel={channel}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
const channelMap: Partial<
Record<SidebarId, { Input: InputChannel } | { Output: OutputChannel }>
> = {
ina: { Input: InputChannel.InA },
inb: { Input: InputChannel.InB },
inc: { Input: InputChannel.InC },
ind: { Input: InputChannel.InD },
out1: { Output: OutputChannel.Out1 },
out2: { Output: OutputChannel.Out2 },
out3: { Output: OutputChannel.Out3 },
out4: { Output: OutputChannel.Out4 },
out5: { Output: OutputChannel.Out5 },
out6: { Output: OutputChannel.Out6 },
out7: { Output: OutputChannel.Out7 },
out8: { Output: OutputChannel.Out8 },
};
const renderTab = () => {
switch (activeSidebar) {
case 'overview':
@@ -147,68 +177,25 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
case 'geq':
return <GeqSection dsp={dsp} setGeqBand={api.setGeqBand} setGeqBypass={api.setGeqBypass} />;
case 'ina':
case 'miscellaneous':
return (
<ChannelSection
<MiscellaneousPanel
dsp={dsp}
channel={{ Input: InputChannel.InA }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
onConnect={api.connect}
onDisconnect={api.disconnect}
factoryReset={api.factoryReset}
copyChannel={api.copyChannel}
setInputSource={api.setInputSource}
linkChannels={api.linkChannels}
unlockDevice={api.unlockDevice}
lockDevice={api.lockDevice}
/>
);
case 'inb':
return (
<ChannelSection
dsp={dsp}
channel={{ Input: InputChannel.InB }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'inc':
return (
<ChannelSection
dsp={dsp}
channel={{ Input: InputChannel.InC }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'ind':
return (
<ChannelSection
dsp={dsp}
channel={{ Input: InputChannel.InD }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'out1':
return (
<ChannelSection
dsp={dsp}
channel={{ Output: OutputChannel.Out1 }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
default:
return null;
default: {
const channel = channelMap[activeSidebar];
return channel ? renderChannel(channel) : null;
}
}
};
@@ -0,0 +1,110 @@
.connection-section {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-user-select: none;
}
.connection-panel {
width: 340px;
display: flex;
flex-direction: column;
}
.connection-panel__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
min-width: 0;
}
.connection-panel__header-text {
flex: 1;
min-width: 0;
}
.connection-panel__title {
margin: 0;
font-size: var(--font-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.connection-panel__type {
margin-top: var(--space-1);
font-size: var(--font-sm);
color: var(--text-muted);
font-family: var(--font-mono);
letter-spacing: 0.03em;
}
.connection-panel__specs {
margin: var(--space-5) 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.connection-panel__spec {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
}
.connection-panel__spec dt {
color: var(--text-muted);
font-size: var(--font-sm);
}
.connection-panel__spec dd {
margin: 0;
color: var(--text-primary);
}
.connection-panel__mono {
font-family: var(--font-mono);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.connection-panel__actions {
margin-top: auto;
display: flex;
justify-content: flex-end;
}
.connection-panel__status {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
.connection-panel__led {
height: 14px;
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
@@ -0,0 +1,140 @@
import { useState } from 'react';
import { DSP } from '../../../../types';
import { Channel, InputSource } from '../../types';
import Button from '../../../../components/Button/Button';
import Card from '../../../../components/Card/Card';
import Led from '../../../../components/Led/Led';
import './MiscellaneousSection.css';
function MiscellaneousPanel({
dsp,
onConnect,
onDisconnect,
factoryReset,
copyChannel,
setInputSource,
linkChannels,
unlockDevice,
lockDevice,
}: {
dsp: DSP;
onConnect: () => void;
onDisconnect: () => void;
factoryReset: (force: boolean) => Promise<boolean>;
copyChannel: (channel1: Channel, channel2: Channel) => Promise<boolean>;
setInputSource: (inputSource: InputSource) => Promise<boolean>;
linkChannels: (source: Channel, destinations: Channel[]) => Promise<boolean>;
unlockDevice: (password: string) => Promise<boolean>;
lockDevice: (password: string) => Promise<boolean>;
}) {
const [isFactoryResetting, setIsFactoryResetting] = useState(false);
const statusLabel =
dsp.status === 'connected'
? 'Connected'
: dsp.status === 'connecting'
? 'Connecting…'
: 'Disconnected';
const handleFactoryReset = async () => {
const confirmed = window.confirm(
'Are you sure you want to factory reset this DSP? This cannot be undone.',
);
if (!confirmed) {
return;
}
setIsFactoryResetting(true);
try {
await factoryReset(true);
} finally {
setIsFactoryResetting(false);
}
};
return (
<div className="connection-section">
<Card className="connection-panel">
<div className="connection-panel__header">
<div className="connection-panel__header-text">
<h1 className="connection-panel__title" title={dsp.name}>
{dsp.name}
</h1>
<div className="connection-panel__type">{dsp.type}</div>
</div>
<div className="connection-panel__status">
<span className="connection-panel__led">
<Led status={dsp.status} />
</span>
<span
className={`status-label status-label--${dsp.status}`}
>
{statusLabel}
</span>
</div>
</div>
<dl className="connection-panel__specs">
<div className="connection-panel__spec">
<dt>IP address</dt>
<dd className="connection-panel__mono" title={dsp.ip}>
{dsp.ip}
</dd>
</div>
<div className="connection-panel__spec">
<dt>Port</dt>
<dd className="connection-panel__mono">{dsp.port}</dd>
</div>
<div className="connection-panel__spec">
<dt>Device ID</dt>
<dd
className="connection-panel__mono"
title={String(dsp.deviceId) || undefined}
>
{dsp.deviceId || '—'}
</dd>
</div>
</dl>
<div className="connection-panel__actions">
{dsp.status === 'connected' ? (
<Button variant="outline" onClick={onDisconnect}>
Disconnect
</Button>
) : (
<Button
variant="primary"
onClick={onConnect}
disabled={dsp.status === 'connecting'}
>
{dsp.status === 'connecting' ? 'Connecting…' : 'Connect'}
</Button>
)}
</div>
<div className="miscellaneous-panel__actions">
<Button
variant="outline"
onClick={handleFactoryReset}
disabled={
dsp.status !== 'connected' || isFactoryResetting
}
>
{isFactoryResetting ? 'Resetting…' : 'Factory Reset'}
</Button>
</div>
</Card>
</div>
);
}
export default MiscellaneousPanel;
+44
View File
@@ -844,5 +844,49 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
}
});
},
async unlockDevice(password: string): Promise<boolean> {
return withPollingPaused(dsp, setDsps, async () => {
try {
const success = await invoke<boolean>('unlock_device', {
id: dsp.id,
password,
});
if (!success) {
return false;
}
await updateDSPState();
return true;
} catch (err) {
notify(`Unlock failed: ${String(err)}`, 'error', dsp.name);
return false;
}
});
},
async lockDevice(password: string): Promise<boolean> {
return withPollingPaused(dsp, setDsps, async () => {
try {
const success = await invoke<boolean>('lock_device', {
id: dsp.id,
password,
});
if (!success) {
return false;
}
await updateDSPState();
return true;
} catch (err) {
notify(`Lock failed: ${String(err)}`, 'error', dsp.name);
return false;
}
});
},
};
}