chore: release v0.26.3
This commit is contained in:
@@ -49,6 +49,7 @@ import {
|
||||
GetScpStatus, SetScpEnabled, DownloadScp,
|
||||
DownloadULSCounties, ULSStatus, BackfillUSCounties, BackfillRDA, RDADatabaseCount,
|
||||
GetCtyDatInfo, RefreshCtyDat, GetAwardReferenceMeta, UpdateAwardReferenceList,
|
||||
GetSpotColors, SaveSpotColors, ResetSpotColors, GetFlexZoom, SaveFlexZoom,
|
||||
ComputeStationInfo,
|
||||
GetUIPref, SetUIPref,
|
||||
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas, GetFlexBandPower, SaveFlexBandPower,
|
||||
@@ -1283,6 +1284,54 @@ function ComingSoon({ id, icon: Icon }: { id: SectionId; icon?: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
// The panadapter statuses, in the order the cluster ranks them: most wanted
|
||||
// first, then the markers that are orthogonal to the entity, then the two
|
||||
// "nothing here" cases. Must match spotColorOrder in spotcolors.go — the Go side
|
||||
// is the authority, this list only decides the order of the rows.
|
||||
const SPOT_COLOR_ROWS = [
|
||||
'new', 'new-band-mode', 'new-band', 'new-mode', 'new-slot',
|
||||
'new-pota', 'new-county', 'new-pfx',
|
||||
'my-call', 'worked', 'none',
|
||||
];
|
||||
|
||||
// SmartSDR colours are #AARRGGBB — the alpha FIRST, which is not what CSS or an
|
||||
// <input type="color"> expects. These two convert between the radio's order and
|
||||
// the browser's, so the operator picks a colour in a normal picker and the radio
|
||||
// gets what it understands.
|
||||
function argbToCss(argb?: string): string {
|
||||
const v = (argb ?? '').trim();
|
||||
if (!/^#[0-9a-fA-F]{8}$/.test(v)) return '';
|
||||
return `#${v.slice(3)}${v.slice(1, 3)}`; // #AARRGGBB → #RRGGBBAA
|
||||
}
|
||||
function cssToArgb(rgb: string, alpha: string): string {
|
||||
const v = (rgb || '#000000').trim();
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(v)) return '';
|
||||
return `#${alpha}${v.slice(1)}`.toUpperCase();
|
||||
}
|
||||
|
||||
// ArgbInput edits one #AARRGGBB value as what it really is: a colour and an
|
||||
// opacity. A single text field would be asking an operator to type eight hex
|
||||
// digits in an order no other program uses.
|
||||
function ArgbInput({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) {
|
||||
const valid = /^#[0-9a-fA-F]{8}$/.test((value ?? '').trim());
|
||||
const alpha = valid ? value.slice(1, 3).toUpperCase() : 'FF';
|
||||
const rgb = valid ? `#${value.slice(3)}` : '#808080';
|
||||
return (
|
||||
<span className="flex items-center gap-1 shrink-0" title={label}>
|
||||
<input type="color" value={rgb}
|
||||
onChange={(e) => onChange(cssToArgb(e.target.value, alpha))}
|
||||
className="size-6 rounded border border-border bg-transparent p-0 cursor-pointer" />
|
||||
<select value={alpha}
|
||||
onChange={(e) => onChange(cssToArgb(rgb, e.target.value))}
|
||||
className="h-6 rounded border border-border bg-background text-[10px] px-0.5">
|
||||
{[['FF', '100%'], ['C0', '75%'], ['80', '50%'], ['40', '25%'], ['20', '12%'], ['00', 'off']].map(([a, l]) => (
|
||||
<option key={a} value={a}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// FlexBandPanel — everything that follows the band, in ONE row per band:
|
||||
// antennas and TX power.
|
||||
//
|
||||
@@ -1747,6 +1796,26 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
};
|
||||
// cty.dat is not downloaded by OpsLog — it is shipped and reloaded from disk —
|
||||
// so the page reports it rather than offering a button it cannot honour.
|
||||
// Panadapter spot palette. Saved on its own — it is a table of colours, not a
|
||||
// form: waiting for the modal's Save to see a colour on the waterfall would
|
||||
// make picking one a guessing game.
|
||||
type SpotColor = { text: string; bg?: string };
|
||||
const [spotColors, setSpotColors] = useState<{ enabled: boolean; colors: Record<string, SpotColor> }>({ enabled: true, colors: {} });
|
||||
useEffect(() => { GetSpotColors().then((c: any) => setSpotColors(c ?? { enabled: true, colors: {} })).catch(() => {}); }, []);
|
||||
const saveSpotColors = (next: { enabled: boolean; colors: Record<string, SpotColor> }) => {
|
||||
setSpotColors(next);
|
||||
SaveSpotColors(next as any).catch(() => {});
|
||||
};
|
||||
const setSpotColor = (k: string, patch: Partial<SpotColor>) => saveSpotColors({
|
||||
...spotColors,
|
||||
colors: { ...spotColors.colors, [k]: { ...(spotColors.colors[k] ?? { text: '' }), ...patch } },
|
||||
});
|
||||
// Panadapter auto-zoom. Saved immediately for the same reason as the palette:
|
||||
// the only way to judge a width is to click a spot and look at the radio.
|
||||
const [flexZoom, setFlexZoom] = useState<{ enabled: boolean; cw_khz: number; ssb_khz: number; digi_khz: number; centre: boolean }>(
|
||||
{ enabled: false, cw_khz: 25, ssb_khz: 200, digi_khz: 50, centre: false });
|
||||
useEffect(() => { GetFlexZoom().then((z: any) => setFlexZoom(z)).catch(() => {}); }, []);
|
||||
const saveFlexZoom = (next: typeof flexZoom) => { setFlexZoom(next); SaveFlexZoom(next as any).catch(() => {}); };
|
||||
const [ctyInfo, setCtyInfo] = useState<{ entities: number; file_mod_time?: string }>({ entities: 0 });
|
||||
const [ctyBusy, setCtyBusy] = useState(false);
|
||||
useEffect(() => { GetCtyDatInfo().then((i) => setCtyInfo(i as any)).catch(() => {}); }, []);
|
||||
@@ -2983,27 +3052,11 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className="col-span-2">
|
||||
<FlexDiscover onPick={(ip, port) => setCatCfg((s) => ({ ...s, flex_host: ip, flex_port: port }))} />
|
||||
</div>
|
||||
<label className="col-span-2 flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!catCfg.flex_spots} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, flex_spots: !!c }))} />
|
||||
{t('cat.flexSpots')} <span className="text-xs text-muted-foreground">{t('cat.flexSpotsHint')}</span>
|
||||
</label>
|
||||
<label className="col-span-2 flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!catCfg.flex_decode_spots} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, flex_decode_spots: !!c }))} />
|
||||
{t('cat.flexDecodeSpots')} <span className="text-xs text-muted-foreground">{t('cat.flexDecodeSpotsHint')}</span>
|
||||
</label>
|
||||
{catCfg.flex_decode_spots && (
|
||||
<div className="col-span-2 flex items-center gap-2 pl-6">
|
||||
<Label className="text-sm">{t('cat.flexDecodeSecs')}</Label>
|
||||
<Input type="number" className="w-24" min={10} max={3600}
|
||||
value={catCfg.flex_decode_secs || 120}
|
||||
onChange={(e) => setCatCfg((s) => ({ ...s, flex_decode_secs: parseInt(e.target.value) || 120 }))} />
|
||||
<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>
|
||||
{/* What OpsLog DOES with a Flex — panadapter spots, decode spots,
|
||||
the DAX switch for voice messages — moved to Settings →
|
||||
FlexRadio, with the per-band antennas and power. What stays
|
||||
here is the link itself: an address and a port, which is what
|
||||
this page is about for every other backend too. */}
|
||||
</>
|
||||
)}
|
||||
{catCfg.backend === 'xiegu' && (
|
||||
@@ -3480,7 +3533,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
|
||||
function UltrabeamPanel() {
|
||||
const isSteppir = ultrabeam.type === 'steppir';
|
||||
const isSerial = isSteppir && ultrabeam.transport === 'serial';
|
||||
// Serial is no longer a SteppIR-only route: an Ultrabeam controller has an
|
||||
// RS232 port too, and a plain FTDI cable reaches it without the Ethernet
|
||||
// adapter the TCP route needs.
|
||||
const isSerial = ultrabeam.transport === 'serial';
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
@@ -3494,10 +3550,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('hw.motorType')}</Label>
|
||||
{/* Ultrabeam is TCP only; picking it forces the transport back to TCP
|
||||
so the serial fields never apply to it. */}
|
||||
{/* Both antennas take either route now, so picking a type no longer
|
||||
forces the transport. */}
|
||||
<Select value={ultrabeam.type ?? 'ultrabeam'}
|
||||
onValueChange={(v) => setUltrabeam((s) => ({ ...s, type: v, transport: v === 'ultrabeam' ? 'tcp' : s.transport }))}>
|
||||
onValueChange={(v) => setUltrabeam((s) => ({ ...s, type: v }))}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ultrabeam">Ultrabeam</SelectItem>
|
||||
@@ -3505,7 +3561,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{isSteppir && (
|
||||
{(
|
||||
<div className="space-y-1">
|
||||
<Label>{t('hw.motorTransport')}</Label>
|
||||
<Select value={ultrabeam.transport ?? 'tcp'}
|
||||
@@ -4728,7 +4784,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{t(`ftx.c_${k}`)}
|
||||
</label>
|
||||
);
|
||||
const KEYS: (keyof AutoCallCriteria)[] = ['dxcc', 'band', 'mode', 'slot', 'grid', 'county', 'pota', 'pfx'];
|
||||
const KEYS: (keyof AutoCallCriteria)[] = ['dxcc', 'bandmode', 'band', 'mode', 'slot', 'grid', 'county', 'pota', 'pfx'];
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={t('sec.ftx')} hint={t('ftx.hint')} />
|
||||
@@ -4898,7 +4954,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium">{t('clu.macros')}</span>
|
||||
<p className="text-xs text-muted-foreground">{t('clu.macrosHint')}</p>
|
||||
</div>
|
||||
{/* Two columns of six: twelve rows stacked would push everything else
|
||||
in this panel off the screen. */}
|
||||
@@ -5012,7 +5067,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('gsc.hint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5074,7 +5128,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">km</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('bo.nearKmHint')}</p>
|
||||
|
||||
{/* A live count, because a feed that is connected but silent looks
|
||||
exactly like one that is broken until a number moves. */}
|
||||
@@ -5117,7 +5170,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">{t('clu.selfSpotHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7150,7 +7202,110 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
tunergenius: TunerGeniusPanelSettings,
|
||||
psu: PSUPanelSettings,
|
||||
pgxl: PGXLPanelSettings,
|
||||
flex: () => <FlexBandPanel bands={lists.bands ?? []} />,
|
||||
flex: () => (
|
||||
<div className="space-y-6">
|
||||
<FlexBandPanel bands={lists.bands ?? []} />
|
||||
{/* Rendered here rather than inside FlexBandPanel so these keep writing
|
||||
to the modal's own CAT settings, saved by its Save button. A panel
|
||||
that saved them itself would be overwritten by that same Save a
|
||||
moment later, using the values it had read on opening. */}
|
||||
<div className="border-t border-border/60 pt-4 space-y-2">
|
||||
<h4 className="text-sm font-semibold text-foreground">{t('cat.flexOptions')}</h4>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!catCfg.flex_decode_spots} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, flex_decode_spots: !!c }))} />
|
||||
{t('cat.flexDecodeSpots')} <span className="text-xs text-muted-foreground">{t('cat.flexDecodeSpotsHint')}</span>
|
||||
</label>
|
||||
{catCfg.flex_decode_spots && (
|
||||
<div className="flex items-center gap-2 pl-6">
|
||||
<Label className="text-sm">{t('cat.flexDecodeSecs')}</Label>
|
||||
<Input type="number" className="w-24" min={10} max={3600}
|
||||
value={catCfg.flex_decode_secs || 120}
|
||||
onChange={(e) => setCatCfg((s) => ({ ...s, flex_decode_secs: parseInt(e.target.value) || 120 }))} />
|
||||
<span className="text-xs text-muted-foreground">{t('cat.flexDecodeSecsHint')}</span>
|
||||
</div>
|
||||
)}
|
||||
<label className="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>
|
||||
</div>
|
||||
|
||||
{/* Auto-zoom on a spot click. Its own block: it changes the RADIO's
|
||||
display, where everything above changes what OpsLog draws on it. */}
|
||||
<div className="border-t border-border/60 pt-4 space-y-2">
|
||||
<h4 className="text-sm font-semibold text-foreground">{t('flexzoom.title')}</h4>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={flexZoom.enabled} onCheckedChange={(c) => saveFlexZoom({ ...flexZoom, enabled: !!c })} />
|
||||
{t('flexzoom.enable')}
|
||||
</label>
|
||||
{flexZoom.enabled && (
|
||||
<div className="pl-6 space-y-2">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{([['cw_khz', 'flexzoom.cw'], ['ssb_khz', 'flexzoom.ssb'], ['digi_khz', 'flexzoom.digi']] as const).map(([k, lbl]) => (
|
||||
<label key={k} className="flex items-center gap-1.5 text-xs">
|
||||
<span className="text-muted-foreground">{t(lbl)}</span>
|
||||
<Input type="number" min={1} max={14000} className="h-8 w-20"
|
||||
value={String((flexZoom as any)[k] ?? '')}
|
||||
onChange={(e) => saveFlexZoom({ ...flexZoom, [k]: parseInt(e.target.value, 10) || 0 } as any)} />
|
||||
<span className="text-muted-foreground">kHz</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className="flex items-start gap-2 text-xs cursor-pointer">
|
||||
<Checkbox className="mt-0.5" checked={flexZoom.centre} onCheckedChange={(c) => saveFlexZoom({ ...flexZoom, centre: !!c })} />
|
||||
<span>{t('flexzoom.centre')} <span className="text-muted-foreground">{t('flexzoom.centreHint')}</span></span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Spot colours, one row per status.
|
||||
|
||||
Saved on the spot rather than on the modal's Save: this is a table of
|
||||
colours, and having to close a dialog to see what a colour looks like
|
||||
on the waterfall turns picking one into a guessing game. */}
|
||||
<div className="border-t border-border/60 pt-4 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="text-sm font-semibold text-foreground">{t('spotcol.title')}</h4>
|
||||
<button type="button" className="text-[11px] text-muted-foreground hover:text-foreground underline"
|
||||
onClick={() => ResetSpotColors().then((c: any) => setSpotColors(c)).catch(() => {})}>
|
||||
{t('spotcol.reset')}
|
||||
</button>
|
||||
</div>
|
||||
{/* Whether spots are drawn at all comes first: colouring them is the
|
||||
next question, and the two were on different pages. */}
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!catCfg.flex_spots} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, flex_spots: !!c }))} />
|
||||
{t('cat.flexSpots')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={spotColors.enabled}
|
||||
onCheckedChange={(c) => saveSpotColors({ ...spotColors, enabled: !!c })} />
|
||||
{t('spotcol.enable')}
|
||||
</label>
|
||||
{spotColors.enabled && (
|
||||
<div className="rounded-md border border-border divide-y divide-border max-w-2xl">
|
||||
{SPOT_COLOR_ROWS.map((k) => {
|
||||
const c = spotColors.colors[k] ?? { text: '', bg: '' };
|
||||
return (
|
||||
<div key={k} className="flex items-center gap-3 px-3 py-1.5">
|
||||
<span className="text-xs flex-1 min-w-0">{t('spotcol.s_' + k)}</span>
|
||||
{/* The preview is the point of the row: two colour boxes say
|
||||
nothing about what the pair looks like together. */}
|
||||
<span className="text-xs font-mono px-2 py-0.5 rounded"
|
||||
style={{ color: argbToCss(c.text) || undefined, background: argbToCss(c.bg) || undefined }}>
|
||||
DL1ABC
|
||||
</span>
|
||||
<ArgbInput label={t('spotcol.text')} value={c.text} onChange={(v) => setSpotColor(k, { text: v })} />
|
||||
<ArgbInput label={t('spotcol.bg')} value={c.bg ?? ''} onChange={(v) => setSpotColor(k, { bg: v })} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
audio: AudioPanel,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user