diff --git a/changelog.json b/changelog.json index cd001e4..24a2359 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,14 @@ "en": [ "Fixed the colour theme sometimes reverting to the default when reopening OpsLog — it's now restored reliably.", "ADIF export field picker: the per-group All / None buttons work again.", - "Station Control: cards now pack tightly into balanced columns instead of leaving big gaps under shorter panels — a cleaner, more even dashboard." + "Station Control: cards now pack tightly into balanced columns instead of leaving big gaps under shorter panels — a cleaner, more even dashboard.", + "Main-view band map: Ctrl+↑ / Ctrl+↓ jumps to the next spot above / below the current frequency and tunes to it." ], "fr": [ "Correction du thème qui revenait parfois au défaut à la réouverture d'OpsLog — il est maintenant restauré de façon fiable.", "Sélecteur de champs à l'export ADIF : les boutons Tout / Aucun par groupe refonctionnent.", - "Station Control : les cartes se rangent en colonnes équilibrées et se tassent au lieu de laisser de gros trous sous les panneaux plus courts — tableau de bord plus propre et régulier." + "Station Control : les cartes se rangent en colonnes équilibrées et se tassent au lieu de laisser de gros trous sous les panneaux plus courts — tableau de bord plus propre et régulier.", + "Band map de l'écran principal : Ctrl+↑ / Ctrl+↓ saute au spot suivant au-dessus / en dessous de la fréquence courante et s'y accorde." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 298b864..314202d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5320,6 +5320,7 @@ export default function App() { currentFreqHz={band && freqMhz ? Math.round(parseFloat(freqMhz) * 1_000_000) : 0} onSpotClick={handleSpotClick} onClose={() => setBandMapShown(false)} + keyNav /> )} diff --git a/frontend/src/components/BandMap.tsx b/frontend/src/components/BandMap.tsx index 7e2d620..10c8171 100644 --- a/frontend/src/components/BandMap.tsx +++ b/frontend/src/components/BandMap.tsx @@ -41,6 +41,10 @@ interface Props { // globally from the band-map tab toolbar. hideDigital?: boolean; fitToBand?: boolean; + // keyNav enables Ctrl+↑ / Ctrl+↓ to hop to the next spot above / below the rig + // frequency (and tune to it). Only the docked Main-view band map sets this, so + // the multi-band Band Map tab (several maps) doesn't fight over the shortcut. + keyNav?: boolean; } const BAND_RANGES: Record = { @@ -153,7 +157,7 @@ const BOT_PAD = 14; // the top-most freq label isn't clipped at y=0 // last; ties broken by closeness to the rig freq). const MAX_VISIBLE_SPOTS = 30; -export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false }: Props) { +export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false, keyNav = false }: Props) { const { t } = useI18n(); const range = BAND_RANGES[band]; const segments = SEGMENT_COLORS[band] ?? []; @@ -362,6 +366,38 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o scrollerRef.current.scrollTop = Math.max(0, y - containerH / 2); } + // Ctrl+↑ / Ctrl+↓ hop to the next spot above / below the rig frequency and tune + // to it. Higher freq is UP on the map (see freqToY), so ↑ = next higher spot. + // Only active on the docked Main-view map (keyNav) and ignored while typing. + useEffect(() => { + if (!keyNav) return; + const onKey = (e: KeyboardEvent) => { + if (!e.ctrlKey || e.altKey || e.metaKey) return; + if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; + const ae = document.activeElement as HTMLElement | null; + const tag = (ae?.tagName || '').toLowerCase(); + if (tag === 'input' || tag === 'textarea' || tag === 'select' || ae?.isContentEditable) return; + const list = spots + .filter((s) => (s.band ?? '') === band && s.freq_hz > 0) + .slice() + .sort((a, b) => a.freq_hz - b.freq_hz); + if (!list.length) return; + const cur = currentFreqHz || (lo + hi) * 500; // mid-band kHz→Hz when no rig freq + const EPS = 50; // Hz, so we don't re-pick the spot we're already sitting on + let target: Spot | undefined; + if (e.key === 'ArrowUp') { + target = list.find((s) => s.freq_hz > cur + EPS); + } else { + for (let i = list.length - 1; i >= 0; i--) { if (list[i].freq_hz < cur - EPS) { target = list[i]; break; } } + } + if (!target) return; + e.preventDefault(); + onSpotClick(target); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [keyNav, spots, band, currentFreqHz, lo, hi, onSpotClick]); + const currentKHz = currentFreqHz ? currentFreqHz / 1000 : 0; const showRigPointer = currentKHz >= lo && currentKHz <= hi; const rigY = freqToY(currentKHz);