feat: add MiscellaneousPanel - need to implement UI
This commit is contained in:
@@ -224,7 +224,34 @@ pub async fn copy_channel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Command `0x2D` -- TODO
|
// 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
|
// 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`
|
// Command `0x30`
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { DSP408Api } from '../../dsp408';
|
|||||||
import { DSP408 } from '../../../../types';
|
import { DSP408 } from '../../../../types';
|
||||||
import { SidebarId } from '../../constants';
|
import { SidebarId } from '../../constants';
|
||||||
import './DSP408Panel.css';
|
import './DSP408Panel.css';
|
||||||
|
import MiscellaneousPanel from '../MiscellaneousSection/MiscellaneousSection';
|
||||||
|
|
||||||
function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
||||||
const [activeSidebar, setActiveSidebar] = useState<SidebarId>('overview');
|
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> },
|
{ 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' },
|
{ id: 'f00', name: 'Factory Preset' },
|
||||||
...dsp.state.presets.names.map((name, index) => ({
|
...dsp.state.presets.names.map((name, index) => ({
|
||||||
@@ -107,6 +108,35 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
|||||||
setPresetLoading(false);
|
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 = () => {
|
const renderTab = () => {
|
||||||
switch (activeSidebar) {
|
switch (activeSidebar) {
|
||||||
case 'overview':
|
case 'overview':
|
||||||
@@ -147,68 +177,25 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
|||||||
case 'geq':
|
case 'geq':
|
||||||
return <GeqSection dsp={dsp} setGeqBand={api.setGeqBand} setGeqBypass={api.setGeqBypass} />;
|
return <GeqSection dsp={dsp} setGeqBand={api.setGeqBand} setGeqBypass={api.setGeqBypass} />;
|
||||||
|
|
||||||
case 'ina':
|
case 'miscellaneous':
|
||||||
return (
|
return (
|
||||||
<ChannelSection
|
<MiscellaneousPanel
|
||||||
dsp={dsp}
|
dsp={dsp}
|
||||||
channel={{ Input: InputChannel.InA }}
|
onConnect={api.connect}
|
||||||
setLowPass={api.setLowPass}
|
onDisconnect={api.disconnect}
|
||||||
setHighPass={api.setHighPass}
|
factoryReset={api.factoryReset}
|
||||||
setPeq={api.setPeqBand}
|
copyChannel={api.copyChannel}
|
||||||
setPeqBypass={api.setPEQBypass}
|
setInputSource={api.setInputSource}
|
||||||
|
linkChannels={api.linkChannels}
|
||||||
|
unlockDevice={api.unlockDevice}
|
||||||
|
lockDevice={api.lockDevice}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'inb':
|
default: {
|
||||||
return (
|
const channel = channelMap[activeSidebar];
|
||||||
<ChannelSection
|
return channel ? renderChannel(channel) : null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user