diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6a2f0c2..658980e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1656,12 +1656,12 @@ export default function App() { // map ("map1"), the locator street map ("map2"), the cluster grid or the // worked-before grid. Per-profile (stored via SetUIPref → profile-prefixed), // 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 [mainPaneLeft, setMainPaneLeft] = useState('map1'); const [mainPaneRight, setMainPaneRight] = useState('map2'); 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([ GetUIPref('mainPaneLeft').catch(() => ''), GetUIPref('mainPaneRight').catch(() => ''), @@ -1786,6 +1786,7 @@ export default function App() { const spotsDirtyRef = useRef(false); useEffect(() => { const vis = mainPaneLeft === 'cluster' || mainPaneRight === 'cluster' + || mainPaneLeft === 'decodes' || mainPaneRight === 'decodes' || activeTab === 'cluster' || activeTab === 'bandmap' || activeTab === 'decodes' || showBandMap; if (vis && !spotsVisibleRef.current && spotsDirtyRef.current) { spotsDirtyRef.current = false; @@ -5446,6 +5447,34 @@ export default function App() { ); + // 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 = () => ( + { + 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 // the same four choices, configured per-profile in Settings → Main view. const renderMainPane = (kind: MainPaneKind) => { @@ -5464,6 +5493,14 @@ export default function App() { ); case 'map2': return ; + 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 ( +
+ {renderDecodesPanel()} +
+ ); case 'cluster': return (
@@ -7205,29 +7242,7 @@ export default function App() { {decodesTabOpen && ( - { - 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))); - }} - /> + {renderDecodesPanel()} )} diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 5944a06..c414b7c 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1029,7 +1029,7 @@ function RelayAutoPanel() { // panes show, independently: the great-circle map, the locator street map, the // 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. -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 }) { const { t } = useI18n(); const [left, setLeft] = useState('map1'); diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 206b890..8211766 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -105,7 +105,7 @@ const en: Dict = { 'settings.leftPane': 'Left pane', 'settings.rightPane': 'Right pane', '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.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', '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', @@ -579,7 +579,7 @@ const fr: Dict = { 'settings.leftPane': 'Volet gauche', 'settings.rightPane': 'Volet droit', '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.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", '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',