This commit is contained in:
2026-06-13 21:56:38 +02:00
parent 81e505e040
commit 08162fa126
9 changed files with 257 additions and 15 deletions
+5 -2
View File
@@ -102,10 +102,13 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
}, [wb]);
// "Newness" of the current band+mode entry, for the award/DX-chase badges.
// Derived straight from the entity's real band_status (all bands it was
// worked on — not just the operator's configured column list).
const curClass = CLASSES.find((c) => classMatchesMode(c, currentMode));
const statusKeys = useMemo(() => Array.from(statusMap.keys()), [statusMap]);
const slotStatus = curClass ? statusMap.get(`${currentBand}|${curClass}`) : undefined;
const bandWorked = CLASSES.some((c) => statusMap.get(`${currentBand}|${c}`));
const modeWorked = !!curClass && cols.some((b) => statusMap.get(`${b.tag}|${curClass}`));
const bandWorked = statusKeys.some((k) => k.split('|')[0] === currentBand);
const modeWorked = !!curClass && statusKeys.some((k) => k.split('|')[1] === curClass);
const newBand = hasDxcc && !newOne && !bandWorked;
const newMode = hasDxcc && !newOne && !!curClass && !modeWorked;
const newBandMode = hasDxcc && !newOne && !!curClass && !slotStatus;
+52 -13
View File
@@ -25,6 +25,7 @@ import {
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, QuitApp, CreateDatabase,
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection,
GetDataDir,
GetQSLDefaults, SaveQSLDefaults,
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload,
@@ -538,6 +539,10 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
const [dbSettings, setDbSettings] = useState<{ path: string; default_path: string; is_custom: boolean }>({ path: '', default_path: '', is_custom: false });
const [dbMsg, setDbMsg] = useState('');
type MySQLCfg = { enabled: boolean; host: string; port: number; user: string; password: string; database: string };
const [mysqlCfg, setMysqlCfg] = useState<MySQLCfg>({ enabled: false, host: '', port: 3306, user: '', password: '', database: '' });
const setMysqlField = (patch: Partial<MySQLCfg>) => setMysqlCfg((s) => ({ ...s, ...patch }));
const [mysqlMsg, setMysqlMsg] = useState('');
const [dataDir, setDataDir] = useState('');
const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
@@ -627,6 +632,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
setQslDefaults(qd as any);
setExtSvc(es as any);
try { setDbSettings(await GetDatabaseSettings() as any); } catch {}
try { setMysqlCfg(await GetMySQLSettings() as any); } catch {}
try { setDataDir(await GetDataDir()); } catch {}
try {
const locs: any = await ListTQSLStationLocations();
@@ -1461,7 +1467,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
return (
<>
<SectionHeader
title="CAT interface (OmniRig)"
title="CAT interface"
hint="Reads the rig's current frequency / band / mode and pushes them into the entry strip in real time. Requires OmniRig (free) installed and configured separately for your rig — OpsLog just talks to it."
/>
<div className="space-y-4 max-w-lg">
@@ -1646,7 +1652,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
return (
<>
<SectionHeader
title="Rotator (PstRotator)"
title="Rotator"
hint="OpsLog sends UDP commands to PstRotator. Enable PstRotator's UDP listener (Setup Communication UDP) before testing."
/>
<div className="space-y-4 max-w-xl">
@@ -2688,7 +2694,6 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
<>
<SectionHeader
title="Database"
hint="Your whole log (QSOs, settings, lookup cache) lives in one SQLite file. Keep it wherever you like — another drive or a synced folder (Seafile, Dropbox…) — and back it up automatically."
/>
<div className="space-y-4 max-w-2xl">
<div className="space-y-1">
@@ -2709,13 +2714,6 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
{dbSettings.is_custom && <Button variant="ghost" size="sm" onClick={resetDefault}>Reset to default</Button>}
</div>
<div className="text-[11px] text-muted-foreground bg-amber-50 border border-amber-200 rounded-md p-2.5 leading-relaxed">
<strong>New</strong> creates a fresh empty logbook and switches to it (handy for a separate contest/SOTA log).{' '}
<strong>Open existing</strong> points OpsLog at a file you already have.{' '}
<strong>Save a copy</strong> clones the current database elsewhere and switches to it.{' '}
Any database change takes effect on the next launch.
</div>
{dbMsg && (
<div className="text-xs bg-emerald-50 border border-emerald-300 text-emerald-800 rounded-md px-3 py-2 flex items-center justify-between gap-3 whitespace-pre-line">
<span>{dbMsg}</span>
@@ -2724,13 +2722,54 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
)}
</div>
{/* Shared MySQL database (multi-operator) */}
<div className="border-t border-border/60 mt-6 pt-5 space-y-3 max-w-2xl">
<div>
<div className="text-sm font-medium">Shared database (multi-operator)</div>
</div>
<div className="grid grid-cols-[130px_1fr] gap-2 items-center">
<Label className="text-sm">Backend</Label>
<Select value={mysqlCfg.enabled ? 'mysql' : 'sqlite'} onValueChange={(v) => setMysqlField({ enabled: v === 'mysql' })}>
<SelectTrigger className="h-8 w-72"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="sqlite">SQLite — local file (solo)</SelectItem>
<SelectItem value="mysql">MySQL — shared server (multi-operator)</SelectItem>
</SelectContent>
</Select>
</div>
{mysqlCfg.enabled && (
<>
<div className="grid grid-cols-[130px_1fr] gap-2 items-center">
<Label className="text-sm">Host</Label>
<Input className="h-8" placeholder="192.168.1.10 or db.example.com" value={mysqlCfg.host} onChange={(e) => setMysqlField({ host: e.target.value })} />
<Label className="text-sm">Port</Label>
<Input type="number" className="h-8 w-28 font-mono" value={mysqlCfg.port} onChange={(e) => setMysqlField({ port: parseInt(e.target.value, 10) || 0 })} />
<Label className="text-sm">Database</Label>
<Input className="h-8" placeholder="opslog" value={mysqlCfg.database} onChange={(e) => setMysqlField({ database: e.target.value })} />
<Label className="text-sm">User</Label>
<Input className="h-8" value={mysqlCfg.user} onChange={(e) => setMysqlField({ user: e.target.value })} />
<Label className="text-sm">Password</Label>
<Input type="password" className="h-8" value={mysqlCfg.password} onChange={(e) => setMysqlField({ password: e.target.value })} />
</div>
<div className="flex items-center gap-3">
<Button variant="outline" size="sm" className="h-8"
onClick={() => { setMysqlMsg('Testing'); TestMySQLConnection(mysqlCfg as any).then(() => setMysqlMsg('Connected database ready ')).catch((e: any) => setMysqlMsg('Failed: ' + String(e?.message ?? e))); }}>
Test &amp; create database
</Button>
<Button size="sm" className="h-8"
onClick={() => { SaveMySQLSettings(mysqlCfg as any).then(() => setMysqlMsg('Saved.')).catch((e: any) => setErr(String(e?.message ?? e))); }}>
Save
</Button>
<span className="text-[11px] text-muted-foreground">{mysqlMsg}</span>
</div>
</>
)}
</div>
{/* Data location */}
<div className="border-t border-border/60 mt-6 pt-5 space-y-3 max-w-2xl">
<div>
<div className="text-sm font-medium">Data location</div>
<div className="text-[11px] text-muted-foreground mt-0.5">
OpsLog is fully portable — all data lives next to the executable so you can run it from a USB stick or reinstall Windows without losing anything.
</div>
</div>
<div className="space-y-1">
<Label>Current data directory</Label>
+6
View File
@@ -153,6 +153,8 @@ export function GetLogFilePath():Promise<string>;
export function GetLookupSettings():Promise<main.LookupSettings>;
export function GetMySQLSettings():Promise<main.MySQLSettings>;
export function GetPOTAToken():Promise<string>;
export function GetQSLDefaults():Promise<main.QSLDefaults>;
@@ -323,6 +325,8 @@ export function SaveListsSettings(arg1:main.ListsSettings):Promise<void>;
export function SaveLookupSettings(arg1:main.LookupSettings):Promise<void>;
export function SaveMySQLSettings(arg1:main.MySQLSettings):Promise<void>;
export function SaveOperatingAntenna(arg1:operating.Antenna):Promise<operating.Antenna>;
export function SaveOperatingStation(arg1:operating.Station):Promise<operating.Station>;
@@ -383,6 +387,8 @@ export function TestLoTWUpload():Promise<string>;
export function TestLookupProvider(arg1:string,arg2:string,arg3:string,arg4:string):Promise<lookup.Result>;
export function TestMySQLConnection(arg1:main.MySQLSettings):Promise<void>;
export function TestPTT(arg1:main.AudioSettings):Promise<void>;
export function TestQRZUpload():Promise<string>;
+12
View File
@@ -278,6 +278,10 @@ export function GetLookupSettings() {
return window['go']['main']['App']['GetLookupSettings']();
}
export function GetMySQLSettings() {
return window['go']['main']['App']['GetMySQLSettings']();
}
export function GetPOTAToken() {
return window['go']['main']['App']['GetPOTAToken']();
}
@@ -618,6 +622,10 @@ export function SaveLookupSettings(arg1) {
return window['go']['main']['App']['SaveLookupSettings'](arg1);
}
export function SaveMySQLSettings(arg1) {
return window['go']['main']['App']['SaveMySQLSettings'](arg1);
}
export function SaveOperatingAntenna(arg1) {
return window['go']['main']['App']['SaveOperatingAntenna'](arg1);
}
@@ -738,6 +746,10 @@ export function TestLookupProvider(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['TestLookupProvider'](arg1, arg2, arg3, arg4);
}
export function TestMySQLConnection(arg1) {
return window['go']['main']['App']['TestMySQLConnection'](arg1);
}
export function TestPTT(arg1) {
return window['go']['main']['App']['TestPTT'](arg1);
}
+22
View File
@@ -1011,6 +1011,28 @@ export namespace main {
}
}
export class MySQLSettings {
enabled: boolean;
host: string;
port: number;
user: string;
password: string;
database: string;
static createFrom(source: any = {}) {
return new MySQLSettings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.enabled = source["enabled"];
this.host = source["host"];
this.port = source["port"];
this.user = source["user"];
this.password = source["password"];
this.database = source["database"];
}
}
export class POTAUnmatched {
activator: string;
date: string;