fix(settings): Preferences stopped lagging behind the keyboard
Typing a cluster macro cost two things per character. The state lived on SettingsModal, so every keystroke in one of those twenty-four boxes re-rendered the WHOLE preferences dialog — every list, every form, every panel. And the save wrote through to Go and into the database on each one: a round trip per character. The editor is now its own module-scoped component with its own state, so a keystroke re-renders twelve rows. And writeUiPrefDebounced holds the database write until the typing stops, while the local cache — which is what everything reads back — is still written at once. Pending values are flushed when the page goes away, so typing and immediately closing does not lose the last word. The behaviour is unchanged: still saved as you type, still no Save button, because a text box whose contents only take effect on some other button is how work gets lost. Also folds the satellite changelog into one [NEW] entry. Satellites are new in this version — nobody reading the notes has seen any of it — so a running account of how it was built, tab then tracking then rotator then where the settings moved to, is the wrong shape. One entry saying what it does.
This commit is contained in:
@@ -916,6 +916,66 @@ const MOTOR_BAND_DEFAULT_KHZ: Record<string, number> = {
|
||||
const relayCountUI = (type: string) =>
|
||||
type === 'kmtronic' || type === 'denkovi' ? 8 : type === 'httpgen' ? 4 : 5;
|
||||
|
||||
// The twelve cluster command macros.
|
||||
//
|
||||
// Module-scoped, with its own state, and that is the point rather than tidiness:
|
||||
// nested inside SettingsModal every keystroke in one of these twenty-four boxes
|
||||
// re-rendered the WHOLE preferences dialog — every list, every form, every
|
||||
// panel — and the letters arrived visibly after the finger had left the key.
|
||||
// Here a keystroke re-renders twelve rows.
|
||||
//
|
||||
// Written on every keystroke as before. This panel has no Save button, and a
|
||||
// text box whose contents only take effect on some other button is how work
|
||||
// gets lost; the database write is what waits (see saveClusterMacros).
|
||||
function ClusterMacroEditor() {
|
||||
const { t } = useI18n();
|
||||
const [macros, setMacros] = useState<ClusterMacro[]>(loadClusterMacros);
|
||||
const setMacro = (i: number, patch: Partial<ClusterMacro>) => {
|
||||
setMacros((cur) => {
|
||||
const next = cur.map((m, j) => (j === i ? { ...m, ...patch } : m));
|
||||
saveClusterMacros(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium">{t('clu.macros')}</span>
|
||||
</div>
|
||||
{/* Two columns of six: twelve rows stacked would push everything else
|
||||
in this panel off the screen. */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1.5">
|
||||
{macros.map((m, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums w-4 text-right shrink-0">{i + 1}</span>
|
||||
<Input
|
||||
className="h-8 w-28 shrink-0 text-xs"
|
||||
placeholder={t('clu.macroLabel')}
|
||||
value={m.label}
|
||||
maxLength={24}
|
||||
onChange={(e) => setMacro(i, { label: e.target.value })}
|
||||
/>
|
||||
{/* 500, not 120. A DXSpider filter is a list of prefixes and an
|
||||
operator's own list of wanted countries runs past a hundred
|
||||
characters easily — the field simply stopped accepting
|
||||
keystrokes, with nothing to say why, and the command was saved
|
||||
truncated. The title shows the whole thing, since the box
|
||||
cannot. */}
|
||||
<Input
|
||||
className="h-8 flex-1 min-w-0 font-mono text-xs"
|
||||
placeholder={t('clu.macroCmd')}
|
||||
value={m.cmd}
|
||||
title={m.cmd}
|
||||
maxLength={500}
|
||||
onChange={(e) => setMacro(i, { cmd: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Live status + OPERATE/STANDBY toggle for ONE configured amplifier (by config
|
||||
// id) — SPE / ACOM / PGXL alike. Module-scoped (not a nested component) so it
|
||||
// isn't remounted on every parent render. Polls once a second while shown.
|
||||
@@ -1950,9 +2010,6 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
const [milesUnit, setMilesUnit] = useState(() => localStorage.getItem('opslog.distanceMiles') === '1');
|
||||
const [region, setRegion] = useState<IaruRegion>(() => iaruRegion());
|
||||
const [clusterWorkedSameSlot, setClusterWorkedSameSlot] = useState(() => localStorage.getItem('opslog.clusterWorkedSameSlot') === '1');
|
||||
// Declared HERE and not in ClusterPanel: that renderer is called as a plain
|
||||
// function by the PANELS map, so it must stay hooks-free.
|
||||
const [clusterMacros, setClusterMacros] = useState<ClusterMacro[]>(loadClusterMacros);
|
||||
const [showQsoRate, setShowQsoRate] = useState(() => localStorage.getItem('opslog.showQsoRate') === '1');
|
||||
const [catModeBeforeFreq, setCatModeBeforeFreq] = useState(() => localStorage.getItem('opslog.catModeBeforeFreq') === '1');
|
||||
// Password-encryption (secret vault) state.
|
||||
@@ -5975,14 +6032,6 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
|
||||
function ClusterPanel() {
|
||||
const sorted = [...clusterServers].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
|
||||
// Written on every keystroke. This panel has no Save button, and a pair of
|
||||
// text boxes whose contents only take effect on some other button is how
|
||||
// work gets lost.
|
||||
const setMacro = (i: number, patch: Partial<ClusterMacro>) => {
|
||||
const next = clusterMacros.map((m, j) => (j === i ? { ...m, ...patch } : m));
|
||||
setClusterMacros(next);
|
||||
saveClusterMacros(next);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
@@ -6075,41 +6124,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
{t('clu.autoConnect')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium">{t('clu.macros')}</span>
|
||||
</div>
|
||||
{/* Two columns of six: twelve rows stacked would push everything else
|
||||
in this panel off the screen. */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1.5">
|
||||
{clusterMacros.map((m, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums w-4 text-right shrink-0">{i + 1}</span>
|
||||
<Input
|
||||
className="h-8 w-28 shrink-0 text-xs"
|
||||
placeholder={t('clu.macroLabel')}
|
||||
value={m.label}
|
||||
maxLength={24}
|
||||
onChange={(e) => setMacro(i, { label: e.target.value })}
|
||||
/>
|
||||
{/* 500, not 120. A DXSpider filter is a list of prefixes and
|
||||
an operator's own list of wanted countries runs past a
|
||||
hundred characters easily — the field simply stopped
|
||||
accepting keystrokes, with nothing to say why, and the
|
||||
command was saved truncated. The title shows the whole
|
||||
thing, since the box cannot. */}
|
||||
<Input
|
||||
className="h-8 flex-1 min-w-0 font-mono text-xs"
|
||||
placeholder={t('clu.macroCmd')}
|
||||
value={m.cmd}
|
||||
title={m.cmd}
|
||||
maxLength={500}
|
||||
onChange={(e) => setMacro(i, { cmd: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ClusterMacroEditor />
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer border-t border-border/60 pt-3">
|
||||
<Checkbox checked={clusterWorkedSameSlot} className="mt-0.5"
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterWorkedSameSlot(v); writeUiPref('opslog.clusterWorkedSameSlot', v ? '1' : '0'); }} />
|
||||
|
||||
Reference in New Issue
Block a user