This commit is contained in:
2026-06-06 14:16:30 +02:00
parent f91f9ff3b8
commit 17f7a00bd7
19 changed files with 1278 additions and 91 deletions
+7 -7
View File
@@ -2861,7 +2861,7 @@ export default function App() {
<DialogHeader className="px-2">
<DialogTitle>Export ADIF</DialogTitle>
<DialogDescription>
Choose which fields to include in the export.
Choose which fields to include. OpsLog writes ADIF 3.1.7.
</DialogDescription>
</DialogHeader>
<div className="px-2 py-1 space-y-2.5">
@@ -2870,10 +2870,10 @@ export default function App() {
onClick={() => runExport(false)}
className="w-full text-left rounded-lg border border-border p-3 hover:border-primary/60 hover:bg-accent/30 transition-colors"
>
<div className="font-semibold text-sm">Standard ADIF</div>
<div className="font-semibold text-sm">Standard ADIF only</div>
<div className="text-xs text-muted-foreground mt-0.5">
Only standard ADIF-defined fields portable to other loggers (Log4OM, N1MM, LoTW).
Application-specific <span className="font-mono">APP_*</span> tags are stripped.
Only fields defined in the ADIF 3.1.7 spec portable to other loggers (Log4OM, N1MM, LoTW).
Application-specific <span className="font-mono">APP_*</span> and any non-standard / vendor tags are stripped.
</div>
</button>
<button
@@ -2881,10 +2881,10 @@ export default function App() {
onClick={() => runExport(true)}
className="w-full text-left rounded-lg border border-border p-3 hover:border-primary/60 hover:bg-accent/30 transition-colors"
>
<div className="font-semibold text-sm">Full (OpsLog round-trip)</div>
<div className="font-semibold text-sm">All fields (OpsLog round-trip)</div>
<div className="text-xs text-muted-foreground mt-0.5">
Every field including OpsLog/application-specific <span className="font-mono">APP_*</span> tags
for a lossless backup you'll re-import into OpsLog.
Every field including application-specific <span className="font-mono">APP_*</span> and vendor tags
a lossless backup you'll re-import into OpsLog.
</div>
</button>
</div>
@@ -0,0 +1,132 @@
import { useEffect, useMemo, useState } from 'react';
import { X, Plus } from 'lucide-react';
import { ADIFFields } from '../../wailsjs/go/main/App';
import { Input } from '@/components/ui/input';
import { Combobox } from '@/components/ui/combobox';
import { Button } from '@/components/ui/button';
type FieldDef = {
name: string; kind: string; category: string;
promoted: boolean; deprecated: boolean; intl: boolean;
};
interface Props {
// The QSO's extras map (uppercase ADIF tag → raw value).
value: Record<string, string> | undefined;
onChange: (next: Record<string, string> | undefined) => void;
}
// AdifExtrasEditor — a dictionary-driven editor for every ADIF field that is
// NOT promoted to a first-class column. Backed by the QSO's extras map, so any
// ADIF 3.1.7 field (plus custom/vendor tags) can be viewed and edited. This is
// what makes "100% of ADIF fields available" true without 160 DB columns.
export function AdifExtrasEditor({ value, onChange }: Props) {
const [dict, setDict] = useState<FieldDef[]>([]);
const [showDeprecated, setShowDeprecated] = useState(false);
useEffect(() => {
ADIFFields().then((f) => setDict((f ?? []) as any)).catch(() => {});
}, []);
// Entries currently set, sorted for stable display.
const entries = useMemo(
() => Object.entries(value ?? {}).sort((a, b) => a[0].localeCompare(b[0])),
[value],
);
// Addable fields: standard non-promoted ADIF fields not already present.
// Deprecated (import-only) fields are hidden unless the toggle is on.
const addable = useMemo(() => {
const have = new Set(Object.keys(value ?? {}));
return dict
.filter((f) => !f.promoted && !have.has(f.name) && (showDeprecated || !f.deprecated))
.map((f) => f.name);
}, [dict, value, showDeprecated]);
const meta = useMemo(() => {
const m: Record<string, FieldDef> = {};
for (const f of dict) m[f.name] = f;
return m;
}, [dict]);
function setKV(key: string, val: string) {
const next = { ...(value ?? {}) };
next[key] = val;
onChange(next);
}
function remove(key: string) {
const next = { ...(value ?? {}) };
delete next[key];
onChange(Object.keys(next).length ? next : undefined);
}
function addField(name: string) {
const key = name.trim().toUpperCase();
if (!key || (value && key in value)) return;
setKV(key, '');
}
return (
<div className="space-y-3">
<p className="text-xs text-muted-foreground">
Every ADIF 3.1.7 field not shown in the other tabs. Pick a field to add it,
or type a custom/vendor tag (e.g. <code className="bg-muted px-1 rounded font-mono">APP_*</code>).
Stored losslessly and exported in the <strong>full</strong> ADIF mode.
</p>
{/* Add a field */}
<div className="flex items-center gap-2">
<div className="flex-1 max-w-xs">
<Combobox
value=""
options={addable}
placeholder="Add ADIF field…"
allowFreeText
onChange={addField}
/>
</div>
<label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer">
<input type="checkbox" checked={showDeprecated} onChange={(e) => setShowDeprecated(e.target.checked)} />
Show deprecated
</label>
</div>
{/* Current entries */}
{entries.length === 0 ? (
<div className="text-[11px] text-muted-foreground italic border border-dashed border-border rounded-md px-3 py-4 text-center">
No extra ADIF fields. Use the picker above to add one.
</div>
) : (
<div className="space-y-1.5">
{entries.map(([k, v]) => {
const def = meta[k];
return (
<div key={k} className="flex items-center gap-2">
<div className="w-48 shrink-0">
<span className="font-mono text-xs font-semibold">{k}</span>
{def && (
<span className="block text-[10px] text-muted-foreground leading-tight">
{def.category}{def.deprecated ? ' · deprecated' : ''}{def.intl ? ' · intl' : ''}
{!def && ''}
</span>
)}
{!def && <span className="block text-[10px] text-amber-600 leading-tight">non-standard</span>}
</div>
<Input
className="flex-1 h-8 text-xs font-mono"
value={v}
onChange={(e) => setKV(k, e.target.value)}
/>
<Button
type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive"
onClick={() => remove(k)} title="Remove field"
>
<X className="size-3.5" />
</Button>
</div>
);
})}
</div>
)}
</div>
);
}
+31 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { Plus, Trash2, RotateCcw, Save, Download, Loader2, Search } from 'lucide-react';
import { Plus, Trash2, RotateCcw, Save, Download, Upload, Loader2, Search } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -17,6 +17,7 @@ import {
ImportAwardReferencesText, GetAwardPresets, ApplyAwardPreset,
ListCountries, DXCCForCountry, DXCCName,
PopulateBuiltinReferences, HasBuiltinReferences,
ExportAwards, ImportAwards,
} from '../../wailsjs/go/main/App';
// Above this many references the editor stops loading the whole list and
@@ -182,6 +183,28 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
async function reset() {
try { setDefs((await ResetAwardDefs()) as any); setSel(0); } catch (e: any) { setErr(String(e?.message ?? e)); }
}
// Export every award definition + reference list to a JSON bundle (backup
// that survives a reinstall / PC change, independent of the database).
async function exportAwards() {
setErr('');
try {
const p = await ExportAwards();
if (p) setErr(`Awards exported to:\n${p}`);
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
// Import an award bundle: definitions are upserted by code, reference lists
// replaced. Reloads the editor afterwards.
async function importAwards() {
setErr('');
try {
const r: any = await ImportAwards();
if (!r || (!r.awards && !r.references)) return; // cancelled
const [d] = await Promise.all([GetAwardDefs(), loadMeta()]);
setDefs((d ?? []) as any); setSel(0);
onSaved();
setErr(`Imported ${r.awards} award(s) and ${r.references} reference(s).`);
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function updateList(code: string) {
setUpdating(code); setErr('');
try { await UpdateAwardReferenceList(code); await loadMeta(); }
@@ -228,7 +251,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
{/* Right: tabbed editor for selected award */}
<div className="flex flex-col min-h-0 overflow-hidden">
{err && <div className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5">{err}</div>}
{err && <div className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5 whitespace-pre-line break-all">{err}</div>}
{!cur ? (
<div className="flex-1 grid place-items-center text-sm text-muted-foreground">Select or create an award.</div>
) : (
@@ -338,6 +361,12 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
<DialogFooter className="px-5 py-3 border-t !flex-row">
<Button variant="ghost" onClick={reset}><RotateCcw className="size-3.5 mr-1" /> Reset to defaults</Button>
<Button variant="outline" onClick={exportAwards} title="Export all award definitions + reference lists to a JSON backup">
<Download className="size-3.5 mr-1" /> Export
</Button>
<Button variant="outline" onClick={importAwards} title="Import an award bundle (definitions + reference lists)">
<Upload className="size-3.5 mr-1" /> Import
</Button>
<div className="flex-1" />
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button onClick={save}><Save className="size-3.5 mr-1" /> Save</Button>
+31 -19
View File
@@ -11,7 +11,11 @@ type Meta = { code: string; count: number; can_update: boolean };
// Fields auto-derived from structured QSO data — their awards (DXCC/WAZ/WAS/…)
// are computed, never manually picked, so they don't belong in this picker.
const COMPUTED_FIELDS = new Set(['dxcc', 'cqz', 'ituz', 'prefix', 'callsign', 'state', 'cont', 'country', 'grid']);
// Fields purely derived from the callsign / cty.dat — their awards are computed,
// never picked. NB: 'state' and 'cnty' are NOT here — they're operator-settable
// QSO fields driving predefined-list awards (WAS/RAC/WAJA/JCC), so they ARE
// pickable (a lookup rarely fills the JA prefecture or VE province).
const COMPUTED_FIELDS = new Set(['dxcc', 'cqz', 'ituz', 'prefix', 'callsign', 'cont', 'country', 'grid']);
// If DXCC-filtered auto-results exceed this, require the user to type instead.
const AUTO_SHOW_MAX = 100;
@@ -72,15 +76,26 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
if (!awards.find((a) => a.code === awardCode)) setAwardCode(awards[0].code);
}, [awards, awardCode]);
// Auto-load DXCC-filtered refs on award/dxcc change with empty query.
// Fetches AUTO_SHOW_MAX+1 so we can distinguish "all results shown" from "too many".
// Is the selected award a giant dynamic list (POTA/SOTA/IOTA)? Those carry a
// per-reference DXCC so we filter by entity; predefined lists (WAS/RAC/WAJA)
// are small and their refs may lack a per-ref DXCC, so we load them whole.
const isDynamic = useMemo(
() => !!defs.find((d) => d.code === awardCode)?.dynamic,
[defs, awardCode],
);
// For dynamic lists, restrict to the contacted entity; otherwise load all.
const refDxcc = isDynamic ? (dxcc ?? 0) : 0;
// Auto-load refs on award/dxcc change with empty query. Fetches AUTO_SHOW_MAX+1
// so we can distinguish "all results shown" from "too many to list".
useEffect(() => {
setAutoResults([]);
if (!dxcc) return;
SearchAwardReferences(awardCode, '', dxcc, AUTO_SHOW_MAX + 1)
// Dynamic lists need an entity to scope to; predefined lists load regardless.
if (isDynamic && !dxcc) return;
SearchAwardReferences(awardCode, '', refDxcc, AUTO_SHOW_MAX + 1)
.then((r) => setAutoResults((r ?? []) as any))
.catch(() => {});
}, [awardCode, dxcc]);
}, [awardCode, dxcc, isDynamic, refDxcc]);
// Typed search (2+ chars).
useEffect(() => {
@@ -88,13 +103,13 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
const t = window.setTimeout(async () => {
setBusy(true);
try {
const r = await SearchAwardReferences(awardCode, q, dxcc ?? 0, 50);
const r = await SearchAwardReferences(awardCode, q, refDxcc, 50);
setSearchResults((r ?? []) as any);
} catch { setSearchResults([]); }
finally { setBusy(false); }
}, 200);
return () => window.clearTimeout(t);
}, [awardCode, q, dxcc]);
}, [awardCode, q, refDxcc]);
const tooManyAuto = autoResults.length > AUTO_SHOW_MAX;
// When q is empty/short: show auto-results (if ≤ AUTO_SHOW_MAX); when typing: show search results.
@@ -212,22 +227,19 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
<Loader2 className="size-3 animate-spin" />Searching
</div>
)}
{/* No callsign yet */}
{!busy && !dxcc && q.length < 2 && (
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
Enter a callsign, or type to search.
</div>
)}
{/* DXCC known but too many auto-results → require typed search */}
{!busy && !!dxcc && q.length < 2 && tooManyAuto && (
{/* Too many auto-results → require typed search */}
{!busy && q.length < 2 && tooManyAuto && (
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
Type 2+ chars to search
</div>
)}
{/* DXCC known, auto-results loaded, none found */}
{!busy && !!dxcc && q.length < 2 && !tooManyAuto && autoResults.length === 0 && (
{/* Empty short-query state: prompt for a callsign (dynamic lists) or
note the list is empty (predefined awards with no references). */}
{!busy && q.length < 2 && !tooManyAuto && autoResults.length === 0 && (
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
No references for this entity.
{isDynamic && !dxcc
? 'Enter a callsign, or type to search.'
: 'No references for this entity.'}
</div>
)}
{/* Typed search, no results */}
+3 -3
View File
@@ -120,7 +120,7 @@ export function AwardsPanel() {
const filteredRefs = useMemo(() => {
if (!current) return [];
const q = refSearch.trim().toUpperCase();
return current.refs.filter((r) => {
return (current.refs ?? []).filter((r) => {
if (refFilter === 'worked' && !r.worked) return false;
if (refFilter === 'notworked' && r.worked) return false;
if (refFilter === 'worked_notconf' && !(r.worked && !r.confirmed)) return false;
@@ -206,11 +206,11 @@ export function AwardsPanel() {
</div>
{/* Band breakdown */}
{current.bands.length > 0 && (
{(current.bands ?? []).length > 0 && (
<div className="px-4 py-2 border-b border-border/60">
<div className="text-[11px] uppercase tracking-wider text-muted-foreground mb-1.5">By band (confirmed / worked)</div>
<div className="flex flex-wrap gap-1.5">
{current.bands.map((b) => (
{(current.bands ?? []).map((b) => (
<div key={b.band} className="rounded-md border border-border bg-card px-2 py-1 text-xs">
<span className="font-mono font-semibold">{b.band}</span>{' '}
<span className="text-emerald-600 font-mono">{b.confirmed}</span>
+87 -34
View File
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Trash2, Search, Loader2 } from 'lucide-react';
import { LookupCallsign, DXCCForCountry, GetAwardDefs, ComputeQSOAwardRefs } from '../../wailsjs/go/main/App';
import { AwardRefSelector } from '@/components/AwardRefSelector';
import { applyAwardRefs, buildAwardRefs } from '@/lib/awardRefs';
import { AdifExtrasEditor } from '@/components/AdifExtrasEditor';
import { applyAwardRefs } from '@/lib/awardRefs';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
@@ -100,23 +101,6 @@ function parseLocalISO(s: string): string | null {
if (!m) return null;
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:00.000Z`;
}
function stringifyExtras(e?: Record<string, string>): string {
if (!e) return '';
return Object.entries(e).map(([k, v]) => `${k} = ${v}`).join('\n');
}
function parseExtras(t: string): Record<string, string> | undefined {
const out: Record<string, string> = {};
for (const raw of t.split('\n')) {
const line = raw.trim();
if (!line) continue;
const idx = line.indexOf('=');
if (idx < 0) continue;
const k = line.slice(0, idx).trim().toUpperCase();
const v = line.slice(idx + 1).trim();
if (k && v) out[k] = v;
}
return Object.keys(out).length ? out : undefined;
}
function numOrUndef(v: any): number | undefined {
if (v === '' || v === null || v === undefined) return undefined;
const n = typeof v === 'number' ? v : parseFloat(String(v));
@@ -163,7 +147,6 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
const [dateOff, setDateOff] = useState(toLocalISO(draft.qso_date_off));
const [endEnabled, setEndEnabled] = useState(!!draft.qso_date_off);
const [confSel, setConfSel] = useState('QSL'); // selected confirmation channel
const [extrasText, setExtrasText] = useState(stringifyExtras(draft.extras));
const [localErr, setLocalErr] = useState('');
const [saving, setSaving] = useState(false);
const [looking, setLooking] = useState(false);
@@ -183,15 +166,17 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
const fieldOf: Record<string, string> = {};
for (const d of list) fieldOf[String(d.code).toUpperCase()] = String(d.field || '').toLowerCase();
awardFieldRef.current = fieldOf;
// Which awards are reference-list (manual) ones? Ask the backend, which
// also tells us pickable vs computed for the current QSO.
// Seed the editable manual refs from the backend, which already matched
// each reference against its award's own list. Seeding from the raw QSO
// field instead would wrongly seed every state-award (WAS/RAC/WAJA) from
// the same `state` value — e.g. a US "CA" would seed RAC@CA too.
try {
const all = (await ComputeQSOAwardRefs(draft as any)) ?? [];
const pickableCodes = new Set(all.filter((r: any) => r.pickable).map((r: any) => String(r.code).toUpperCase()));
const pickable = list
.filter((d) => pickableCodes.has(String(d.code).toUpperCase()))
.map((d) => ({ code: String(d.code), field: String(d.field || '').toLowerCase() }));
setAwardRefs(buildAwardRefs(draft, pickable));
const seed = all
.filter((r: any) => r.pickable)
.map((r: any) => `${String(r.code).toUpperCase()}@${String(r.ref).toUpperCase()}`)
.join(';');
setAwardRefs(seed);
} catch { /* leave manual refs empty on failure */ }
})
.catch(() => {});
@@ -292,7 +277,12 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
my_lat: numOrUndef(draft.my_lat), my_lon: numOrUndef(draft.my_lon),
ant_az: numOrUndef(draft.ant_az), ant_el: numOrUndef(draft.ant_el),
tx_pwr: numOrUndef(draft.tx_pwr),
extras: parseExtras(extrasText),
distance: numOrUndef(draft.distance),
rx_pwr: numOrUndef(draft.rx_pwr),
a_index: numOrUndef(draft.a_index),
k_index: numOrUndef(draft.k_index),
sfi: numOrUndef(draft.sfi),
extras: draft.extras && Object.keys(draft.extras).length ? draft.extras : undefined,
};
// The Award Refs tab is authoritative for the reference-list awards. Reset
// the dedicated columns, then route the picked refs back onto the payload
@@ -334,8 +324,9 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
<TabsTrigger value="contest">Contest</TabsTrigger>
<TabsTrigger value="sat">Sat / Prop</TabsTrigger>
<TabsTrigger value="mystation">My Station</TabsTrigger>
<TabsTrigger value="moreadif">More ADIF</TabsTrigger>
<TabsTrigger value="extras">
Extras
ADIF fields
{extrasCount > 0 && (
<Badge variant="accent" className="ml-1 px-1.5 py-0 text-[9px]">{extrasCount}</Badge>
)}
@@ -602,12 +593,74 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }:
</div>
</TabsContent>
<TabsContent value="extras" className="mt-0 space-y-2">
<p className="text-xs text-muted-foreground">
ADIF fields not promoted to first-class columns. One per line:{' '}
<code className="bg-muted px-1 py-0.5 rounded font-mono">FIELD_NAME = value</code>
</p>
<Textarea rows={14} className="font-mono text-xs" value={extrasText} onChange={(e) => setExtrasText(e.target.value)} />
<TabsContent value="moreadif" className="mt-0 space-y-4">
{/* Special activity (POTA/SOTA/WWFF/SIG) */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Special activity</p>
<div className="grid grid-cols-6 gap-3">
<F label="SIG"><Input value={draft.sig ?? ''} placeholder="POTA" onChange={(e) => set('sig', e.target.value)} /></F>
<F label="SIG info" span={2}><Input value={draft.sig_info ?? ''} placeholder="US-0001" onChange={(e) => set('sig_info', e.target.value)} /></F>
<F label="WWFF ref" span={2}><Input value={draft.wwff_ref ?? ''} placeholder="ONFF-0001" onChange={(e) => set('wwff_ref', e.target.value)} className="font-mono uppercase" /></F>
<F label="Region"><Input value={draft.region ?? ''} onChange={(e) => set('region', e.target.value)} /></F>
</div>
</div>
{/* Power & propagation */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Power &amp; space weather</p>
<div className="grid grid-cols-6 gap-3">
<F label="RX power (W)"><Input type="number" value={draft.rx_pwr ?? ''} onChange={(e) => set('rx_pwr', numOrUndef(e.target.value) as any)} /></F>
<F label="Distance (km)"><Input type="number" value={draft.distance ?? ''} onChange={(e) => set('distance', numOrUndef(e.target.value) as any)} /></F>
<F label="A index"><Input type="number" value={draft.a_index ?? ''} onChange={(e) => set('a_index', numOrUndef(e.target.value) as any)} /></F>
<F label="K index"><Input type="number" value={draft.k_index ?? ''} onChange={(e) => set('k_index', numOrUndef(e.target.value) as any)} /></F>
<F label="SFI"><Input type="number" value={draft.sfi ?? ''} onChange={(e) => set('sfi', numOrUndef(e.target.value) as any)} /></F>
</div>
</div>
{/* Identity & clubs */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Identity &amp; clubs</p>
<div className="grid grid-cols-6 gap-3">
<F label="Contacted op" span={2}><Input value={draft.contacted_op ?? ''} placeholder="EA8XYZ" onChange={(e) => set('contacted_op', e.target.value)} className="font-mono uppercase" /></F>
<F label="Former call (EQ_CALL)" span={2}><Input value={draft.eq_call ?? ''} onChange={(e) => set('eq_call', e.target.value)} className="font-mono uppercase" /></F>
<F label="Class"><Input value={draft.class ?? ''} placeholder="1A" onChange={(e) => set('class', e.target.value)} /></F>
<F label="SKCC"><Input value={draft.skcc ?? ''} onChange={(e) => set('skcc', e.target.value)} /></F>
<F label="FISTS"><Input value={draft.fists ?? ''} onChange={(e) => set('fists', e.target.value)} /></F>
<F label="Ten-Ten"><Input value={draft.ten_ten ?? ''} onChange={(e) => set('ten_ten', e.target.value)} /></F>
<F label="DARC DOK"><Input value={draft.darc_dok ?? ''} onChange={(e) => set('darc_dok', e.target.value)} /></F>
</div>
</div>
{/* Flags & credits */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Flags &amp; credits</p>
<div className="grid grid-cols-6 gap-3">
<F label="QSO complete"><Input value={draft.qso_complete ?? ''} placeholder="Y/N/NIL/?" onChange={(e) => set('qso_complete', e.target.value)} /></F>
<F label="QSO random"><Input value={draft.qso_random ?? ''} placeholder="Y/N" onChange={(e) => set('qso_random', e.target.value)} /></F>
<F label="Silent key"><Input value={draft.silent_key ?? ''} placeholder="Y/N" onChange={(e) => set('silent_key', e.target.value)} /></F>
<F label="SWL"><Input value={draft.swl ?? ''} placeholder="Y/N" onChange={(e) => set('swl', e.target.value)} /></F>
<F label="Credit granted" span={3}><Input value={draft.credit_granted ?? ''} placeholder="DXCC,WAS" onChange={(e) => set('credit_granted', e.target.value)} /></F>
<F label="Credit submitted" span={3}><Input value={draft.credit_submitted ?? ''} onChange={(e) => set('credit_submitted', e.target.value)} /></F>
</div>
</div>
{/* My station extras */}
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">My station (ADIF)</p>
<div className="grid grid-cols-6 gap-3">
<F label="My name" span={2}><Input value={draft.my_name ?? ''} onChange={(e) => set('my_name', e.target.value)} /></F>
<F label="My WWFF ref" span={2}><Input value={draft.my_wwff_ref ?? ''} onChange={(e) => set('my_wwff_ref', e.target.value)} className="font-mono uppercase" /></F>
<F label="My ARRL sect" span={2}><Input value={draft.my_arrl_sect ?? ''} onChange={(e) => set('my_arrl_sect', e.target.value)} /></F>
<F label="My SIG"><Input value={draft.my_sig ?? ''} onChange={(e) => set('my_sig', e.target.value)} /></F>
<F label="My SIG info" span={2}><Input value={draft.my_sig_info ?? ''} onChange={(e) => set('my_sig_info', e.target.value)} /></F>
<F label="My DARC DOK"><Input value={draft.my_darc_dok ?? ''} onChange={(e) => set('my_darc_dok', e.target.value)} /></F>
<F label="My VUCC grids" span={2}><Input value={draft.my_vucc_grids ?? ''} onChange={(e) => set('my_vucc_grids', e.target.value)} className="font-mono uppercase" /></F>
</div>
</div>
</TabsContent>
<TabsContent value="extras" className="mt-0">
<AdifExtrasEditor value={draft.extras} onChange={(next) => set('extras', next as any)} />
</TabsContent>
</div>
</Tabs>
+6
View File
@@ -44,6 +44,10 @@ export function applyAwardRefs(payload: any, awardRefs: string, fieldOf: Record<
case 'iota': payload.iota = ref; break;
case 'sota_ref': payload.sota_ref = ref; break;
case 'pota_ref': payload.pota_ref = ref; break;
// Predefined-list awards on a QSO field (WAS/RAC/WAJA on state, JCC on
// county): picking a reference writes it straight into that column.
case 'state': payload.state = ref; break;
case 'cnty': payload.cnty = ref; break;
case 'wwff':
extras['WWFF_REF'] = ref;
extras['SIG'] = 'WWFF';
@@ -73,6 +77,8 @@ export function awardRefValue(qso: any, code: string, field: string): string {
case 'iota': return (qso.iota ?? '').toUpperCase();
case 'sota_ref': return (qso.sota_ref ?? '').toUpperCase();
case 'pota_ref': return (qso.pota_ref ?? '').toUpperCase();
case 'state': return (qso.state ?? '').toUpperCase();
case 'cnty': return (qso.cnty ?? '').toUpperCase();
case 'wwff': {
const ex = qso.extras ?? {};
if (ex['WWFF_REF']) return String(ex['WWFF_REF']).toUpperCase();
+8 -6
View File
@@ -1,9 +1,9 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {adif} from '../models';
import {qso} from '../models';
import {main} from '../models';
import {profile} from '../models';
import {adif} from '../models';
import {award} from '../models';
import {awardref} from '../models';
import {cat} from '../models';
@@ -15,6 +15,10 @@ import {operating} from '../models';
import {udp} from '../models';
import {lookup} from '../models';
export function ADIFFields():Promise<Array<adif.FieldDef>>;
export function ADIFVersion():Promise<string>;
export function ActivateProfile(arg1:number):Promise<void>;
export function AddQSO(arg1:qso.QSO):Promise<number>;
@@ -77,8 +81,6 @@ export function DeleteQSO(arg1:number):Promise<void>;
export function DeleteUDPIntegration(arg1:number):Promise<void>;
export function DisablePortableMode():Promise<void>;
export function DisconnectAllClusters():Promise<void>;
export function DisconnectClusterServer(arg1:number):Promise<void>;
@@ -89,14 +91,14 @@ export function DownloadConfirmations(arg1:string,arg2:boolean):Promise<void>;
export function DuplicateProfile(arg1:number,arg2:string):Promise<profile.Profile>;
export function EnablePortableMode():Promise<void>;
export function ExportADIF(arg1:string,arg2:boolean):Promise<adif.ExportResult>;
export function ExportADIFFiltered(arg1:string,arg2:boolean,arg3:qso.QueryFilter):Promise<adif.ExportResult>;
export function ExportADIFSelected(arg1:string,arg2:boolean,arg3:Array<number>):Promise<adif.ExportResult>;
export function ExportAwards():Promise<string>;
export function FilterFields():Promise<Array<string>>;
export function FindQSOsForUpload(arg1:string,arg2:string):Promise<Array<qso.UploadRow>>;
@@ -175,7 +177,7 @@ export function ImportADIF(arg1:string,arg2:string,arg3:boolean):Promise<adif.Im
export function ImportAwardReferencesText(arg1:string,arg2:string):Promise<number>;
export function IsPortableMode():Promise<boolean>;
export function ImportAwards():Promise<main.AwardImportResult>;
export function ListAudioInputDevices():Promise<Array<audio.Device>>;
+14 -10
View File
@@ -2,6 +2,14 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function ADIFFields() {
return window['go']['main']['App']['ADIFFields']();
}
export function ADIFVersion() {
return window['go']['main']['App']['ADIFVersion']();
}
export function ActivateProfile(arg1) {
return window['go']['main']['App']['ActivateProfile'](arg1);
}
@@ -126,10 +134,6 @@ export function DeleteUDPIntegration(arg1) {
return window['go']['main']['App']['DeleteUDPIntegration'](arg1);
}
export function DisablePortableMode() {
return window['go']['main']['App']['DisablePortableMode']();
}
export function DisconnectAllClusters() {
return window['go']['main']['App']['DisconnectAllClusters']();
}
@@ -150,10 +154,6 @@ export function DuplicateProfile(arg1, arg2) {
return window['go']['main']['App']['DuplicateProfile'](arg1, arg2);
}
export function EnablePortableMode() {
return window['go']['main']['App']['EnablePortableMode']();
}
export function ExportADIF(arg1, arg2) {
return window['go']['main']['App']['ExportADIF'](arg1, arg2);
}
@@ -166,6 +166,10 @@ export function ExportADIFSelected(arg1, arg2, arg3) {
return window['go']['main']['App']['ExportADIFSelected'](arg1, arg2, arg3);
}
export function ExportAwards() {
return window['go']['main']['App']['ExportAwards']();
}
export function FilterFields() {
return window['go']['main']['App']['FilterFields']();
}
@@ -322,8 +326,8 @@ export function ImportAwardReferencesText(arg1, arg2) {
return window['go']['main']['App']['ImportAwardReferencesText'](arg1, arg2);
}
export function IsPortableMode() {
return window['go']['main']['App']['IsPortableMode']();
export function ImportAwards() {
return window['go']['main']['App']['ImportAwards']();
}
export function ListAudioInputDevices() {
+98
View File
@@ -16,6 +16,28 @@ export namespace adif {
this.size_kb = source["size_kb"];
}
}
export class FieldDef {
name: string;
kind: string;
category: string;
promoted: boolean;
deprecated: boolean;
intl: boolean;
static createFrom(source: any = {}) {
return new FieldDef(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.kind = source["kind"];
this.category = source["category"];
this.promoted = source["promoted"];
this.deprecated = source["deprecated"];
this.intl = source["intl"];
}
}
export class ImportResult {
total: number;
imported: number;
@@ -660,6 +682,20 @@ export namespace main {
this.mic_gain = source["mic_gain"];
}
}
export class AwardImportResult {
awards: number;
references: number;
static createFrom(source: any = {}) {
return new AwardImportResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.awards = source["awards"];
this.references = source["references"];
}
}
export class AwardRefMeta {
code: string;
count: number;
@@ -1184,6 +1220,7 @@ export namespace main {
ok: boolean;
err: string;
db_path: string;
migrated_from_app_data: boolean;
static createFrom(source: any = {}) {
return new StartupStatus(source);
@@ -1194,6 +1231,7 @@ export namespace main {
this.ok = source["ok"];
this.err = source["err"];
this.db_path = source["db_path"];
this.migrated_from_app_data = source["migrated_from_app_data"];
}
}
export class StationInfoComputed {
@@ -1696,6 +1734,36 @@ export namespace qso {
tx_pwr?: number;
comment?: string;
notes?: string;
sig?: string;
sig_info?: string;
my_sig?: string;
my_sig_info?: string;
wwff_ref?: string;
my_wwff_ref?: string;
distance?: number;
rx_pwr?: number;
a_index?: number;
k_index?: number;
sfi?: number;
skcc?: string;
fists?: string;
ten_ten?: string;
contacted_op?: string;
eq_call?: string;
pfx?: string;
my_name?: string;
class?: string;
darc_dok?: string;
my_darc_dok?: string;
region?: string;
silent_key?: string;
swl?: string;
qso_complete?: string;
qso_random?: string;
credit_granted?: string;
credit_submitted?: string;
my_arrl_sect?: string;
my_vucc_grids?: string;
extras?: Record<string, string>;
// Go type: time
created_at: any;
@@ -1803,6 +1871,36 @@ export namespace qso {
this.tx_pwr = source["tx_pwr"];
this.comment = source["comment"];
this.notes = source["notes"];
this.sig = source["sig"];
this.sig_info = source["sig_info"];
this.my_sig = source["my_sig"];
this.my_sig_info = source["my_sig_info"];
this.wwff_ref = source["wwff_ref"];
this.my_wwff_ref = source["my_wwff_ref"];
this.distance = source["distance"];
this.rx_pwr = source["rx_pwr"];
this.a_index = source["a_index"];
this.k_index = source["k_index"];
this.sfi = source["sfi"];
this.skcc = source["skcc"];
this.fists = source["fists"];
this.ten_ten = source["ten_ten"];
this.contacted_op = source["contacted_op"];
this.eq_call = source["eq_call"];
this.pfx = source["pfx"];
this.my_name = source["my_name"];
this.class = source["class"];
this.darc_dok = source["darc_dok"];
this.my_darc_dok = source["my_darc_dok"];
this.region = source["region"];
this.silent_key = source["silent_key"];
this.swl = source["swl"];
this.qso_complete = source["qso_complete"];
this.qso_random = source["qso_random"];
this.credit_granted = source["credit_granted"];
this.credit_submitted = source["credit_submitted"];
this.my_arrl_sect = source["my_arrl_sect"];
this.my_vucc_grids = source["my_vucc_grids"];
this.extras = source["extras"];
this.created_at = this.convertValues(source["created_at"], null);
this.updated_at = this.convertValues(source["updated_at"], null);