chore: release v0.21.3
This commit is contained in:
@@ -4528,6 +4528,16 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
}
|
||||
|
||||
function AudioPanel() {
|
||||
// Names the CAT backend the operator has configured, so the PTT dropdown can
|
||||
// say "CAT — Icom CI-V (USB)" rather than the flatly wrong "CAT (OmniRig)".
|
||||
const catBackendLabel = ({
|
||||
omnirig: t('cat.optOmnirig'),
|
||||
flex: t('cat.optFlex'),
|
||||
icom: t('cat.optIcom'),
|
||||
'icom-net': t('cat.optIcomNet'),
|
||||
tci: t('cat.optTci'),
|
||||
} as Record<string, string>)[catCfg.backend] ?? '';
|
||||
|
||||
const deviceSelect = (
|
||||
field: keyof AudioSettings,
|
||||
devices: AudioDev[],
|
||||
@@ -4539,9 +4549,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
>
|
||||
<SelectTrigger className="h-8"><SelectValue placeholder={placeholder} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_">— none / system default —</SelectItem>
|
||||
<SelectItem value="_">{t('aud.noneDefault')}</SelectItem>
|
||||
{devices.map((d) => (
|
||||
<SelectItem key={d.id} value={d.id}>{d.name}{d.default ? ' (default)' : ''}</SelectItem>
|
||||
<SelectItem key={d.id} value={d.id}>{d.name}{d.default ? ' ' + t('aud.defaultTag') : ''}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -4552,24 +4562,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<SectionHeader
|
||||
title={t('hw.audioVoice')}/>
|
||||
<Button variant="outline" size="sm" className="h-7 text-[11px] shrink-0" onClick={reloadAudioDevices}>
|
||||
Refresh devices
|
||||
{t('aud.refreshDevices')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-w-2xl">
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
<Label className="text-sm">From Radio (RX in)</Label>
|
||||
{deviceSelect('from_radio', audioInputs, 'Rig audio output → soundcard input')}
|
||||
<Label className="text-sm">To Radio (TX out)</Label>
|
||||
{deviceSelect('to_radio', audioOutputs, 'Soundcard output → rig mic/data in')}
|
||||
<Label className="text-sm">Recording mic</Label>
|
||||
{deviceSelect('recording_device', audioInputs, 'Your microphone (record DVK messages)')}
|
||||
<Label className="text-sm">Listening (preview)</Label>
|
||||
{deviceSelect('listening_device', audioOutputs, 'Local speakers for preview')}
|
||||
<Label className="text-sm">{t('aud.fromRadio')}</Label>
|
||||
{deviceSelect('from_radio', audioInputs, t('aud.phFromRadio'))}
|
||||
<Label className="text-sm">{t('aud.toRadio')}</Label>
|
||||
{deviceSelect('to_radio', audioOutputs, t('aud.phToRadio'))}
|
||||
<Label className="text-sm">{t('aud.recMic')}</Label>
|
||||
{deviceSelect('recording_device', audioInputs, t('aud.phRecMic'))}
|
||||
<Label className="text-sm">{t('aud.listening')}</Label>
|
||||
{deviceSelect('listening_device', audioOutputs, t('aud.phListening'))}
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<strong>From Radio</strong> = what you receive (used by the QSO recorder).{' '}
|
||||
<strong>To Radio</strong> = where voice-keyer messages are transmitted.
|
||||
<strong>{t('aud.fromRadioShort')}</strong> {t('aud.explainFrom')}{' '}
|
||||
<strong>{t('aud.toRadioShort')}</strong> {t('aud.explainTo')}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
@@ -4578,14 +4588,12 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
className="h-8"
|
||||
onClick={toggleMonitor}
|
||||
disabled={!monitorOn && !audioCfg.from_radio}
|
||||
title="Hear the rig's RX audio (From Radio) through your Listening device"
|
||||
title={t('aud.monitorTitle')}
|
||||
>
|
||||
{monitorOn ? '■ Stop listening' : '▶ Listen to radio'}
|
||||
{monitorOn ? t('aud.stopListening') : t('aud.listenRadio')}
|
||||
</Button>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{monitorOn
|
||||
? 'RX monitor running — From Radio → Listening device.'
|
||||
: 'Live-monitor the rig here (USB codec now; network audio later).'}
|
||||
{monitorOn ? t('aud.monitorOn') : t('aud.monitorHint')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -4595,101 +4603,106 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
className="h-8"
|
||||
onClick={toggleTX}
|
||||
disabled={!txOn && !audioCfg.to_radio}
|
||||
title="Key PTT and pipe your live mic into the rig (To Radio device)"
|
||||
title={t('aud.txTitle')}
|
||||
>
|
||||
{txOn ? '■ Stop talking (TX)' : '🎙 Talk to radio (TX)'}
|
||||
{txOn ? t('aud.stopTalk') : t('aud.talkRadio')}
|
||||
</Button>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{txOn
|
||||
? 'TRANSMITTING — mic → To Radio, PTT keyed. Click to stop.'
|
||||
: 'Live mic → rig with PTT (USB now; network TX later).'}
|
||||
{txOn ? t('aud.txOn') : t('aud.txHint')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-3 space-y-3 max-w-2xl">
|
||||
<h4 className="text-sm font-semibold text-foreground">QSO recorder</h4>
|
||||
<h4 className="text-sm font-semibold text-foreground">{t('aud.recorder')}</h4>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={audioCfg.qso_record} onCheckedChange={(c) => setAudioField({ qso_record: !!c })} />
|
||||
Record every QSO to an audio file (From Radio + your mic)
|
||||
{t('aud.recordEvery')}
|
||||
</label>
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
<Label className="text-sm">Recordings folder</Label>
|
||||
<Label className="text-sm">{t('aud.recFolder')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input value={audioCfg.qso_dir} onChange={(e) => setAudioField({ qso_dir: e.target.value })}
|
||||
placeholder="C:\…\OpsLog\Recordings" className="h-8 font-mono text-xs" />
|
||||
<Button variant="outline" size="sm" className="h-8 shrink-0"
|
||||
onClick={() => PickAudioFolder().then((d) => { if (d) setAudioField({ qso_dir: d }); }).catch(() => {})}>
|
||||
Browse…
|
||||
{t('aud.browse')}
|
||||
</Button>
|
||||
</div>
|
||||
<Label className="text-sm">Pre-roll (seconds)</Label>
|
||||
<Label className="text-sm">{t('aud.preroll')}</Label>
|
||||
<Input type="number" min={0} max={60} value={audioCfg.preroll_seconds}
|
||||
onChange={(e) => setAudioField({ preroll_seconds: Math.max(0, Math.min(60, parseInt(e.target.value, 10) || 0)) })}
|
||||
className="h-8 w-24 font-mono" />
|
||||
<Label className="text-sm">File format</Label>
|
||||
<Label className="text-sm">{t('aud.format')}</Label>
|
||||
<Select value={audioCfg.format} onValueChange={(v) => setAudioField({ format: v as any })}>
|
||||
<SelectTrigger className="h-8 w-40"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="wav">WAV (lossless, larger)</SelectItem>
|
||||
<SelectItem value="mp3">MP3 (compressed, small)</SelectItem>
|
||||
<SelectItem value="wav">{t('aud.wav')}</SelectItem>
|
||||
<SelectItem value="mp3">{t('aud.mp3')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="text-sm">From Radio level</Label>
|
||||
<Label className="text-sm">{t('aud.fromLevel')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="range" min={10} max={300} step={5} value={audioCfg.from_gain}
|
||||
onChange={(e) => setAudioField({ from_gain: parseInt(e.target.value, 10) })} className="w-48 accent-primary" />
|
||||
<span className="font-mono text-xs w-12 text-right">{audioCfg.from_gain}%</span>
|
||||
</div>
|
||||
<Label className="text-sm">Mic level</Label>
|
||||
<Label className="text-sm">{t('aud.micLevel')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="range" min={10} max={300} step={5} value={audioCfg.mic_gain}
|
||||
onChange={(e) => setAudioField({ mic_gain: parseInt(e.target.value, 10) })} className="w-48 accent-primary" />
|
||||
<span className="font-mono text-xs w-12 text-right">{audioCfg.mic_gain}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">If your voice is louder than the station, lower Mic level.</p>
|
||||
<p className="text-xs text-muted-foreground">{t('aud.levelHint')}</p>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={emailCfg.auto_send} onCheckedChange={(c) => setEmailField({ auto_send: !!c })} />
|
||||
Auto-send the recording to the station by e-mail when I log a QSO
|
||||
{t('aud.autoSend')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-3 space-y-2 max-w-2xl">
|
||||
<h4 className="text-sm font-semibold text-foreground">Voice keyer messages (F1–F6)</h4>
|
||||
<h4 className="text-sm font-semibold text-foreground">{t('aud.dvkTitle')}</h4>
|
||||
<div className="rounded-md border border-border/60 p-2.5 space-y-2">
|
||||
<div className="grid grid-cols-[120px_1fr] gap-2 items-center">
|
||||
<Label className="text-sm">PTT method</Label>
|
||||
<Label className="text-sm">{t('aud.pttMethod')}</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select value={audioCfg.ptt_method} onValueChange={(v) => setAudioField({ ptt_method: v as any })}>
|
||||
<SelectTrigger className="h-8 w-44"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None (VOX)</SelectItem>
|
||||
<SelectItem value="cat">CAT (OmniRig)</SelectItem>
|
||||
<SelectItem value="rts">Serial RTS</SelectItem>
|
||||
<SelectItem value="dtr">Serial DTR</SelectItem>
|
||||
<SelectItem value="none">{t('aud.pttNone')}</SelectItem>
|
||||
{/* This has ALWAYS driven whichever CAT backend is active —
|
||||
it calls the generic manager, and every backend (OmniRig,
|
||||
FlexRadio, Icom CI-V over USB and over the network, TCI)
|
||||
implements SetPTT. The label said "CAT (OmniRig)", so
|
||||
operators on a CI-V rig concluded there was no CAT PTT for
|
||||
them and reached for RTS/DTR or VOX instead. Name the
|
||||
backend actually configured. */}
|
||||
<SelectItem value="cat">{t('aud.pttCat')}{catBackendLabel ? ` — ${catBackendLabel}` : ''}</SelectItem>
|
||||
<SelectItem value="rts">{t('aud.pttRts')}</SelectItem>
|
||||
<SelectItem value="dtr">{t('aud.pttDtr')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{audioCfg.ptt_method !== 'none' && (
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={() => { setDvkErr(''); TestPTT(audioCfg as any).catch((e: any) => setDvkErr('PTT test: ' + String(e?.message ?? e))); }}>
|
||||
Test PTT
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={() => { setDvkErr(''); TestPTT(audioCfg as any).catch((e: any) => setDvkErr(t('aud.errPttTest') + String(e?.message ?? e))); }}>
|
||||
{t('aud.testPtt')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{(audioCfg.ptt_method === 'rts' || audioCfg.ptt_method === 'dtr') && (
|
||||
<>
|
||||
<Label className="text-sm">PTT COM port</Label>
|
||||
<Label className="text-sm">{t('aud.pttPort')}</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select value={audioCfg.ptt_port || '_'} onValueChange={(v) => setAudioField({ ptt_port: v === '_' ? '' : v })}>
|
||||
<SelectTrigger className="h-8 w-44"><SelectValue placeholder="Pick a COM port" /></SelectTrigger>
|
||||
<SelectTrigger className="h-8 w-44"><SelectValue placeholder={t('aud.pickPort')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_">— select —</SelectItem>
|
||||
<SelectItem value="_">{t('aud.selectPort')}</SelectItem>
|
||||
{wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="ghost" size="sm" className="h-8 text-[11px]"
|
||||
onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
|
||||
Refresh
|
||||
{t('aud.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -4706,7 +4719,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<span className="w-7 font-mono text-xs font-bold text-muted-foreground">F{m.slot}</span>
|
||||
<Input
|
||||
className="h-8 flex-1"
|
||||
placeholder={`Message ${m.slot} label (CQ, report, 73…)`}
|
||||
placeholder={t('aud.msgPlaceholder', { n: m.slot })}
|
||||
value={m.label}
|
||||
onChange={(e) => setDvkMsgs((ms) => ms.map((x) => x.slot === m.slot ? { ...x, label: e.target.value } : x))}
|
||||
onBlur={(e) => SetDVKLabel(m.slot, e.target.value).catch(() => {})}
|
||||
@@ -4722,21 +4735,21 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
setDvkErr('');
|
||||
DVKStartRecord(m.slot).catch((err) => setDvkErr('Record: ' + String(err?.message ?? err)));
|
||||
DVKStartRecord(m.slot).catch((err) => setDvkErr(t('aud.errRecord') + String(err?.message ?? err)));
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
DVKStopRecord().then(reloadDvk).catch((err) => setDvkErr('Save: ' + String(err?.message ?? err)));
|
||||
DVKStopRecord().then(reloadDvk).catch((err) => setDvkErr(t('aud.errSave') + String(err?.message ?? err)));
|
||||
}}
|
||||
>
|
||||
{recHere ? '● Recording…' : '● Hold to rec'}
|
||||
{recHere ? t('aud.recordingNow') : t('aud.holdRec')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline" size="sm" className="h-8 w-20 shrink-0"
|
||||
disabled={!m.has_audio || dvkStat.recording}
|
||||
onClick={() => (dvkStat.playing ? DVKStop() : DVKPreview(m.slot).catch((err) => setDvkErr('Play: ' + String(err?.message ?? err))))}
|
||||
onClick={() => (dvkStat.playing ? DVKStop() : DVKPreview(m.slot).catch((err) => setDvkErr(t('aud.errPlay') + String(err?.message ?? err))))}
|
||||
>
|
||||
{dvkStat.playing ? '■ Stop' : '▶ Play'}
|
||||
{dvkStat.playing ? t('aud.stop') : t('aud.play')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user