feat(decodes): offer the panel as a Main-view pane
It joins the left/right dropdowns in Settings -> General alongside the maps, the cluster and the rest, so decodes can sit beside the entry strip instead of only behind a tab. Per-profile like the other pane choices. The panel itself is now built in ONE place and rendered from both: two copies of that call would be two sets of props to keep in step, and the click handler in particular is not something to duplicate. The status refresh follows. Its "is anything showing a spot status?" test decides whether a logged QSO refreshes the NEW badges now or only marks them dirty, and a panel that had become a pane would have gone on wearing stale badges whenever it was shown that way rather than as a tab.
This commit is contained in:
+40
-25
@@ -1656,12 +1656,12 @@ export default function App() {
|
|||||||
// map ("map1"), the locator street map ("map2"), the cluster grid or the
|
// map ("map1"), the locator street map ("map2"), the cluster grid or the
|
||||||
// worked-before grid. Per-profile (stored via SetUIPref → profile-prefixed),
|
// worked-before grid. Per-profile (stored via SetUIPref → profile-prefixed),
|
||||||
// so it's loaded async on mount and re-read on profile:changed below.
|
// so it's loaded async on mount and re-read on profile:changed below.
|
||||||
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent' | 'icom' | 'yaesu' | 'netcontrol';
|
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent' | 'icom' | 'yaesu' | 'netcontrol' | 'decodes';
|
||||||
const [mapZoomSignal, setMapZoomSignal] = useState(0); // bump → world map auto-zooms now
|
const [mapZoomSignal, setMapZoomSignal] = useState(0); // bump → world map auto-zooms now
|
||||||
const [mainPaneLeft, setMainPaneLeft] = useState<MainPaneKind>('map1');
|
const [mainPaneLeft, setMainPaneLeft] = useState<MainPaneKind>('map1');
|
||||||
const [mainPaneRight, setMainPaneRight] = useState<MainPaneKind>('map2');
|
const [mainPaneRight, setMainPaneRight] = useState<MainPaneKind>('map2');
|
||||||
const loadMainPanes = useCallback(async () => {
|
const loadMainPanes = useCallback(async () => {
|
||||||
const valid = (v: string): v is MainPaneKind => v === 'map1' || v === 'map2' || v === 'cluster' || v === 'worked' || v === 'flex' || v === 'recent' || v === 'icom' || v === 'yaesu' || v === 'netcontrol';
|
const valid = (v: string): v is MainPaneKind => v === 'map1' || v === 'map2' || v === 'cluster' || v === 'worked' || v === 'flex' || v === 'recent' || v === 'icom' || v === 'yaesu' || v === 'netcontrol' || v === 'decodes';
|
||||||
const [l, r] = await Promise.all([
|
const [l, r] = await Promise.all([
|
||||||
GetUIPref('mainPaneLeft').catch(() => ''),
|
GetUIPref('mainPaneLeft').catch(() => ''),
|
||||||
GetUIPref('mainPaneRight').catch(() => ''),
|
GetUIPref('mainPaneRight').catch(() => ''),
|
||||||
@@ -1786,6 +1786,7 @@ export default function App() {
|
|||||||
const spotsDirtyRef = useRef(false);
|
const spotsDirtyRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vis = mainPaneLeft === 'cluster' || mainPaneRight === 'cluster'
|
const vis = mainPaneLeft === 'cluster' || mainPaneRight === 'cluster'
|
||||||
|
|| mainPaneLeft === 'decodes' || mainPaneRight === 'decodes'
|
||||||
|| activeTab === 'cluster' || activeTab === 'bandmap' || activeTab === 'decodes' || showBandMap;
|
|| activeTab === 'cluster' || activeTab === 'bandmap' || activeTab === 'decodes' || showBandMap;
|
||||||
if (vis && !spotsVisibleRef.current && spotsDirtyRef.current) {
|
if (vis && !spotsVisibleRef.current && spotsDirtyRef.current) {
|
||||||
spotsDirtyRef.current = false;
|
spotsDirtyRef.current = false;
|
||||||
@@ -5446,6 +5447,34 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The FT decodes panel, built in ONE place: it is offered both as a tab and as
|
||||||
|
// a Main-view pane, and two copies of this call would be two sets of props to
|
||||||
|
// keep in step.
|
||||||
|
const renderDecodesPanel = () => (
|
||||||
|
<DecodesPanel
|
||||||
|
decodes={decodes}
|
||||||
|
txMsgs={txMsgs}
|
||||||
|
txState={txState}
|
||||||
|
spotStatus={spotStatus as any}
|
||||||
|
myCall={station.callsign}
|
||||||
|
// A click ANSWERS the station: it hands the decode back to WSJT-X/MSHV as
|
||||||
|
// a Reply, which is the same thing as double-clicking the line in their
|
||||||
|
// own window.
|
||||||
|
//
|
||||||
|
// Deliberately NOT a rig tune, unlike a cluster spot. On FT8 the whole
|
||||||
|
// band is inside one passband, so moving the dial changes nothing about
|
||||||
|
// who gets answered — and it would only fight the digital application
|
||||||
|
// for the VFO. The entry is still filled, so the QSO can be logged here.
|
||||||
|
onCall={(d) => {
|
||||||
|
onCallsignInput(d.call, { force: true });
|
||||||
|
AnswerDecode(
|
||||||
|
d.instance ?? '', d.ms ?? 0, d.snr, d.dt ?? 0,
|
||||||
|
d.audio_hz ?? 0, d.mode ?? '', d.msg ?? '', !!d.low_conf,
|
||||||
|
).catch((e: any) => setError(String(e?.message ?? e)));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Render one Main-view pane. The two sides (mainPaneLeft/Right) each pick from
|
// Render one Main-view pane. The two sides (mainPaneLeft/Right) each pick from
|
||||||
// the same four choices, configured per-profile in Settings → Main view.
|
// the same four choices, configured per-profile in Settings → Main view.
|
||||||
const renderMainPane = (kind: MainPaneKind) => {
|
const renderMainPane = (kind: MainPaneKind) => {
|
||||||
@@ -5464,6 +5493,14 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
case 'map2':
|
case 'map2':
|
||||||
return <LocatorMap toGrid={grid} toLabel={callsign} />;
|
return <LocatorMap toGrid={grid} toLabel={callsign} />;
|
||||||
|
case 'decodes':
|
||||||
|
// Same panel as the tab, in a pane. It brings its own filter bar and
|
||||||
|
// column header, so it needs no frame of its own here.
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
|
||||||
|
{renderDecodesPanel()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case 'cluster':
|
case 'cluster':
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
|
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
|
||||||
@@ -7205,29 +7242,7 @@ export default function App() {
|
|||||||
|
|
||||||
{decodesTabOpen && (
|
{decodesTabOpen && (
|
||||||
<TabsContent value="decodes" className="mt-0 flex flex-col min-h-0 flex-1 data-[state=inactive]:hidden">
|
<TabsContent value="decodes" className="mt-0 flex flex-col min-h-0 flex-1 data-[state=inactive]:hidden">
|
||||||
<DecodesPanel
|
{renderDecodesPanel()}
|
||||||
decodes={decodes}
|
|
||||||
txMsgs={txMsgs}
|
|
||||||
txState={txState}
|
|
||||||
spotStatus={spotStatus as any}
|
|
||||||
myCall={station.callsign}
|
|
||||||
// A click ANSWERS the station: it hands the decode back to
|
|
||||||
// WSJT-X/MSHV as a Reply, which is the same thing as
|
|
||||||
// double-clicking the line in their own window.
|
|
||||||
//
|
|
||||||
// Deliberately NOT a rig tune, unlike a cluster spot. On FT8
|
|
||||||
// the whole band is inside one passband, so moving the dial
|
|
||||||
// changes nothing about who gets answered — and it would only
|
|
||||||
// fight the digital application for the VFO. The entry is
|
|
||||||
// still filled, so the QSO can be logged here.
|
|
||||||
onCall={(d) => {
|
|
||||||
onCallsignInput(d.call, { force: true });
|
|
||||||
AnswerDecode(
|
|
||||||
d.instance ?? '', d.ms ?? 0, d.snr, d.dt ?? 0,
|
|
||||||
d.audio_hz ?? 0, d.mode ?? '', d.msg ?? '', !!d.low_conf,
|
|
||||||
).catch((e: any) => setError(String(e?.message ?? e)));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@ function RelayAutoPanel() {
|
|||||||
// panes show, independently: the great-circle map, the locator street map, the
|
// panes show, independently: the great-circle map, the locator street map, the
|
||||||
// cluster grid or the worked-before grid. Per-profile (stored via SetUIPref,
|
// cluster grid or the worked-before grid. Per-profile (stored via SetUIPref,
|
||||||
// which is profile-prefixed). Self-contained so it owns its async-loaded state.
|
// which is profile-prefixed). Self-contained so it owns its async-loaded state.
|
||||||
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol'];
|
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol', 'decodes'];
|
||||||
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable }: { onChanged?: (side: 'left' | 'right', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean }) {
|
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable }: { onChanged?: (side: 'left' | 'right', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [left, setLeft] = useState('map1');
|
const [left, setLeft] = useState('map1');
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const en: Dict = {
|
|||||||
'settings.leftPane': 'Left pane', 'settings.rightPane': 'Right pane',
|
'settings.leftPane': 'Left pane', 'settings.rightPane': 'Right pane',
|
||||||
'settings.pane.map1': 'Map — great-circle + beam', 'settings.pane.map2': 'Map — locator (street)',
|
'settings.pane.map1': 'Map — great-circle + beam', 'settings.pane.map2': 'Map — locator (street)',
|
||||||
'settings.pane.cluster': 'Cluster spots', 'settings.pane.worked': 'Worked before',
|
'settings.pane.cluster': 'Cluster spots', 'settings.pane.worked': 'Worked before',
|
||||||
'settings.pane.recent': 'Recent QSOs', 'settings.pane.netcontrol': 'Net control',
|
'settings.pane.recent': 'Recent QSOs', 'settings.pane.netcontrol': 'Net control', 'settings.pane.decodes': 'FT decodes',
|
||||||
'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'Yaesu CAT not connected', 'yaesu.meters': 'Meters', 'yaesu.bandMode': 'Band & mode', 'yaesu.receive': 'Receive', 'yaesu.noiseFilter': 'Noise & filter', 'yaesu.transmit': 'Transmit', 'yaesu.refresh': 'Refresh', 'yaesu.tuneHint': 'Start an antenna-tuner cycle', 'yaesu.sToRst': 'Click to fill the RST sent', 'yaesu.sidebandHint': 'Click to select this mode; click again to switch sideband (U/L)', 'yaesu.splitUpHint': 'Transmit this far above the receive frequency, and turn split on', 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': 'Break-in: the rig switches to receive between characters', 'yaesu.zinHint': 'Zero-in: retune so the station you hear lands on your CW pitch',
|
'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'Yaesu CAT not connected', 'yaesu.meters': 'Meters', 'yaesu.bandMode': 'Band & mode', 'yaesu.receive': 'Receive', 'yaesu.noiseFilter': 'Noise & filter', 'yaesu.transmit': 'Transmit', 'yaesu.refresh': 'Refresh', 'yaesu.tuneHint': 'Start an antenna-tuner cycle', 'yaesu.sToRst': 'Click to fill the RST sent', 'yaesu.sidebandHint': 'Click to select this mode; click again to switch sideband (U/L)', 'yaesu.splitUpHint': 'Transmit this far above the receive frequency, and turn split on', 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': 'Break-in: the rig switches to receive between characters', 'yaesu.zinHint': 'Zero-in: retune so the station you hear lands on your CW pitch',
|
||||||
'theme.auto': 'Auto (system)', 'theme.light-warm': 'Warm light', 'theme.light-cool': 'Cool light',
|
'theme.auto': 'Auto (system)', 'theme.light-warm': 'Warm light', 'theme.light-cool': 'Cool light',
|
||||||
'theme.light-sage': 'Sage light', 'theme.light-nordic': 'Nordic light', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Dim slate', 'theme.dark-warm': 'Warm dark',
|
'theme.light-sage': 'Sage light', 'theme.light-nordic': 'Nordic light', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Dim slate', 'theme.dark-warm': 'Warm dark',
|
||||||
@@ -579,7 +579,7 @@ const fr: Dict = {
|
|||||||
'settings.leftPane': 'Volet gauche', 'settings.rightPane': 'Volet droit',
|
'settings.leftPane': 'Volet gauche', 'settings.rightPane': 'Volet droit',
|
||||||
'settings.pane.map1': 'Carte — orthodromie + faisceau', 'settings.pane.map2': 'Carte — locator (rue)',
|
'settings.pane.map1': 'Carte — orthodromie + faisceau', 'settings.pane.map2': 'Carte — locator (rue)',
|
||||||
'settings.pane.cluster': 'Spots cluster', 'settings.pane.worked': 'Déjà contactés',
|
'settings.pane.cluster': 'Spots cluster', 'settings.pane.worked': 'Déjà contactés',
|
||||||
'settings.pane.recent': 'QSO récents', 'settings.pane.netcontrol': 'Gestion de net',
|
'settings.pane.recent': 'QSO récents', 'settings.pane.netcontrol': 'Gestion de net', 'settings.pane.decodes': 'Decodes FT',
|
||||||
'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'CAT Yaesu non connecté', 'yaesu.meters': 'Mesures', 'yaesu.bandMode': 'Bande et mode', 'yaesu.receive': 'Réception', 'yaesu.noiseFilter': 'Bruit et filtre', 'yaesu.transmit': 'Émission', 'yaesu.refresh': 'Actualiser', 'yaesu.tuneHint': "Lancer un cycle d'accord d'antenne", 'yaesu.sToRst': 'Cliquer pour remplir le RST envoyé', 'yaesu.sidebandHint': 'Cliquer pour choisir ce mode ; recliquer pour changer de bande latérale (U/L)', 'yaesu.splitUpHint': "Émettre à cette distance au-dessus de la fréquence de réception, et activer le split", 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': "Break-in : la radio repasse en réception entre les caractères", 'yaesu.zinHint': "Zéro-in : réaccorde pour que la station entendue tombe sur votre note CW",
|
'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'CAT Yaesu non connecté', 'yaesu.meters': 'Mesures', 'yaesu.bandMode': 'Bande et mode', 'yaesu.receive': 'Réception', 'yaesu.noiseFilter': 'Bruit et filtre', 'yaesu.transmit': 'Émission', 'yaesu.refresh': 'Actualiser', 'yaesu.tuneHint': "Lancer un cycle d'accord d'antenne", 'yaesu.sToRst': 'Cliquer pour remplir le RST envoyé', 'yaesu.sidebandHint': 'Cliquer pour choisir ce mode ; recliquer pour changer de bande latérale (U/L)', 'yaesu.splitUpHint': "Émettre à cette distance au-dessus de la fréquence de réception, et activer le split", 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': "Break-in : la radio repasse en réception entre les caractères", 'yaesu.zinHint': "Zéro-in : réaccorde pour que la station entendue tombe sur votre note CW",
|
||||||
'theme.auto': 'Auto (système)', 'theme.light-warm': 'Clair chaud', 'theme.light-cool': 'Clair froid',
|
'theme.auto': 'Auto (système)', 'theme.light-warm': 'Clair chaud', 'theme.light-cool': 'Clair froid',
|
||||||
'theme.light-sage': 'Clair sauge', 'theme.light-nordic': 'Clair nordique', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Ardoise tamisé', 'theme.dark-warm': 'Sombre chaud',
|
'theme.light-sage': 'Clair sauge', 'theme.light-nordic': 'Clair nordique', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Ardoise tamisé', 'theme.dark-warm': 'Sombre chaud',
|
||||||
|
|||||||
Reference in New Issue
Block a user