up
This commit is contained in:
@@ -299,6 +299,20 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [band, containerH, currentFreqHz, range, lo, hi]);
|
||||
|
||||
// Re-centre on the rig frequency whenever the zoom level changes — zooming
|
||||
// would otherwise drift away from where you're operating. Skips the initial
|
||||
// mount (the band-centre effect above handles that).
|
||||
const firstZoomRef = useRef(true);
|
||||
useEffect(() => {
|
||||
if (firstZoomRef.current) { firstZoomRef.current = false; return; }
|
||||
const el = scrollerRef.current;
|
||||
if (!el || !range || containerH <= 0) return;
|
||||
const kHz = currentFreqHz && currentFreqHz / 1000 >= lo && currentFreqHz / 1000 <= hi
|
||||
? currentFreqHz / 1000 : (lo + hi) / 2;
|
||||
el.scrollTop = Math.max(0, freqToY(kHz) - containerH / 2);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [zoomIdx]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
@@ -11,10 +11,21 @@ interface Props {
|
||||
busy: boolean;
|
||||
currentBand: string;
|
||||
currentMode: string;
|
||||
bands?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS
|
||||
}
|
||||
|
||||
// 13-column band layout — no 4m, no SHF (per user preference).
|
||||
const BANDS: { tag: string; label: string }[] = [
|
||||
// Compact column label for a band tag: keep the classic V/U for 2m/70cm,
|
||||
// strip the trailing "m" for meter bands (160m→160), and shorten cm bands
|
||||
// (13cm→13c) so the column stays narrow.
|
||||
function bandColLabel(tag: string): string {
|
||||
if (tag === '2m') return 'V';
|
||||
if (tag === '70cm') return 'U';
|
||||
if (tag.endsWith('cm')) return tag.replace('cm', 'c');
|
||||
return tag.replace(/m$/, '');
|
||||
}
|
||||
|
||||
// Default 13-column band layout, used when the operator hasn't configured bands.
|
||||
const DEFAULT_BANDS: { tag: string; label: string }[] = [
|
||||
{ tag: '160m', label: '160' },
|
||||
{ tag: '80m', label: '80' },
|
||||
{ tag: '60m', label: '60' },
|
||||
@@ -67,7 +78,13 @@ function cellTitle(band: string, cls: string, status: string, current: boolean):
|
||||
return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`;
|
||||
}
|
||||
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode }: Props) {
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands }: Props) {
|
||||
// Columns from the operator's configured bands (so the matrix shows only the
|
||||
// bands they actually use), falling back to the built-in default set.
|
||||
const cols = useMemo(
|
||||
() => (bands && bands.length ? bands.map((tag) => ({ tag, label: bandColLabel(tag) })) : DEFAULT_BANDS),
|
||||
[bands],
|
||||
);
|
||||
const dxcc = wb?.dxcc ?? 0;
|
||||
const dxccName = wb?.dxcc_name ?? '';
|
||||
const dxccCount = wb?.dxcc_count ?? 0;
|
||||
@@ -136,7 +153,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode }: Props) {
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-[26px]" />
|
||||
{BANDS.map((b) => (
|
||||
{cols.map((b) => (
|
||||
<th
|
||||
key={b.tag}
|
||||
className={cn(
|
||||
@@ -162,7 +179,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode }: Props) {
|
||||
>
|
||||
{cls}
|
||||
</th>
|
||||
{BANDS.map((b) => {
|
||||
{cols.map((b) => {
|
||||
const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
|
||||
const isCurrent = b.tag === currentBand && classCurrent;
|
||||
return (
|
||||
|
||||
@@ -55,6 +55,7 @@ interface Props {
|
||||
wbBusy?: boolean;
|
||||
band: string;
|
||||
mode: string;
|
||||
bands?: string[]; // configured bands for the worked-before matrix columns
|
||||
imageUrl?: string;
|
||||
onOpenImage?: () => void;
|
||||
// Optional controlled active tab (so the app can switch it via keyboard).
|
||||
@@ -117,7 +118,7 @@ function Field({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid, details, onChange, wb, wbBusy, band, mode, tab, onTab, keyerActive }: Props) {
|
||||
export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid, details, onChange, wb, wbBusy, band, mode, bands, tab, onTab, keyerActive }: Props) {
|
||||
const [internalOpen, setInternalOpen] = useState<TabName>('stats');
|
||||
const open = tab ?? internalOpen; // controlled when `tab` is provided
|
||||
// Bearing/distance from operator's home grid to the remote station.
|
||||
@@ -181,7 +182,7 @@ export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid,
|
||||
<div className="overflow-y-auto min-h-0">
|
||||
{open === 'stats' && (
|
||||
<div className="px-3 py-2.5">
|
||||
<BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} />
|
||||
<BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} bands={bands} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -196,9 +197,16 @@ export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid,
|
||||
<Field label="Prefix">
|
||||
<Input className="font-mono uppercase" value={prefix} readOnly tabIndex={-1} />
|
||||
</Field>
|
||||
{/* DXCC #, CQ zone, ITU zone, Continent and Azimuth SP live in the
|
||||
main entry strip — visible without opening F2. F2 keeps the
|
||||
less-needed long-path bearing and both distances. */}
|
||||
<Field label="CQ zone">
|
||||
<Input inputMode="numeric" maxLength={2} className="font-mono" value={details.cqz ?? ''} placeholder="—"
|
||||
onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); onChange({ cqz: v === '' ? undefined : parseInt(v, 10) }); }} />
|
||||
</Field>
|
||||
<Field label="ITU zone">
|
||||
<Input inputMode="numeric" maxLength={2} className="font-mono" value={details.ituz ?? ''} placeholder="—"
|
||||
onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); onChange({ ituz: v === '' ? undefined : parseInt(v, 10) }); }} />
|
||||
</Field>
|
||||
{/* DXCC #, Continent and Azimuth SP live in the main entry strip /
|
||||
bandeau. F2 keeps CQ/ITU zones, the long-path bearing and distances. */}
|
||||
<Field label="Azimuth LP">
|
||||
<Input
|
||||
readOnly
|
||||
|
||||
@@ -86,6 +86,8 @@ interface Props {
|
||||
onDelete: (id: number) => void;
|
||||
onClose: () => void;
|
||||
countries?: string[];
|
||||
bands?: string[];
|
||||
modes?: string[];
|
||||
}
|
||||
|
||||
function toLocalISO(d: any): string {
|
||||
@@ -131,7 +133,20 @@ function QslSelect({ value, onChange }: { value?: string; onChange: (v: string)
|
||||
);
|
||||
}
|
||||
|
||||
export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }: Props) {
|
||||
export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], bands, modes }: Props) {
|
||||
// Use the operator's configured band/mode lists (incl. custom ones like 13cm);
|
||||
// fall back to the built-in sets. Always include the QSO's own band/mode so an
|
||||
// imported/legacy value is never silently dropped from the dropdown.
|
||||
const bandList = useMemo(() => {
|
||||
const base = (bands && bands.length ? bands : BANDS).slice();
|
||||
if (qso.band && !base.includes(qso.band)) base.unshift(qso.band);
|
||||
return base;
|
||||
}, [bands, qso.band]);
|
||||
const modeList = useMemo(() => {
|
||||
const base = (modes && modes.length ? modes : MODES).slice();
|
||||
if (qso.mode && !base.includes(qso.mode)) base.unshift(qso.mode);
|
||||
return base;
|
||||
}, [modes, qso.mode]);
|
||||
const [draft, setDraft] = useState<QSO>(() => JSON.parse(JSON.stringify(qso)));
|
||||
// Frequencies are edited as kHz + Hz (Log4OM style) and recombined on save.
|
||||
const splitHz = (hz?: number) => hz
|
||||
@@ -366,21 +381,21 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
|
||||
<Label className="w-20 shrink-0">Band</Label>
|
||||
<Select value={draft.band || ''} onValueChange={(v) => set('band', v)}>
|
||||
<SelectTrigger className="h-8 flex-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{BANDS.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
|
||||
<SelectContent>{bandList.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0">RX Band</Label>
|
||||
<Select value={draft.band_rx || '_'} onValueChange={(v) => set('band_rx', v === '_' ? '' : v)}>
|
||||
<SelectTrigger className="h-8 flex-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="_">—</SelectItem>{BANDS.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
|
||||
<SelectContent><SelectItem value="_">—</SelectItem>{bandList.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0">Mode</Label>
|
||||
<Select value={draft.mode || ''} onValueChange={(v) => set('mode', v)}>
|
||||
<SelectTrigger className="h-8 flex-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{MODES.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
|
||||
<SelectContent>{modeList.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -426,6 +426,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
const [autofocusWB, setAutofocusWB] = useState(() => localStorage.getItem('opslog.autofocusWB') !== '0');
|
||||
const [showBeamMap, setShowBeamMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
|
||||
const [startEqEnd, setStartEqEnd] = useState(() => localStorage.getItem('opslog.startEqualsEnd') === '1');
|
||||
const [catModeBeforeFreq, setCatModeBeforeFreq] = useState(() => localStorage.getItem('opslog.catModeBeforeFreq') === '1');
|
||||
|
||||
// E-mail / SMTP (send QSO recordings).
|
||||
type EmailCfg = {
|
||||
@@ -2939,6 +2940,20 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={catModeBeforeFreq}
|
||||
onCheckedChange={(c) => { const v = !!c; setCatModeBeforeFreq(v); writeUiPref('opslog.catModeBeforeFreq', v ? '1' : '0'); }}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span>
|
||||
Set CAT mode before frequency (older rigs)
|
||||
<span className="block text-xs text-muted-foreground mt-0.5">
|
||||
When clicking a spot, send the mode to the rig first, then the frequency. Some older transceivers drop the mode command if it arrives right after a band change, needing a second click. Both commands are also spaced out slightly to let the rig settle.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="border-t border-border/60 pt-4 space-y-2">
|
||||
<h4 className="text-sm font-semibold text-foreground">ClubLog exceptions (DXpedition overrides)</h4>
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
|
||||
Reference in New Issue
Block a user