feat: status bar added
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
ConnectClusterServer, DisconnectClusterServer,
|
||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
|
||||
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
|
||||
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, QuitApp,
|
||||
GetQSLDefaults, SaveQSLDefaults,
|
||||
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload,
|
||||
TestLoTWUpload, ListTQSLStationLocations,
|
||||
@@ -141,6 +142,7 @@ type SectionId =
|
||||
| 'lists-modes'
|
||||
| 'cluster'
|
||||
| 'backup'
|
||||
| 'database'
|
||||
| 'awards'
|
||||
| 'cat'
|
||||
| 'rotator'
|
||||
@@ -171,6 +173,7 @@ const TREE: TreeNode[] = [
|
||||
{ kind: 'item', label: 'DX Cluster', id: 'cluster' },
|
||||
{ kind: 'item', label: 'UDP integrations (WSJT-X, JTDX, MSHV…)', id: 'udp' },
|
||||
{ kind: 'item', label: 'Database backup', id: 'backup' },
|
||||
{ kind: 'item', label: 'Database location', id: 'database' },
|
||||
{ kind: 'item', label: 'Awards', id: 'awards', disabled: true },
|
||||
],
|
||||
},
|
||||
@@ -196,6 +199,7 @@ const SECTION_LABELS: Partial<Record<SectionId, string>> = {
|
||||
'lists-modes': 'Modes & default RST',
|
||||
cluster: 'DX Cluster',
|
||||
backup: 'Database backup',
|
||||
database: 'Database location',
|
||||
udp: 'UDP integrations',
|
||||
awards: 'Awards',
|
||||
cat: 'CAT interface',
|
||||
@@ -319,7 +323,9 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
const [activeProfile, setActiveProfile] = useState<Profile | null>(null);
|
||||
const updateActive = (patch: Partial<Profile>) =>
|
||||
setActiveProfile((p) => (p ? { ...p, ...patch } : p));
|
||||
const [lists, setLists] = useState<ListsSettings>({ bands: [], modes: [] });
|
||||
const [lists, setLists] = useState<ListsSettings>({ bands: [], modes: [], rst_phone: [], rst_cw: [], rst_digital: [] });
|
||||
// RST report lists edited as free text (one/space-separated values).
|
||||
const [rstText, setRstText] = useState({ phone: '', cw: '', digital: '' });
|
||||
// Custom band drafts (catalog covers ADIF spec but the user may have
|
||||
// exotic or experimental bands not listed).
|
||||
const [bandDraft, setBandDraft] = useState('');
|
||||
@@ -384,6 +390,9 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
const [backupRunning, setBackupRunning] = useState(false);
|
||||
const [backupResult, setBackupResult] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
|
||||
const [dbSettings, setDbSettings] = useState<{ path: string; default_path: string; is_custom: boolean }>({ path: '', default_path: '', is_custom: false });
|
||||
const [dbMsg, setDbMsg] = useState('');
|
||||
|
||||
const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
|
||||
const [clusterAutoConnect, setClusterAutoConnectState] = useState(false);
|
||||
const [clusterStatuses, setClusterStatuses] = useState<ClusterServerStatus[]>([]);
|
||||
@@ -457,6 +466,11 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
setLookup(l);
|
||||
setActiveProfile(ap as Profile);
|
||||
setLists(ls);
|
||||
setRstText({
|
||||
phone: ((ls as any).rst_phone ?? []).join(' '),
|
||||
cw: ((ls as any).rst_cw ?? []).join(' '),
|
||||
digital: ((ls as any).rst_digital ?? []).join(' '),
|
||||
});
|
||||
await reloadProfiles();
|
||||
await reloadClusterServers();
|
||||
setCatCfg(c);
|
||||
@@ -464,6 +478,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
setBackupCfg(b as any);
|
||||
setQslDefaults(qd as any);
|
||||
setExtSvc(es as any);
|
||||
try { setDbSettings(await GetDatabaseSettings() as any); } catch {}
|
||||
try {
|
||||
const locs: any = await ListTQSLStationLocations();
|
||||
setStationLocations((locs ?? []).map((l: any) => l.name).filter(Boolean));
|
||||
@@ -600,7 +615,13 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
default_rst_rcvd: (m.default_rst_rcvd ?? '').trim(),
|
||||
}))
|
||||
.filter((m) => m.name !== '');
|
||||
await SaveListsSettings({ bands, modes } as any);
|
||||
const splitList = (s: string) => s.split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
|
||||
await SaveListsSettings({
|
||||
bands, modes,
|
||||
rst_phone: splitList(rstText.phone),
|
||||
rst_cw: splitList(rstText.cw),
|
||||
rst_digital: splitList(rstText.digital),
|
||||
} as any);
|
||||
|
||||
if (activeProfile) {
|
||||
await SaveProfile({
|
||||
@@ -1233,6 +1254,28 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RST report lists — the dropdown choices in the entry form. */}
|
||||
<div className="mt-6 max-w-4xl">
|
||||
<div className="text-sm font-semibold mb-1">RST report lists</div>
|
||||
<div className="text-[11px] text-muted-foreground mb-2">
|
||||
The choices offered in the entry form's RST dropdowns, per mode family. One value per line (or space-separated). The first one is the top of the list.
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Phone (SSB/AM/FM)</Label>
|
||||
<Textarea rows={8} className="font-mono text-xs" value={rstText.phone} onChange={(e) => setRstText((s) => ({ ...s, phone: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">CW / RTTY / PSK</Label>
|
||||
<Textarea rows={8} className="font-mono text-xs" value={rstText.cw} onChange={(e) => setRstText((s) => ({ ...s, cw: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Digital (FT8/FT4/JT…) — dB</Label>
|
||||
<Textarea rows={8} className="font-mono text-xs" value={rstText.digital} onChange={(e) => setRstText((s) => ({ ...s, digital: e.target.value }))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2121,6 +2164,74 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function DatabasePanel() {
|
||||
async function refreshDb() { try { setDbSettings(await GetDatabaseSettings() as any); } catch {} }
|
||||
async function openExisting() {
|
||||
try {
|
||||
const p = await PickOpenDatabase();
|
||||
if (!p) return;
|
||||
await OpenDatabase(p);
|
||||
await refreshDb();
|
||||
setDbMsg(`Database set to:\n${p}\nRestart OpsLog to apply.`);
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
async function saveCopy() {
|
||||
try {
|
||||
const p = await PickSaveDatabase();
|
||||
if (!p) return;
|
||||
await MoveDatabase(p);
|
||||
await refreshDb();
|
||||
setDbMsg(`A copy was saved to:\n${p}\nand selected. Restart OpsLog to apply.`);
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
async function resetDefault() {
|
||||
try {
|
||||
await ResetDatabaseToDefault();
|
||||
await refreshDb();
|
||||
setDbMsg('Database reset to the default location. Restart OpsLog to apply.');
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Database location"
|
||||
hint="Keep your log database wherever you like — another drive or a synced folder (Seafile, Dropbox…) — so it survives a Windows reinstall. Everything (QSOs, settings, lookup cache) lives in this one file."
|
||||
/>
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="space-y-1">
|
||||
<Label>Current database</Label>
|
||||
<div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">
|
||||
{dbSettings.path || '—'}
|
||||
{dbSettings.is_custom
|
||||
? <span className="ml-2 text-[10px] text-emerald-700">(custom location)</span>
|
||||
: <span className="ml-2 text-[10px] text-muted-foreground">(default)</span>}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">Default: <span className="font-mono">{dbSettings.default_path}</span></div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={openExisting}>Open existing database…</Button>
|
||||
<Button variant="outline" size="sm" onClick={saveCopy}>Save a copy & switch to it…</Button>
|
||||
{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>Open existing</strong> points OpsLog at a database file you already have (e.g. after reinstalling Windows).{' '}
|
||||
<strong>Save a copy</strong> writes the current database to a new place and switches to it.{' '}
|
||||
A 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>
|
||||
<Button size="sm" variant="destructive" className="shrink-0" onClick={() => QuitApp()}>Quit now</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Map sections to their content + icon (for placeholder).
|
||||
const PANELS: Record<SectionId, () => JSX.Element> = {
|
||||
station: StationPanel,
|
||||
@@ -2134,6 +2245,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
cluster: ClusterPanel,
|
||||
udp: UDPIntegrationsPanelWrapper,
|
||||
backup: BackupPanel,
|
||||
database: DatabasePanel,
|
||||
awards: () => <ComingSoon id="awards" icon={Award} />,
|
||||
cat: CATPanel,
|
||||
rotator: RotatorPanel,
|
||||
|
||||
Reference in New Issue
Block a user