feat: New fit to band and hide FTx option in bandmap tab
This commit is contained in:
+21
-1
@@ -839,6 +839,12 @@ export default function App() {
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
// Global band-map-tab options (apply to every map in the tab): hide FTx/digital
|
||||
// spots, and fit each map to its full band span.
|
||||
const [bandMapHideFt, setBandMapHideFt] = useState(() => localStorage.getItem('opslog.bandMapHideFt') === '1');
|
||||
const [bandMapFit, setBandMapFit] = useState(() => localStorage.getItem('opslog.bandMapFit') === '1');
|
||||
const toggleBandMapHideFt = useCallback(() => setBandMapHideFt((v) => { const n = !v; writeUiPref('opslog.bandMapHideFt', n ? '1' : '0'); return n; }), []);
|
||||
const toggleBandMapFit = useCallback(() => setBandMapFit((v) => { const n = !v; writeUiPref('opslog.bandMapFit', n ? '1' : '0'); return n; }), []);
|
||||
// Single band map docked beside the table (toggled by the toolbar button,
|
||||
// visible across tabs). Independent of the multi-band "Band Map" tab.
|
||||
const [showBandMap, setShowBandMap] = useState(() => localStorage.getItem('bandmap.show') === '1');
|
||||
@@ -4122,7 +4128,7 @@ export default function App() {
|
||||
|
||||
<TabsContent value="bandmap" className="mt-0 flex flex-col min-h-0 flex-1">
|
||||
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-border/60 shrink-0 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground mr-1">Bands:</span>
|
||||
<span className="text-xs text-muted-foreground mr-1">{t('bmp.bandsLabel')}</span>
|
||||
{bands.map((b) => {
|
||||
const on = bandMapBands.includes(b);
|
||||
return (
|
||||
@@ -4133,6 +4139,18 @@ export default function App() {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
<button type="button" onClick={toggleBandMapHideFt} title={t('bmp.hideFtTitle')}
|
||||
className={cn('px-2 py-0.5 rounded-full border text-[11px] font-medium transition-colors',
|
||||
bandMapHideFt ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
|
||||
{t('bmp.hideFt')}
|
||||
</button>
|
||||
<button type="button" onClick={toggleBandMapFit} title={t('bmp.fitTitle')}
|
||||
className={cn('px-2 py-0.5 rounded-full border text-[11px] font-medium transition-colors',
|
||||
bandMapFit ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
|
||||
{t('bmp.fitBand')}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 flex gap-2 p-2 overflow-x-auto">
|
||||
{bandMapBands.length === 0 ? (
|
||||
@@ -4148,6 +4166,8 @@ export default function App() {
|
||||
currentFreqHz={band === b && freqMhz ? Math.round(parseFloat(freqMhz) * 1_000_000) : 0}
|
||||
onSpotClick={handleSpotClick}
|
||||
onClose={() => toggleBandMapBand(b)}
|
||||
hideDigital={bandMapHideFt}
|
||||
fitToBand={bandMapFit}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -35,6 +35,12 @@ interface Props {
|
||||
onClose?: () => void;
|
||||
side?: 'left' | 'right';
|
||||
onToggleSide?: () => void;
|
||||
// hideDigital drops every DATA-class (FT8/FT4/JS8/…) spot so the crowded
|
||||
// watering holes don't hog the map. fitToBand overrides zoom so the whole
|
||||
// band edge-to-edge fits the visible height (no scrolling). Both are driven
|
||||
// globally from the band-map tab toolbar.
|
||||
hideDigital?: boolean;
|
||||
fitToBand?: boolean;
|
||||
}
|
||||
|
||||
const BAND_RANGES: Record<string, [number, number]> = {
|
||||
@@ -147,7 +153,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 }: Props) {
|
||||
export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false }: Props) {
|
||||
const { t } = useI18n();
|
||||
const range = BAND_RANGES[band];
|
||||
const segments = SEGMENT_COLORS[band] ?? [];
|
||||
@@ -167,7 +173,12 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
||||
const fallback: [number, number] = range ?? [0, 1];
|
||||
const [lo, hi] = fallback;
|
||||
const span = hi - lo;
|
||||
const pxPerKHz = PX_PER_KHZ[zoomIdx];
|
||||
// Fit-to-band computes the exact px/kHz so the whole band edge-to-edge fills
|
||||
// the visible height; otherwise use the discrete zoom step.
|
||||
const fitPxPerKHz = fitToBand && containerH > 0 && span > 0
|
||||
? Math.max(0.05, (containerH - TOP_PAD - BOT_PAD) / span)
|
||||
: 0;
|
||||
const pxPerKHz = fitPxPerKHz || PX_PER_KHZ[zoomIdx];
|
||||
|
||||
// Anti-overlap layout: each label wants to sit at its true freq, but
|
||||
// never closer than PILL_H from the previous one. Sorted top-to-bottom
|
||||
@@ -199,7 +210,8 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
||||
// spots carry no mode word and the band-plan fallback labels them the
|
||||
// generic "DATA" rather than "FT8". CW and SSB are always shown in full.
|
||||
const isFlood = (s: Spot) => spotModeCategory(inferSpotMode(s.comment ?? '', s.freq_hz)) === 'DATA';
|
||||
const ftSpots = inBand.filter(isFlood);
|
||||
// hideDigital removes them entirely; otherwise they're capped below.
|
||||
const ftSpots = hideDigital ? [] : inBand.filter(isFlood);
|
||||
const otherSpots = inBand.filter((s) => !isFlood(s));
|
||||
|
||||
// Rank a DATA spot by usefulness (new entity → unworked → worked); ties
|
||||
@@ -277,7 +289,7 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
||||
totalH: Math.max(innerH + TOP_PAD + BOT_PAD, lastLabelBottom + BOT_PAD),
|
||||
hidden: hiddenCount,
|
||||
};
|
||||
}, [spots, range, lo, hi, span, pxPerKHz, containerH, spotStatus, currentFreqHz]);
|
||||
}, [spots, range, lo, hi, span, pxPerKHz, containerH, spotStatus, currentFreqHz, hideDigital]);
|
||||
|
||||
// freqToY for elements rendered outside the memo (ticks, rig pointer).
|
||||
// Must mirror the same offset so the rig triangle sits on the right kHz.
|
||||
@@ -350,13 +362,13 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
||||
<div className="h-full w-full flex flex-col min-h-0 bg-card">
|
||||
<div className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground bg-muted/40 border-b border-border flex flex-nowrap items-center gap-0.5 shrink-0">
|
||||
<span className="flex-1 min-w-0 truncate">{t('bmp.map')} · {band}</span>
|
||||
<button type="button" onClick={() => setZoomIdx((z) => Math.max(0, z - 1))} disabled={zoomIdx === 0}
|
||||
<button type="button" onClick={() => setZoomIdx((z) => Math.max(0, z - 1))} disabled={fitToBand || zoomIdx === 0}
|
||||
className="size-5 shrink-0 inline-flex items-center justify-center rounded hover:bg-muted disabled:opacity-30"
|
||||
title={t('bmp.zoomOut')}>
|
||||
<Minus className="size-3" />
|
||||
</button>
|
||||
<span className="shrink-0 font-mono text-[10px] normal-case tracking-normal whitespace-nowrap px-0.5">{pxPerKHz}px/kHz</span>
|
||||
<button type="button" onClick={() => setZoomIdx((z) => Math.min(PX_PER_KHZ.length - 1, z + 1))} disabled={zoomIdx === PX_PER_KHZ.length - 1}
|
||||
<span className="shrink-0 font-mono text-[10px] normal-case tracking-normal whitespace-nowrap px-0.5">{fitToBand ? t('bmp.fit') : `${pxPerKHz}px/kHz`}</span>
|
||||
<button type="button" onClick={() => setZoomIdx((z) => Math.min(PX_PER_KHZ.length - 1, z + 1))} disabled={fitToBand || zoomIdx === PX_PER_KHZ.length - 1}
|
||||
className="size-5 shrink-0 inline-flex items-center justify-center rounded hover:bg-muted disabled:opacity-30"
|
||||
title={t('bmp.zoomIn')}>
|
||||
<Plus className="size-3" />
|
||||
|
||||
@@ -168,6 +168,7 @@ const en: Dict = {
|
||||
'bmp.statusNew': 'NEW DXCC (entity never worked)', 'bmp.statusNewBand': 'NEW BAND (entity not worked on this band)', 'bmp.statusNewSlot': 'NEW SLOT (mode not worked on this band)',
|
||||
'bmp.statusWorked': 'Worked (this band + mode already in log)', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
|
||||
'bmp.map': 'Map', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
|
||||
'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge',
|
||||
'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.legendWorked': 'Worked', 'bmp.footerHint': 'scroll · ctrl+wheel = zoom · ◎ = jump to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)',
|
||||
'frm.welcome': 'Welcome to OpsLog', 'frm.intro': 'Set up your station to start logging. These fields stamp every QSO and can be changed later in Preferences → Station Information (and per profile).',
|
||||
'frm.callsign': 'Callsign', 'frm.locator': 'Locator', 'frm.operator': 'Operator', 'frm.operatorPh': 'same as callsign', 'frm.owner': 'Owner', 'frm.ownerPh': 'station owner callsign', 'frm.name': 'Name', 'frm.namePh': 'your first name',
|
||||
@@ -345,6 +346,7 @@ const fr: Dict = {
|
||||
'bmp.statusNew': 'NOUVEAU DXCC (entité jamais contactée)', 'bmp.statusNewBand': 'NOUVELLE BANDE (entité non contactée sur cette bande)', 'bmp.statusNewSlot': 'NOUVEAU MODE (mode non contacté sur cette bande)',
|
||||
'bmp.statusWorked': 'Contacté (cette bande + mode déjà au log)', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
|
||||
'bmp.map': 'Carte', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
|
||||
'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande',
|
||||
'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.legendWorked': 'Contacté', 'bmp.footerHint': 'défiler · ctrl+molette = zoom · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)',
|
||||
'frm.welcome': 'Bienvenue dans OpsLog', 'frm.intro': 'Configure ta station pour commencer à logger. Ces champs sont inscrits sur chaque QSO et peuvent être modifiés plus tard dans Préférences → Informations station (et par profil).',
|
||||
'frm.callsign': 'Indicatif', 'frm.locator': 'Locator', 'frm.operator': 'Opérateur', 'frm.operatorPh': "identique à l'indicatif", 'frm.owner': 'Propriétaire', 'frm.ownerPh': 'indicatif du propriétaire de la station', 'frm.name': 'Nom', 'frm.namePh': 'ton prénom',
|
||||
|
||||
Reference in New Issue
Block a user