qsl designer

This commit is contained in:
2026-06-11 21:54:35 +02:00
parent 6150498a9e
commit 408b29896c
252 changed files with 13989 additions and 277 deletions
+88 -56
View File
@@ -15,6 +15,7 @@ import {
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
GetAudioSettings, SaveAudioSettings, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
GetClublogCtyInfo, SetClublogCtyEnabled, DownloadClublogCty,
GetSecretStatus, SetPassphrase, RemovePassphrase,
GetEmailSettings, SaveEmailSettings, TestEmail,
GetDVKMessages, SetDVKLabel, DVKStartRecord, DVKStopRecord, DVKPreview, DVKStop, GetDVKStatus,
ListClusterServers, SaveClusterServer, DeleteClusterServer,
@@ -113,7 +114,7 @@ const MODE_CATALOG: Array<{ name: string; sent: string; rcvd: string }> = [
const emptyProfile = (): Profile => ({
id: 0,
name: '',
callsign: '', operator: '', owner_callsign: '',
callsign: '', operator: '', op_name: '', owner_callsign: '',
my_grid: '', my_country: '',
my_state: '', my_cnty: '',
my_street: '', my_city: '', my_postal_code: '',
@@ -426,7 +427,29 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
const [autofocusWB, setAutofocusWB] = useState(() => localStorage.getItem('opslog.autofocusWB') !== '0');
const [showBeamMap, setShowBeamMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
const [startEqEnd, setStartEqEnd] = useState(() => localStorage.getItem('opslog.startEqualsEnd') === '1');
const [lookupOnBlur, setLookupOnBlur] = useState(() => localStorage.getItem('opslog.lookupOnBlur') === '1');
const [catModeBeforeFreq, setCatModeBeforeFreq] = useState(() => localStorage.getItem('opslog.catModeBeforeFreq') === '1');
// Password-encryption (secret vault) state.
const [secret, setSecret] = useState<{ has_passphrase: boolean; unlocked: boolean }>({ has_passphrase: false, unlocked: false });
const [ppNew, setPpNew] = useState('');
const [ppConfirm, setPpConfirm] = useState('');
const [ppErr, setPpErr] = useState('');
const [ppBusy, setPpBusy] = useState(false);
const refreshSecret = async () => { try { setSecret(await GetSecretStatus() as any); } catch {} };
useEffect(() => { refreshSecret(); }, []);
const applyPassphrase = async () => {
if (ppNew !== ppConfirm) { setPpErr('Passphrases do not match'); return; }
setPpBusy(true); setPpErr('');
try { await SetPassphrase(ppNew); setPpNew(''); setPpConfirm(''); await refreshSecret(); }
catch (e: any) { setPpErr(String(e?.message ?? e)); }
finally { setPpBusy(false); }
};
const removePassphrase = async () => {
setPpBusy(true); setPpErr('');
try { await RemovePassphrase(ppNew); setPpNew(''); setPpConfirm(''); await refreshSecret(); }
catch (e: any) { setPpErr(String(e?.message ?? e)); }
finally { setPpBusy(false); }
};
// E-mail / SMTP (send QSO recordings).
type EmailCfg = {
@@ -837,6 +860,11 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
<Input className="font-mono uppercase" value={p.operator ?? ''} onChange={(e) => updateActive({ operator: e.target.value })} placeholder="F4XYZ" />
<div className="text-[10px] text-muted-foreground">Who's at the radio (ADIF OPERATOR).</div>
</div>
<div className="space-y-1 col-span-2">
<Label>Operator name</Label>
<Input className="max-w-xs" value={p.op_name ?? ''} onChange={(e) => updateActive({ op_name: e.target.value })} placeholder="e.g. Greg" />
<div className="text-[10px] text-muted-foreground">Your first name used as the signature on QSL cards.</div>
</div>
<div className="space-y-1 col-span-2">
<Label>Owner callsign</Label>
<Input className="font-mono uppercase max-w-xs" value={p.owner_callsign ?? ''} onChange={(e) => updateActive({ owner_callsign: e.target.value })} placeholder="(leave blank if same as station)" />
@@ -1488,6 +1516,13 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
</Select>
</div>
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={catModeBeforeFreq}
onCheckedChange={(c) => { const v = !!c; setCatModeBeforeFreq(v); writeUiPref('opslog.catModeBeforeFreq', v ? '1' : '0'); }}
/>
Set mode before frequency <span className="text-xs text-muted-foreground">(older rigs that drop the mode after a band change)</span>
</label>
<p className="text-xs text-muted-foreground">
Configure your rig (COM port, baud rate, model) in OmniRig's own settings GUI first.
OpsLog will read whichever Rig slot you select here. Set <strong>CAT delay</strong>
@@ -2896,63 +2931,60 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
function GeneralPanel() {
return (
<>
<SectionHeader title="General" hint="App behaviour preferences (saved instantly, machine-local)." />
<div className="space-y-4 max-w-lg">
<label className="flex items-start gap-2 text-sm cursor-pointer">
<Checkbox
checked={autofocusWB}
onCheckedChange={(c) => { const v = !!c; setAutofocusWB(v); writeUiPref('opslog.autofocusWB', v ? '1' : '0'); }}
className="mt-0.5"
/>
<span>
Auto-focus "Worked before" for stations already worked
<span className="block text-xs text-muted-foreground mt-0.5">
When you type a callsign you've contacted before, OpsLog jumps to the Worked before tab. Turn off to stay on your current tab.
</span>
</span>
<SectionHeader title="General" hint="App behaviour (saved instantly)." />
<div className="space-y-3 max-w-lg">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={autofocusWB} onCheckedChange={(c) => { const v = !!c; setAutofocusWB(v); writeUiPref('opslog.autofocusWB', v ? '1' : '0'); }} />
Auto-focus "Worked before" for known stations
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={showBeamMap} onCheckedChange={(c) => { const v = !!c; setShowBeamMap(v); writeUiPref('opslog.showBeamOnMap', v ? '1' : '0'); }} />
Show the antenna beam heading on the Main map
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={startEqEnd} onCheckedChange={(c) => { const v = !!c; setStartEqEnd(v); writeUiPref('opslog.startEqualsEnd', v ? '1' : '0'); }} />
QSO start time = end time <span className="text-xs text-muted-foreground">(matches LoTW when you call a while)</span>
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={lookupOnBlur} onCheckedChange={(c) => { const v = !!c; setLookupOnBlur(v); writeUiPref('opslog.lookupOnBlur', v ? '1' : '0'); }} />
Look up the callsign only after leaving the field <span className="text-xs text-muted-foreground">(not while typing)</span>
</label>
<label className="flex items-start gap-2 text-sm cursor-pointer">
<Checkbox
checked={showBeamMap}
onCheckedChange={(c) => { const v = !!c; setShowBeamMap(v); writeUiPref('opslog.showBeamOnMap', v ? '1' : '0'); }}
className="mt-0.5"
/>
<span>
Show the antenna beam heading on the Main map
<span className="block text-xs text-muted-foreground mt-0.5">
Draws the beam lobe at the rotor heading (and the opposite/both directions when an Ultrabeam is reversed or bidirectional). Turn off to hide it.
</span>
</span>
</label>
<label className="flex items-start gap-2 text-sm cursor-pointer">
<Checkbox
checked={startEqEnd}
onCheckedChange={(c) => { const v = !!c; setStartEqEnd(v); writeUiPref('opslog.startEqualsEnd', v ? '1' : '0'); }}
className="mt-0.5"
/>
<span>
QSO start time = end time (log at completion)
<span className="block text-xs text-muted-foreground mt-0.5">
Sets TIME_ON equal to TIME_OFF the moment you log the QSO instead of when you first entered the call. Useful when you call a station for a while: the logged time then matches the other operator's, so LoTW/eQSL confirmations match.
</span>
</span>
</label>
<label className="flex items-start gap-2 text-sm cursor-pointer">
<Checkbox
checked={catModeBeforeFreq}
onCheckedChange={(c) => { const v = !!c; setCatModeBeforeFreq(v); writeUiPref('opslog.catModeBeforeFreq', v ? '1' : '0'); }}
className="mt-0.5"
/>
<span>
Set CAT mode before frequency (older rigs)
<span className="block text-xs text-muted-foreground mt-0.5">
When clicking a spot, send the mode to the rig first, then the frequency. Some older transceivers drop the mode command if it arrives right after a band change, needing a second click. Both commands are also spaced out slightly to let the rig settle.
</span>
</span>
</label>
<div className="border-t border-border/60 pt-4 space-y-2">
<h4 className="text-sm font-semibold text-foreground">Password encryption</h4>
{secret.has_passphrase ? (
<>
<p className="text-xs text-muted-foreground">
Passwords encrypted{' '}
{secret.unlocked
? <span className="text-emerald-700 font-medium">— unlocked</span>
: <span className="text-amber-600 font-medium">— locked (unlock at launch)</span>}.
</p>
{secret.unlocked && (
<>
<div className="flex items-center gap-2 max-w-md">
<Input type="password" placeholder="New passphrase (to change)" value={ppNew} onChange={(e) => { setPpNew(e.target.value); setPpErr(''); }} className="h-8" />
<Input type="password" placeholder="Confirm" value={ppConfirm} onChange={(e) => { setPpConfirm(e.target.value); setPpErr(''); }} className="h-8" />
<Button size="sm" disabled={!ppNew || ppBusy} onClick={applyPassphrase}>Change</Button>
</div>
<Button variant="outline" size="sm" disabled={ppBusy} onClick={removePassphrase}>Remove encryption (store passwords in clear)</Button>
</>
)}
</>
) : (
<>
<p className="text-xs text-muted-foreground">
Encrypt saved passwords with a passphrase (asked at launch). Stays portable — re-enter it on another PC.
</p>
<div className="flex items-center gap-2 max-w-md">
<Input type="password" placeholder="Passphrase" value={ppNew} onChange={(e) => { setPpNew(e.target.value); setPpErr(''); }} className="h-8" />
<Input type="password" placeholder="Confirm" value={ppConfirm} onChange={(e) => { setPpConfirm(e.target.value); setPpErr(''); }} className="h-8" />
<Button size="sm" disabled={!ppNew || ppNew !== ppConfirm || ppBusy} onClick={applyPassphrase}>Encrypt</Button>
</div>
</>
)}
{ppErr && <div className="text-xs text-destructive">{ppErr}</div>}
</div>
<div className="border-t border-border/60 pt-4 space-y-2">
<h4 className="text-sm font-semibold text-foreground">ClubLog exceptions (DXpedition overrides)</h4>