diff --git a/app.go b/app.go index 3128b6e..65fcc19 100644 --- a/app.go +++ b/app.go @@ -1220,6 +1220,15 @@ func (a *App) startup(ctx context.Context) { } }) a.qsoRec = audio.NewRecorder() + if a.audioMgr != nil { + // A running monitor picks the new level up immediately: the operator is + // listening while they move the slider, and asking them to stop and + // restart it to hear the change is how a working control gets reported + // as broken. + if cfg, err := a.GetAudioSettings(); err == nil { + a.audioMgr.SetMonitorGain(cfg.FromGain) + } + } a.startQSORecorderIfEnabled() // NET Control store (global JSON, shared across logbooks). @@ -7051,6 +7060,12 @@ func (a *App) SaveAudioSettings(s AudioSettings) error { } // Apply device/preroll/enable changes to the running recorder. a.startQSORecorderIfEnabled() + // And to a monitor ALREADY RUNNING: the operator is listening while they + // move the slider. Making them stop and restart the monitor to hear the + // change is how a working control gets reported as doing nothing. + if a.audioMgr != nil { + a.audioMgr.SetMonitorGain(s.FromGain) + } return nil } @@ -8900,6 +8915,7 @@ func (a *App) AudioStartMonitor() error { return fmt.Errorf(`no "From radio" capture device set — pick the rig's USB Audio CODEC in Settings → Audio`) } applog.Printf("audio: RX monitor start (from=%q → listen=%q)", cfg.FromRadio, cfg.ListeningDevice) + a.audioMgr.SetMonitorGain(cfg.FromGain) return a.audioMgr.StartMonitor(cfg.FromRadio, cfg.ListeningDevice) } diff --git a/changelog.json b/changelog.json index a6be974..d0ef41e 100644 --- a/changelog.json +++ b/changelog.json @@ -6,13 +6,19 @@ "Cluster filters gain NEW POTA and NEW COUNTY, alongside the existing status chips.", "A playback that fails to start now says so in the log instead of ending silently after a tenth of a second.", "Replaying a recording before the previous one has finished now works: the new playback waits for the audio device to be free instead of keying the radio and releasing at once.", - "The play button becomes a stop button while the recording is going out: it cuts the transmission and releases the PTT, and you can send it again straight away." + "The play button becomes a stop button while the recording is going out: it cuts the transmission and releases the PTT, and you can send it again straight away.", + "The \"From radio\" level now applies to what you HEAR through OpsLog, not only to recordings, and takes effect while the monitor is running.", + "The padlocks on frequency, band, mode and times no longer flicker under the pointer and refuse the click.", + "The callsign field is wider, taking the room from the RST pair." ], "fr": [ "Les filtres du cluster gagnent NEW POTA et NEW COUNTY, à côté des pastilles de statut existantes.", "Une lecture qui ne démarre pas le signale désormais dans le journal, au lieu de s'arrêter en silence au bout d'un dixième de seconde.", "Relancer un enregistrement avant la fin du précédent fonctionne désormais : la nouvelle lecture attend que le périphérique audio soit libre, au lieu de passer en émission et de relâcher aussitôt.", - "Le bouton de lecture devient un bouton d'arrêt pendant l'émission de l'enregistrement : il coupe l'émission et relâche le PTT, et vous pouvez le renvoyer aussitôt." + "Le bouton de lecture devient un bouton d'arrêt pendant l'émission de l'enregistrement : il coupe l'émission et relâche le PTT, et vous pouvez le renvoyer aussitôt.", + "Le niveau « From radio » agit désormais sur ce que vous ENTENDEZ dans OpsLog, et plus seulement sur les enregistrements ; il prend effet moniteur en marche.", + "Les cadenas de fréquence, bande, mode et heures ne scintillent plus sous le curseur et acceptent le clic.", + "Le champ indicatif est plus large, la place venant de la paire RST." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3e7057a..eb213b6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -393,6 +393,33 @@ function WindowControls() { ); } +// LockPad — the padlock toggle shown in a lockable field's label. Its icon +// matches the state, so a glance says which fields are immune to CAT updates +// and to the live clock. +// +// It lives HERE, at module level, and not inside App. Defined inside, React saw +// a new component type on every render of App — and App re-renders about four +// times a second while the CAT polls — so the button was unmounted and rebuilt +// each time. It flickered under the pointer and swallowed clicks, because the +// node being clicked no longer existed by the time the click landed. +function LockPad({ on, title, onToggle }: { on: boolean; title: string; onToggle: () => void }) { + const Icon = on ? Lock : Unlock; + return ( + + ); +} + export default function App() { const { t, lang } = useI18n(); // === Lists from settings (fallback for first paint) === @@ -455,27 +482,6 @@ export default function App() { return next; }); }; - // Small padlock toggle rendered inside each lockable field's label. Match - // the icon to the current state so the user can tell at a glance which - // fields are immune to CAT updates / live clock. - function LockBtn({ k, title }: { k: LockKey; title: string }) { - const on = locks[k]; - const Icon = on ? Lock : Unlock; - return ( - - ); - } const [band, setBand] = useState('20m'); const [mode, setMode] = useState('SSB'); const [freqMhz, setFreqMhz] = useState(''); @@ -3710,8 +3716,13 @@ export default function App() { // single-row strip or the full Log4OM-style columnar layout below. Keeping // them as shared consts avoids duplicating the (large) per-field JSX + // handlers across the two layouts. + // The callsign field is WIDER than the fields around it, deliberately: it is + // the one field always typed into, it carries the REC badges on its right, and + // a long portable call (VK9/OH1ABC/MM) must stay readable as it is entered. + // The room comes from the RST pair, which never needs more than five + // characters. const callsignBlock = ( -
+
); const rstTxBlock = ( -
+
{ setRstSent(v); rstUserEditedRef.current = true; }} />
); const rstRxBlock = ( -
+
{ setRstRcvd(v); rstUserEditedRef.current = true; }} />
); @@ -3897,7 +3908,7 @@ export default function App() { ) : null; const startBlock = (
- + - + - +
@@ -4173,7 +4184,7 @@ export default function App() { }; const freqBlock = (
- + >8) + } + return out +} + // StopMonitor stops the RX monitor passthrough. func (m *Manager) StopMonitor() { m.mu.Lock()