This commit is contained in:
2026-05-26 00:56:08 +02:00
parent 7ace2cc602
commit 7e518ddba3
10 changed files with 51169 additions and 51 deletions
+124 -34
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
AlertCircle, Antenna, CheckCircle2, Clock, Compass, Hash, Loader2, Lock,
Maximize2, Minimize2, Pencil, RadioTower, RefreshCw, Send, Settings, Trash2, Unlock, X,
Maximize2, Minimize2, Pencil, RadioTower, RefreshCw, Send, Settings, Square, Trash2, Unlock, X,
} from 'lucide-react';
import {
@@ -14,6 +14,7 @@ import {
SetCompactMode,
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig,
RefreshCtyDat,
RotatorGoTo, RotatorStop,
GetCATSettings,
} from '../wailsjs/go/main/App';
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime';
@@ -32,6 +33,10 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import {
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
@@ -344,6 +349,11 @@ export default function App() {
const [importing, setImporting] = useState(false);
const [importResult, setImportResult] = useState<ImportResult | null>(null);
const [importErrorsOpen, setImportErrorsOpen] = useState(false);
const [importDupsOpen, setImportDupsOpen] = useState(false);
// ADIF import confirmation: after the user picks a file, hold the path
// until they confirm the options (skip duplicates etc.).
const [pendingImportPath, setPendingImportPath] = useState<string | null>(null);
const [importSkipDups, setImportSkipDups] = useState(true);
// === Lookup + WB ===
const [lookupResult, setLookupResult] = useState<LookupResult | null>(null);
@@ -673,14 +683,29 @@ export default function App() {
try {
const path = await OpenADIFFile();
if (!path) return;
setImporting(true);
setImportResult(null);
setImportErrorsOpen(false);
const res = await ImportADIF(path);
// Stash the path and open the options dialog. The actual import
// is fired from runImport() when the user clicks "Import".
setPendingImportPath(path);
} catch (e: any) { setError(String(e?.message ?? e)); }
}
async function runImport() {
const path = pendingImportPath;
if (!path || importing) return;
setPendingImportPath(null);
setImporting(true);
setImportResult(null);
setImportErrorsOpen(false);
setImportDupsOpen(false);
try {
const res = await ImportADIF(path, importSkipDups);
setImportResult(res);
await refresh();
} catch (e: any) { setError(String(e?.message ?? e)); }
finally { setImporting(false); }
} catch (e: any) {
setError(String(e?.message ?? e));
} finally {
setImporting(false);
}
}
const menus: Menu[] = useMemo(() => [
@@ -820,37 +845,55 @@ export default function App() {
<div className="w-px h-4 bg-border mx-2" />
<Badge variant="accent" className="font-mono">{band}</Badge>
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 font-mono" variant="outline">{mode}</Badge>
{/* Bearing pill — clickable hook for the future rotator action.
Today it's a passive display; once the rotor backend lands we
wire onClick → rotate to short-path azimuth. */}
{/* Bearing controls — three separate buttons so SP and LP are
both directly clickable, plus an always-visible Stop. The
old Shift/Ctrl shortcuts were not discoverable enough. */}
{(() => {
const p = pathBetween(station.my_grid, grid);
const disabled = !p;
const goto = (az: number) => RotatorGoTo(Math.round(az), -1).catch((err) => setError(String(err?.message ?? err)));
return (
<button
type="button"
disabled={disabled}
onClick={() => { /* TODO: rotor.gotoAzimuth(p.bearingShort) */ }}
title={
p
? `Short-path ${Math.round(p.bearingShort)}° · ${Math.round(p.distanceShort).toLocaleString()} km\nLong-path ${Math.round(p.bearingLong)}° · ${Math.round(p.distanceLong).toLocaleString()} km\n(rotor control coming soon)`
: (station.my_grid ? 'No remote grid' : 'Set your station grid in Preferences')
}
className={cn(
'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-mono font-semibold border transition-colors',
disabled
? 'bg-muted/40 text-muted-foreground/60 border-border/40 cursor-not-allowed'
: 'bg-sky-100 text-sky-800 border-sky-300 hover:bg-sky-200 cursor-pointer',
)}
>
<Compass className="size-3" />
{p ? `${Math.round(p.bearingShort)}°` : '—°'}
{p && (
<span className="text-[9px] text-sky-700/80 font-medium ml-0.5">
LP {Math.round(p.bearingLong)}°
</span>
)}
</button>
<div className="inline-flex items-center rounded-full border border-sky-300 bg-sky-100 overflow-hidden text-[11px] font-mono font-semibold">
<button
type="button"
disabled={disabled}
onClick={() => p && goto(p.bearingShort)}
title={p
? `Rotate short-path · ${Math.round(p.distanceShort).toLocaleString()} km`
: (station.my_grid ? 'No remote grid' : 'Set your station grid in Preferences')}
className={cn(
'inline-flex items-center gap-1 px-2 py-0.5 transition-colors',
disabled
? 'text-muted-foreground/60 cursor-not-allowed'
: 'text-sky-800 hover:bg-sky-200 cursor-pointer',
)}
>
<Compass className="size-3" />
{p ? `${Math.round(p.bearingShort)}°` : '—°'}
</button>
<button
type="button"
disabled={disabled}
onClick={() => p && goto(p.bearingLong)}
title={p ? `Rotate long-path · ${Math.round(p.distanceLong).toLocaleString()} km` : ''}
className={cn(
'px-1.5 py-0.5 border-l border-sky-300 text-[10px] transition-colors',
disabled
? 'text-muted-foreground/60 cursor-not-allowed'
: 'text-sky-800 hover:bg-sky-200 cursor-pointer',
)}
>
LP {p ? `${Math.round(p.bearingLong)}°` : '—'}
</button>
<button
type="button"
onClick={() => RotatorStop().catch((err) => setError(String(err?.message ?? err)))}
title="Stop rotation"
className="px-1.5 py-0.5 border-l border-sky-300 text-rose-700 hover:bg-rose-100 hover:text-rose-800 cursor-pointer transition-colors"
>
<Square className="size-2.5 fill-current" />
</button>
</div>
);
})()}
{catState.enabled && (
@@ -1226,8 +1269,19 @@ export default function App() {
<div className="flex items-center gap-3 flex-wrap">
<strong>Import complete.</strong>
<Badge variant="outline" className="bg-white/60 font-mono text-emerald-700 border-emerald-300">{importResult.imported} imported</Badge>
{importResult.duplicates > 0 && (
<Badge variant="outline" className="bg-white/60 font-mono text-sky-700 border-sky-300">{importResult.duplicates} duplicates</Badge>
)}
<Badge variant="outline" className="bg-white/60 font-mono text-amber-700 border-amber-300">{importResult.skipped} skipped</Badge>
<Badge variant="outline" className="bg-white/60 font-mono">{importResult.total} total</Badge>
{importResult.duplicates > 0 && importResult.duplicate_samples && importResult.duplicate_samples.length > 0 && (
<button className="underline text-xs" onClick={() => setImportDupsOpen((v) => !v)}>
{importDupsOpen ? 'Hide' : 'Show'} duplicates
{importResult.duplicates > importResult.duplicate_samples.length
? ` (first ${importResult.duplicate_samples.length} of ${importResult.duplicates})`
: ''}
</button>
)}
{importResult.errors && importResult.errors.length > 0 && (
<button className="underline text-xs" onClick={() => setImportErrorsOpen((v) => !v)}>
{importErrorsOpen ? 'Hide' : 'Show'} {importResult.errors.length} error{importResult.errors.length > 1 ? 's' : ''}
@@ -1235,6 +1289,11 @@ export default function App() {
)}
<button className="ml-auto" onClick={() => setImportResult(null)}><X className="size-4" /></button>
</div>
{importDupsOpen && importResult.duplicate_samples && (
<ul className="font-mono text-[11px] pl-6 max-h-32 overflow-y-auto list-disc border-t border-current/20 pt-2 mt-1">
{importResult.duplicate_samples.map((d, i) => <li key={i}>{d}</li>)}
</ul>
)}
{importErrorsOpen && importResult.errors && (
<ul className="font-mono text-[11px] pl-6 max-h-32 overflow-y-auto list-disc border-t border-current/20 pt-2 mt-1">
{importResult.errors.map((e, i) => <li key={i}>{e}</li>)}
@@ -1352,6 +1411,37 @@ export default function App() {
onCancel={() => { if (!deletingAll) setShowDeleteAll(false); }}
/>
)}
{pendingImportPath && (
<Dialog open onOpenChange={(o) => { if (!o) setPendingImportPath(null); }}>
<DialogContent className="max-w-lg px-6">
<DialogHeader className="px-2">
<DialogTitle>Import ADIF</DialogTitle>
<DialogDescription className="text-xs break-all">
{pendingImportPath}
</DialogDescription>
</DialogHeader>
<div className="py-2 px-2 space-y-3">
<label className="flex items-start gap-2 text-sm cursor-pointer">
<Checkbox
checked={importSkipDups}
onCheckedChange={(c) => setImportSkipDups(!!c)}
className="mt-0.5"
/>
<span>
Skip duplicates
<span className="block text-xs text-muted-foreground mt-0.5">
Records that match an existing QSO on (callsign + UTC minute + band + mode) are not re-inserted. Uncheck to import everything as-is useful for merging two logs that overlap intentionally.
</span>
</span>
</label>
</div>
<DialogFooter className="px-2">
<Button variant="outline" onClick={() => setPendingImportPath(null)}>Cancel</Button>
<Button onClick={runImport}>Import</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
);
}