chore: release v0.25.9
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
GetCATSettings, SaveCATSettings, DiscoverFlexRadios,
|
||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||
GetRotorPresets, SaveRotorPresets, ResetRotorPresets,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||
@@ -1399,16 +1400,25 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [bandDraft, setBandDraft] = useState('');
|
||||
const [modeDraft, setModeDraft] = useState('');
|
||||
const [catCfg, setCatCfg] = useState<CATSettings>({
|
||||
enabled: false, backend: 'omnirig', omnirig_rig: 1, omnirig_vfo: '', flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120,
|
||||
enabled: false, backend: 'omnirig', omnirig_rig: 1, omnirig_vfo: '', flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120, flex_dvk_dax: false,
|
||||
yaesu_port: '', yaesu_baud: 38400, yaesu_low_lines: false, kenwood_low_lines: false, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', kenwood_data_mode: 'usb', xiegu_port: '', xiegu_baud: 19200, xiegu_addr: 0x70, xiegu_ptt_line: '',
|
||||
icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false,
|
||||
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
|
||||
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0, offset_on: false, offset_hz: 0,
|
||||
digital_default: 'FT8', share_enabled: false, share_port: 4532, share_proto: 'rigctl', share_tci_port: 40001,
|
||||
ptt_hotkey_enabled: false, ptt_hotkey: '', ptt_hotkey_toggle: false,
|
||||
});
|
||||
// While true, the next key press is captured as the PTT hotkey.
|
||||
const [capturingPtt, setCapturingPtt] = useState(false);
|
||||
const [rotors, setRotors] = useState<RotatorDevice[]>([]);
|
||||
const [rotorPresets, setRotorPresets] = useState<{ label: string; azimuth: number }[]>([]);
|
||||
// Whether the presets have actually been READ back yet.
|
||||
//
|
||||
// An empty list is a legitimate answer — an operator may want no quick-turn
|
||||
// buttons at all — so the backend stores it as one. That makes "not loaded
|
||||
// yet" and "deliberately none" the same value, and a Save landing in the first
|
||||
// state wiped the buttons for good. Saving is gated on having read them, so
|
||||
// the only empty list that can ever reach the store is one the operator made.
|
||||
const [rotorPresetsLoaded, setRotorPresetsLoaded] = useState(false);
|
||||
const [rotatorTesting, setRotatorTesting] = useState(false);
|
||||
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
|
||||
@@ -1825,6 +1835,11 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
await reloadClusterServers();
|
||||
setCatCfg(c);
|
||||
setRotors((r ?? []) as any);
|
||||
// Loaded HERE, in the loader that runs on mount — not only in the
|
||||
// event-driven one below. Missing from this one, the state stayed empty
|
||||
// on a normal open and Save then wrote an empty list over the operator's
|
||||
// buttons. See rotorPresetsLoaded for the belt to this brace.
|
||||
try { setRotorPresets((((await GetRotorPresets()) ?? []) as any)); setRotorPresetsLoaded(true); } catch {}
|
||||
try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
|
||||
@@ -1868,6 +1883,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
try { setLookup(await GetLookupSettings() as any); } catch {}
|
||||
try { setCatCfg(await GetCATSettings() as any); } catch {}
|
||||
try { setRotors(((await GetRotators()) ?? []) as any); } catch {}
|
||||
try { setRotorPresets((((await GetRotorPresets()) ?? []) as any)); setRotorPresetsLoaded(true); } catch {}
|
||||
try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
|
||||
@@ -2062,6 +2078,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
await SaveLookupSettings(lookup as any);
|
||||
await SaveCATSettings(catCfg as any);
|
||||
await SaveRotators(rotors as any);
|
||||
// Only once they have been read back — see rotorPresetsLoaded.
|
||||
if (rotorPresetsLoaded) await SaveRotorPresets(rotorPresets as any);
|
||||
await SaveUltrabeamSettings(ultrabeam as any);
|
||||
await SaveAntGeniusSettings(antgenius as any);
|
||||
await SaveTunerGeniusSettings(tunergenius as any);
|
||||
@@ -2862,6 +2880,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<span className="text-xs text-muted-foreground">{t('cat.flexDecodeSecsHint')}</span>
|
||||
</div>
|
||||
)}
|
||||
<label className="col-span-2 flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox className="mt-0.5" checked={!!catCfg.flex_dvk_dax} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, flex_dvk_dax: !!c }))} />
|
||||
<span>{t('cat.flexDvkDax')} <span className="text-xs text-muted-foreground">{t('cat.flexDvkDaxHint')}</span></span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
{catCfg.backend === 'xiegu' && (
|
||||
@@ -3142,6 +3164,28 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* Transverter offset. OUTSIDE the per-backend blocks on purpose: a
|
||||
transverter hangs off the IF of whatever radio you own, and the
|
||||
offset is applied in the CAT manager, above every backend. Tucked
|
||||
under the OmniRig/Icom section it would have been invisible to the
|
||||
Yaesu, Kenwood, Xiegu, Flex and TCI operators who need it just as
|
||||
much. */}
|
||||
<label className="col-span-2 flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox className="mt-0.5" checked={!!catCfg.offset_on}
|
||||
onCheckedChange={(c) => setCatCfg((s) => ({ ...s, offset_on: !!c }))} />
|
||||
<span>{t('cat.offsetOn')} <span className="text-xs text-muted-foreground">{t('cat.offsetHint')}</span></span>
|
||||
</label>
|
||||
{catCfg.offset_on && (
|
||||
<div className="col-span-2 flex items-center gap-2 pl-6">
|
||||
<Label className="text-sm">{t('cat.offsetMhz')}</Label>
|
||||
<Input
|
||||
type="number" step="0.000001" className="w-40"
|
||||
value={(catCfg.offset_hz ?? 0) / 1e6}
|
||||
onChange={(e) => setCatCfg((s) => ({ ...s, offset_hz: Math.round((parseFloat(e.target.value) || 0) * 1e6) }))}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">{t('cat.offsetExample')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label>{t('cat.digitalDefault')}</Label>
|
||||
<Select
|
||||
@@ -4135,6 +4179,41 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick-turn buttons for the rotor widget. Their azimuths start out
|
||||
computed from the station square, so they are right for THIS QTH
|
||||
rather than copied from someone else's. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<div className="text-sm font-semibold">{t('rot.presets')}</div>
|
||||
<p className="text-xs text-muted-foreground">{t('rot.presetsHint')}</p>
|
||||
<div className="space-y-1.5">
|
||||
{rotorPresets.map((p, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<Input className="w-24" maxLength={6} value={p.label}
|
||||
placeholder={t('rot.presetName')}
|
||||
onChange={(e) => setRotorPresets((list) => list.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />
|
||||
<Input className="w-24" type="number" min={0} max={359} value={p.azimuth}
|
||||
onChange={(e) => setRotorPresets((list) => list.map((x, j) => (j === i ? { ...x, azimuth: Math.max(0, Math.min(359, parseInt(e.target.value, 10) || 0)) } : x)))} />
|
||||
<span className="text-xs text-muted-foreground">°</span>
|
||||
<Button variant="ghost" size="sm" title={t('rot.remove')}
|
||||
onClick={() => setRotorPresets((list) => list.filter((_, j) => j !== i))}>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{rotorPresets.length < 8 && (
|
||||
<Button variant="outline" size="sm" onClick={() => setRotorPresets((l) => [...l, { label: '', azimuth: 0 }])}>
|
||||
<Plus className="size-3.5 mr-1" /> {t('rot.presetAdd')}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={() => { ResetRotorPresets().then((p) => setRotorPresets((p ?? []) as any)).catch((e) => setErr(String(e?.message ?? e))); }}>
|
||||
{t('rot.presetsReset')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{rotatorTest && (
|
||||
<div className={cn(
|
||||
'text-xs rounded-md p-2.5 border',
|
||||
|
||||
Reference in New Issue
Block a user