feat(entry): one padlock instead of five
Logging a contact from a piece of paper — a contest sheet, a friend's report, a QSO worked on another radio — means the frequency, the band, the mode, the date and both times all have to stop following the rig and the clock at once. That is a single decision, and it was five clicks in five different places, each of which had to be found first. The padlock beside Start UTC now holds all five and releases all five, with the same behaviour as before: locking pre-fills the times so the fields are not empty, unlocking hands them back to the clock and snaps a QSO already in progress to now. The five per-field locks stay underneath. Everything downstream reads them, and individually they say the right thing — "this value is decoupled from the rig". Only the control is one.
This commit is contained in:
+36
-31
@@ -566,7 +566,9 @@ function LockPad({ on, title, onToggle }: { on: boolean; title: string; onToggle
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={onToggle}
|
||||
title={`${on ? 'Unlock' : 'Lock'} ${title}`}
|
||||
// The whole tooltip, not a verb glued to a noun: the caller knows what
|
||||
// this padlock does and can say it in the operator's own language.
|
||||
title={title}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-3.5 rounded transition-colors',
|
||||
on ? 'text-warning hover:text-warning' : 'text-muted-foreground/40 hover:text-muted-foreground',
|
||||
@@ -618,31 +620,34 @@ export default function App() {
|
||||
});
|
||||
const locksRef = useRef(locks);
|
||||
useEffect(() => { locksRef.current = locks; }, [locks]);
|
||||
const toggleLock = (k: LockKey) => {
|
||||
setLocks((s) => {
|
||||
const wasLocked = s[k];
|
||||
const next = { ...s, [k]: !wasLocked };
|
||||
if (wasLocked) {
|
||||
// Unlocking → restore automatic behavior. Without this the locked
|
||||
// value would linger forever: a stale Start time would never refresh
|
||||
// even after a new callsign is entered.
|
||||
if (k === 'start') {
|
||||
// If a QSO is currently in progress (callsign typed), snap start
|
||||
// to now since we missed the auto-start moment. Otherwise clear.
|
||||
setQsoStartedAt(callsign.trim() ? new Date() : null);
|
||||
} else if (k === 'end') {
|
||||
// Drop the frozen end so the field tracks the live UTC clock.
|
||||
setQsoEndedAt(null);
|
||||
}
|
||||
} else {
|
||||
// Locking (manual / deferred entry) → pre-fill with today's date + the
|
||||
// current UTC time so the fields aren't empty; the operator just adjusts.
|
||||
const now = new Date();
|
||||
if (k === 'start') setQsoStartedAt((d) => d ?? now);
|
||||
else if (k === 'end') setQsoEndedAt((d) => d ?? now);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
// ONE padlock, not five.
|
||||
//
|
||||
// Logging a contact from a piece of paper — a contest sheet, a friend's
|
||||
// report, a QSO worked on another radio — means the frequency, the band, the
|
||||
// mode, the date and both times all have to stop following the rig and the
|
||||
// clock at once. That is a single decision, and it used to be five clicks in
|
||||
// five different places, each of which had to be found first.
|
||||
//
|
||||
// The five per-field locks stay underneath, because everything downstream
|
||||
// reads them and they say the right thing individually ("this value is
|
||||
// decoupled from the rig"). Only the control is one.
|
||||
const manualEntry = locks.start && locks.end && locks.band && locks.mode && locks.freq;
|
||||
const setManualEntry = (on: boolean) => {
|
||||
setLocks({ band: on, mode: on, freq: on, start: on, end: on });
|
||||
if (on) {
|
||||
// Pre-filled with today's date and the current UTC time so the fields are
|
||||
// not empty; the operator only has to correct them.
|
||||
const now = new Date();
|
||||
setQsoStartedAt((d) => d ?? now);
|
||||
setQsoEndedAt((d) => d ?? now);
|
||||
} else {
|
||||
// Back to automatic. Without this the frozen values would linger for
|
||||
// ever: a start time held from a backdated entry would never refresh,
|
||||
// even after a new callsign is typed. A QSO already in progress snaps its
|
||||
// start to now, since the moment it would have been taken has passed.
|
||||
setQsoStartedAt(callsign.trim() ? new Date() : null);
|
||||
setQsoEndedAt(null);
|
||||
}
|
||||
};
|
||||
const [band, setBand] = useState('20m');
|
||||
const [mode, setMode] = useState('SSB');
|
||||
@@ -5615,7 +5620,7 @@ export default function App() {
|
||||
) : null;
|
||||
const startBlock = (
|
||||
<div className="flex flex-col w-28">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-success">{t('field.startUtc')} <LockPad on={locks.start} title="start time" onToggle={() => toggleLock('start')} /></Label>
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-success">{t('field.startUtc')} <LockPad on={manualEntry} title={manualEntry ? t('field.manualEntryOff') : t('field.manualEntryOn')} onToggle={() => setManualEntry(!manualEntry)} /></Label>
|
||||
<Input
|
||||
readOnly={!locks.start}
|
||||
tabIndex={locks.start ? 0 : -1}
|
||||
@@ -5634,7 +5639,7 @@ export default function App() {
|
||||
);
|
||||
const endBlock = (
|
||||
<div className="flex flex-col w-28">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-danger">{t('field.endUtc')} <LockPad on={locks.end} title="end time" onToggle={() => toggleLock('end')} /></Label>
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-danger">{t('field.endUtc')}</Label>
|
||||
<Input
|
||||
readOnly={!locks.end}
|
||||
tabIndex={locks.end ? 0 : -1}
|
||||
@@ -5905,7 +5910,7 @@ export default function App() {
|
||||
// used in the full layout to save vertical height.
|
||||
const bandRow = (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.band')} <LockPad on={locks.band} title="band" onToggle={() => toggleLock('band')} /></Label>
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.band')}</Label>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Select value={band} onValueChange={onBandUserChange}>
|
||||
<SelectTrigger tabIndex={-1} className="h-8"><SelectValue /></SelectTrigger>
|
||||
@@ -5916,7 +5921,7 @@ export default function App() {
|
||||
);
|
||||
const modeRow = (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.mode')} <LockPad on={locks.mode} title="mode" onToggle={() => toggleLock('mode')} /></Label>
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.mode')}</Label>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Select value={mode} onValueChange={onModeUserChange}>
|
||||
<SelectTrigger tabIndex={-1} className="h-8"><SelectValue /></SelectTrigger>
|
||||
@@ -6014,7 +6019,7 @@ export default function App() {
|
||||
};
|
||||
const freqBlock = (
|
||||
<div className="flex flex-col w-32">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1">{t('field.txFreq')} <LockPad on={locks.freq} title="frequency" onToggle={() => toggleLock('freq')} /></Label>
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1">{t('field.txFreq')}</Label>
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
className="font-mono"
|
||||
|
||||
@@ -72,7 +72,7 @@ const en: Dict = {
|
||||
'field.band': 'Band', 'field.mode': 'Mode', 'field.country': 'Country', 'field.comment': 'Comment',
|
||||
'field.note': 'Note', 'field.rstTx': 'RST tx', 'field.rstRx': 'RST rx',
|
||||
'field.txFreq': 'TX Freq (MHz)', 'field.freqTuneHint': 'Type a frequency and press Enter to tune the radio here.', 'field.freq': 'Freq (MHz)', 'field.rxFreq': 'RX Freq (MHz)', 'field.rxBand': 'RX Band',
|
||||
'field.startUtc': 'Start UTC', 'field.endUtc': 'End UTC', 'field.snt': 'Snt', 'field.rcv': 'Rcv',
|
||||
'field.manualEntryOn': 'Manual entry — hold the frequency, band, mode and both times so you can log a contact from paper while the radio stays where it is', 'field.manualEntryOff': 'Back to following the radio and the clock', 'field.startUtc': 'Start UTC', 'field.endUtc': 'End UTC', 'field.snt': 'Snt', 'field.rcv': 'Rcv',
|
||||
'btn.logQso': 'Log QSO', 'btn.clear': 'Clear', 'btn.spot': 'Spot', 'btn.saving': '…',
|
||||
// Language chooser
|
||||
'lang.choose': 'Choose your language', 'lang.chooseHint': 'You can change this later in Settings → General.',
|
||||
@@ -691,7 +691,7 @@ const fr: Dict = {
|
||||
'field.band': 'Bande', 'field.mode': 'Mode', 'field.country': 'Pays', 'field.comment': 'Commentaire',
|
||||
'field.note': 'Note', 'field.rstTx': 'RST tx', 'field.rstRx': 'RST rx',
|
||||
'field.txFreq': 'Fréq TX (MHz)', 'field.freqTuneHint': 'Tape une fréquence et appuie sur Entrée pour y accorder la radio.', 'field.freq': 'Fréq (MHz)', 'field.rxFreq': 'Fréq RX (MHz)', 'field.rxBand': 'Bande RX',
|
||||
'field.startUtc': 'Début UTC', 'field.endUtc': 'Fin UTC', 'field.snt': 'Env', 'field.rcv': 'Reç',
|
||||
'field.manualEntryOn': 'Saisie manuelle — fige la fréquence, la bande, le mode et les deux heures, pour enregistrer un contact depuis une feuille pendant que la radio reste où elle est', 'field.manualEntryOff': 'Revenir au suivi de la radio et de l’horloge', 'field.startUtc': 'Début UTC', 'field.endUtc': 'Fin UTC', 'field.snt': 'Env', 'field.rcv': 'Reç',
|
||||
'btn.logQso': 'Enregistrer', 'btn.clear': 'Effacer', 'btn.spot': 'Spot', 'btn.saving': '…',
|
||||
'lang.choose': 'Choisissez votre langue', 'lang.chooseHint': 'Modifiable plus tard dans Réglages → Général.',
|
||||
'lang.english': 'English', 'lang.french': 'Français', 'whatsnew.title': 'Nouveautés', 'whatsnew.newTag': 'Nouveau', 'whatsnew.close': 'Compris', 'whatsnew.none': 'Aucune nouveauté pour cette version pour le moment.',
|
||||
|
||||
Reference in New Issue
Block a user