feat: Cluster alert implemented
This commit is contained in:
@@ -67,6 +67,7 @@ import { ClusterGrid } from '@/components/ClusterGrid';
|
||||
import { cleanSpotter, inferSpotMode, spotModeCategory, spotStatusKey } from '@/lib/spot';
|
||||
import { WorkedBeforeGrid } from '@/components/WorkedBeforeGrid';
|
||||
import { NetControlPanel } from '@/components/NetControlPanel';
|
||||
import { AlertsModal } from '@/components/AlertsModal';
|
||||
import { BulkEditModal } from '@/components/BulkEditModal';
|
||||
import { ChatPanel, type ChatMsg, type ChatPresence } from '@/components/ChatPopover';
|
||||
import { DetailsPanel, type DetailsState } from '@/components/DetailsPanel';
|
||||
@@ -730,6 +731,8 @@ export default function App() {
|
||||
}
|
||||
const chatShown = chatOpen && chatAvailable;
|
||||
|
||||
const [alertsOpen, setAlertsOpen] = useState(false); // Alert management modal
|
||||
|
||||
// NET Control tab — enabled from Tools (persisted; once on it's a tab like Cluster).
|
||||
const [netEnabled, setNetEnabled] = useState(() => localStorage.getItem('opslog.netEnabled') === '1');
|
||||
useEffect(() => { localStorage.setItem('opslog.netEnabled', netEnabled ? '1' : '0'); }, [netEnabled]);
|
||||
@@ -1100,6 +1103,35 @@ export default function App() {
|
||||
return () => { off(); };
|
||||
}, [showToast]);
|
||||
|
||||
// DX-cluster spot alerts: a matched rule fires here. Play a beep (WebAudio, no
|
||||
// asset needed — CSP-safe) and/or show a toast, per the rule's chosen actions.
|
||||
useEffect(() => {
|
||||
const off = EventsOn('alert:fired', (p: any) => {
|
||||
if (p?.sound) {
|
||||
try {
|
||||
const AC = (window as any).AudioContext || (window as any).webkitAudioContext;
|
||||
const ctx = new AC();
|
||||
const beep = (freq: number, at: number, dur: number) => {
|
||||
const o = ctx.createOscillator(); const g = ctx.createGain();
|
||||
o.type = 'sine'; o.frequency.value = freq;
|
||||
o.connect(g); g.connect(ctx.destination);
|
||||
g.gain.setValueAtTime(0.0001, ctx.currentTime + at);
|
||||
g.gain.exponentialRampToValueAtTime(0.25, ctx.currentTime + at + 0.01);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + at + dur);
|
||||
o.start(ctx.currentTime + at); o.stop(ctx.currentTime + at + dur + 0.02);
|
||||
};
|
||||
beep(880, 0, 0.14); beep(1320, 0.16, 0.18); // two-tone chirp
|
||||
window.setTimeout(() => ctx.close().catch(() => {}), 600);
|
||||
} catch { /* audio blocked — ignore */ }
|
||||
}
|
||||
if (p?.visual) {
|
||||
const call = String(p?.call ?? ''); const band = String(p?.band ?? ''); const rule = String(p?.rule ?? 'alert');
|
||||
showToast(`🔔 ${rule}: ${call}${band ? ` on ${band}` : ''}${p?.country ? ` — ${p.country}` : ''}`);
|
||||
}
|
||||
});
|
||||
return () => { off(); };
|
||||
}, [showToast]);
|
||||
|
||||
// Poll PstRotator for the live antenna heading (status bar). Cheap when the
|
||||
// rotator is disabled (the backend just reads settings and returns).
|
||||
useEffect(() => {
|
||||
@@ -2209,6 +2241,7 @@ export default function App() {
|
||||
{ type: 'item', label: cwEnabled ? '✓ CW decoder (RX audio)' : 'CW decoder (RX audio)', action: 'tools.cwdecoder' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: netEnabled ? '✓ NET Control' : 'NET Control', action: 'tools.net' },
|
||||
{ type: 'item', label: 'Alert management…', action: 'tools.alerts' },
|
||||
{ type: 'separator' },
|
||||
// Maintenance — bumped here while we only have one entry. Will move
|
||||
// to a Tools → Maintenance submenu once Clublog + LoTW refresh land.
|
||||
@@ -2237,6 +2270,7 @@ export default function App() {
|
||||
case 'tools.dvk': setDvkEnabled((v) => !v); break;
|
||||
case 'tools.cwdecoder': toggleCwDecoder(); break;
|
||||
case 'tools.net': setNetEnabled((v) => { const nv = !v; if (nv) setActiveTab('net'); return nv; }); break;
|
||||
case 'tools.alerts': setAlertsOpen(true); break;
|
||||
case 'tools.refreshCty': refreshCtyDat(); break;
|
||||
case 'tools.downloadRefs': downloadRefs(); break;
|
||||
case 'help.about': setShowAbout(true); break;
|
||||
@@ -4101,6 +4135,10 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{alertsOpen && (
|
||||
<AlertsModal onClose={() => setAlertsOpen(false)} bands={bands} modes={modes} countries={countries} />
|
||||
)}
|
||||
|
||||
<AutoEQSL
|
||||
onSent={(call) => showToast(`OpsLog QSL sent to ${call}`)}
|
||||
onError={(msg) => showToast(msg)}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Bell, Plus, Trash2, Volume2, Mail, Eye, X, Search } from 'lucide-react';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
ListAlertRules, SaveAlertRule, DeleteAlertRule, GetAlertEmailTo, SetAlertEmailTo,
|
||||
} from '@/../wailsjs/go/main/App';
|
||||
import { alerts } from '@/../wailsjs/go/models';
|
||||
|
||||
type Rule = alerts.Rule;
|
||||
|
||||
const CONTINENTS = ['AF', 'AN', 'AS', 'EU', 'NA', 'OC', 'SA'];
|
||||
|
||||
function emptyRule(): Rule {
|
||||
return alerts.Rule.createFrom({
|
||||
id: '', name: 'New alert', enabled: true,
|
||||
calls: [], countries: [], continents: [], bands: [], modes: [],
|
||||
spotter_call: '', spotter_continents: [], spotter_countries: [],
|
||||
sound: true, visual: true, email: false, again_after_min: 0, skip_worked: false,
|
||||
});
|
||||
}
|
||||
|
||||
// MultiCheck — a scrollable checkbox list for a set of options with an optional
|
||||
// search box (used for the long country list). Empty selection = ALL.
|
||||
function MultiCheck({ options, selected, onToggle, searchable, height = 'h-52' }: {
|
||||
options: string[]; selected: string[]; onToggle: (v: string) => void; searchable?: boolean; height?: string;
|
||||
}) {
|
||||
const [q, setQ] = useState('');
|
||||
const shown = useMemo(
|
||||
() => (q ? options.filter((o) => o.toLowerCase().includes(q.toLowerCase())) : options),
|
||||
[options, q],
|
||||
);
|
||||
const sel = new Set((selected ?? []).map((s) => s.toLowerCase()));
|
||||
return (
|
||||
<div className="rounded-md border border-border">
|
||||
{searchable && (
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border/60">
|
||||
<Search className="size-3 text-muted-foreground" />
|
||||
<input className="flex-1 bg-transparent text-xs outline-none" placeholder="Filter…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('overflow-y-auto p-1', height)}>
|
||||
{shown.map((o) => (
|
||||
<label key={o} className="flex items-center gap-2 text-xs px-1.5 py-0.5 rounded hover:bg-accent/40 cursor-pointer">
|
||||
<Checkbox checked={sel.has(o.toLowerCase())} onCheckedChange={() => onToggle(o)} />
|
||||
<span className="truncate">{o}</span>
|
||||
</label>
|
||||
))}
|
||||
{shown.length === 0 && <div className="text-[11px] text-muted-foreground px-2 py-3 text-center">no match</div>}
|
||||
</div>
|
||||
<div className="px-2 py-1 border-t border-border/60 text-[10px] text-muted-foreground">
|
||||
{(selected?.length ?? 0) === 0 ? 'none selected = ALL' : `${selected!.length} selected`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertsModal({ onClose, bands, modes, countries }: {
|
||||
onClose: () => void; bands: string[]; modes: string[]; countries: string[];
|
||||
}) {
|
||||
const [rules, setRules] = useState<Rule[]>([]);
|
||||
const [draft, setDraft] = useState<Rule | null>(null);
|
||||
const [tab, setTab] = useState('def'); // active editor tab (reset to Definition on new/select)
|
||||
const [emailTo, setEmailTo] = useState('');
|
||||
const [err, setErr] = useState('');
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try { setRules(((await ListAlertRules()) ?? []) as Rule[]); }
|
||||
catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}, []);
|
||||
useEffect(() => { refresh(); GetAlertEmailTo().then((v) => setEmailTo(v || '')).catch(() => {}); }, [refresh]);
|
||||
|
||||
const patch = (p: Partial<Rule>) => setDraft((d) => (d ? alerts.Rule.createFrom({ ...d, ...p }) : d));
|
||||
const toggleIn = (key: keyof Rule, v: string) => setDraft((d) => {
|
||||
if (!d) return d;
|
||||
const cur = ((d as any)[key] as string[]) ?? [];
|
||||
const has = cur.some((x) => x.toLowerCase() === v.toLowerCase());
|
||||
const next = has ? cur.filter((x) => x.toLowerCase() !== v.toLowerCase()) : [...cur, v];
|
||||
return alerts.Rule.createFrom({ ...d, [key]: next });
|
||||
});
|
||||
|
||||
async function save() {
|
||||
if (!draft) return;
|
||||
if (!draft.name.trim()) { setErr('Give the rule a name'); return; }
|
||||
try { const saved = await SaveAlertRule(draft); await refresh(); setDraft(saved as Rule); setErr(''); }
|
||||
catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
async function del() {
|
||||
if (!draft) return;
|
||||
if (!draft.id) { setDraft(null); return; }
|
||||
if (!window.confirm(`Delete alert "${draft.name}"?`)) return;
|
||||
try { await DeleteAlertRule(draft.id); setDraft(null); await refresh(); }
|
||||
catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2"><Bell className="size-4 text-primary" /> Alert management</DialogTitle>
|
||||
<DialogDescription>Alert when a spot matches a rule. Empty filters = ANY; the filters you set are ANDed (e.g. France + 20m = French stations on 20m).</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-3 min-h-0 px-5" style={{ height: '60vh' }}>
|
||||
{/* Rule list */}
|
||||
<div className="w-56 shrink-0 flex flex-col border border-border rounded-md">
|
||||
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border/60">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground flex-1">Rules</span>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-1.5" onClick={() => { setDraft(emptyRule()); setTab('def'); }}><Plus className="size-3.5" /></Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-1">
|
||||
{rules.length === 0 && <div className="text-[11px] text-muted-foreground px-2 py-4 text-center">No rules yet — click +</div>}
|
||||
{rules.map((r) => (
|
||||
<button key={r.id} onClick={() => { setDraft(alerts.Rule.createFrom(r)); setTab('def'); }}
|
||||
className={cn('w-full text-left px-2 py-1.5 rounded text-xs flex items-center gap-1.5',
|
||||
draft?.id === r.id ? 'bg-accent text-accent-foreground font-semibold' : 'hover:bg-muted/60')}>
|
||||
<span className={cn('size-1.5 rounded-full shrink-0', r.enabled ? 'bg-emerald-500' : 'bg-muted-foreground/40')} />
|
||||
<span className="truncate flex-1">{r.name}</span>
|
||||
{r.sound && <Volume2 className="size-3 text-muted-foreground shrink-0" />}
|
||||
{r.email && <Mail className="size-3 text-muted-foreground shrink-0" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-2 py-1.5 border-t border-border/60">
|
||||
<Label className="text-[10px] text-muted-foreground">Alert e-mail to</Label>
|
||||
<Input className="h-7 text-xs mt-0.5" placeholder="[email protected]" value={emailTo}
|
||||
onChange={(e) => setEmailTo(e.target.value)}
|
||||
onBlur={() => SetAlertEmailTo(emailTo).catch(() => {})} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="flex-1 min-w-0 border border-border rounded-md flex flex-col">
|
||||
{!draft ? (
|
||||
<div className="flex-1 flex items-center justify-center text-sm text-muted-foreground">Select or create a rule.</div>
|
||||
) : (
|
||||
<Tabs value={tab} onValueChange={setTab} className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex items-center gap-2 px-2 pt-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="def">Definition</TabsTrigger>
|
||||
<TabsTrigger value="call">Call / DXCC</TabsTrigger>
|
||||
<TabsTrigger value="bm">Band / Mode</TabsTrigger>
|
||||
<TabsTrigger value="orig">Origin</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-y-auto p-3">
|
||||
<TabsContent value="def" className="mt-0 space-y-3">
|
||||
<div>
|
||||
<Label className="text-xs">Rule name</Label>
|
||||
<Input value={draft.name} onChange={(e) => patch({ name: e.target.value })} />
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={draft.enabled} onCheckedChange={(c) => patch({ enabled: !!c })} /> Alert enabled
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs w-40 shrink-0">Alert again after (min)</Label>
|
||||
<Input type="number" className="h-8 w-24" value={draft.again_after_min}
|
||||
onChange={(e) => patch({ again_after_min: parseInt(e.target.value) || 0 })} />
|
||||
<span className="text-[11px] text-muted-foreground">0 = once/session · -1 = always</span>
|
||||
</div>
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<Label className="text-xs font-semibold">Actions</Label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-1.5 text-sm cursor-pointer"><Checkbox checked={draft.visual} onCheckedChange={(c) => patch({ visual: !!c })} /><Eye className="size-3.5" /> Visual</label>
|
||||
<label className="flex items-center gap-1.5 text-sm cursor-pointer"><Checkbox checked={draft.sound} onCheckedChange={(c) => patch({ sound: !!c })} /><Volume2 className="size-3.5" /> Sound</label>
|
||||
<label className="flex items-center gap-1.5 text-sm cursor-pointer"><Checkbox checked={draft.email} onCheckedChange={(c) => patch({ email: !!c })} /><Mail className="size-3.5" /> E-mail</label>
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer border-t border-border/60 pt-3">
|
||||
<Checkbox checked={draft.skip_worked} onCheckedChange={(c) => patch({ skip_worked: !!c })} /> Skip calls already worked (same band + mode)
|
||||
</label>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="call" className="mt-0 grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Callsigns (one per line, wildcards: IW3*, */P)</Label>
|
||||
<textarea className="w-full h-52 rounded-md border border-border bg-background p-2 text-xs font-mono resize-none"
|
||||
placeholder={'DL1ABC\nIW3*\n*/P'}
|
||||
value={(draft.calls ?? []).join('\n')}
|
||||
onChange={(e) => patch({ calls: e.target.value.split('\n').map((x) => x.trim()).filter(Boolean) })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Countries (DXCC)</Label>
|
||||
<MultiCheck options={countries} selected={draft.countries ?? []} onToggle={(v) => toggleIn('countries', v)} searchable />
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label className="text-xs">Continents</Label>
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{CONTINENTS.map((c) => (
|
||||
<label key={c} className="flex items-center gap-1.5 text-xs cursor-pointer">
|
||||
<Checkbox checked={(draft.continents ?? []).includes(c)} onCheckedChange={() => toggleIn('continents', c)} /> {c}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="bm" className="mt-0 grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Bands</Label>
|
||||
<MultiCheck options={bands} selected={draft.bands ?? []} onToggle={(v) => toggleIn('bands', v)} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Modes</Label>
|
||||
<MultiCheck options={modes} selected={draft.modes ?? []} onToggle={(v) => toggleIn('modes', v)} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="orig" className="mt-0 grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Spotter callsign (wildcard)</Label>
|
||||
<Input className="font-mono" placeholder="e.g. F* or DL1ABC" value={draft.spotter_call ?? ''}
|
||||
onChange={(e) => patch({ spotter_call: e.target.value })} />
|
||||
<Label className="text-xs mt-2 block">Spotter continents</Label>
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{CONTINENTS.map((c) => (
|
||||
<label key={c} className="flex items-center gap-1.5 text-xs cursor-pointer">
|
||||
<Checkbox checked={(draft.spotter_continents ?? []).includes(c)} onCheckedChange={() => toggleIn('spotter_continents', c)} /> {c}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Spotter countries</Label>
|
||||
<MultiCheck options={countries} selected={draft.spotter_countries ?? []} onToggle={(v) => toggleIn('spotter_countries', v)} searchable />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
{/* Editor actions */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-t border-border/60">
|
||||
{err && <span className="text-[11px] text-rose-600 flex-1 truncate">{err}</span>}
|
||||
<div className="flex-1" />
|
||||
<Button variant="ghost" size="sm" className="text-rose-700" onClick={del}><Trash2 className="size-3.5" /> Delete</Button>
|
||||
<Button size="sm" onClick={save}>Save rule</Button>
|
||||
</div>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end px-5 pb-4">
|
||||
<Button variant="outline" size="sm" onClick={onClose}><X className="size-3.5" /> Close</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Vendored
+11
@@ -12,6 +12,7 @@ import {cluster} from '../models';
|
||||
import {extsvc} from '../models';
|
||||
import {powergenius} from '../models';
|
||||
import {winkeyer} from '../models';
|
||||
import {alerts} from '../models';
|
||||
import {audio} from '../models';
|
||||
import {operating} from '../models';
|
||||
import {udp} from '../models';
|
||||
@@ -88,6 +89,8 @@ export function DXCCForCountry(arg1:string):Promise<number>;
|
||||
|
||||
export function DXCCName(arg1:number):Promise<string>;
|
||||
|
||||
export function DeleteAlertRule(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteAllQSO():Promise<number>;
|
||||
|
||||
export function DeleteAwardReference(arg1:string,arg2:string):Promise<void>;
|
||||
@@ -212,6 +215,8 @@ export function FlexTune(arg1:boolean):Promise<void>;
|
||||
|
||||
export function GetActiveProfile():Promise<profile.Profile>;
|
||||
|
||||
export function GetAlertEmailTo():Promise<string>;
|
||||
|
||||
export function GetAntGeniusSettings():Promise<main.AntGeniusSettings>;
|
||||
|
||||
export function GetAntGeniusStatus():Promise<antgenius.Status>;
|
||||
@@ -354,6 +359,8 @@ export function LaunchAutostartProgram(arg1:string):Promise<main.AutostartLaunch
|
||||
|
||||
export function LaunchAutostartPrograms():Promise<Array<main.AutostartLaunchResult>>;
|
||||
|
||||
export function ListAlertRules():Promise<Array<alerts.Rule>>;
|
||||
|
||||
export function ListAudioInputDevices():Promise<Array<audio.Device>>;
|
||||
|
||||
export function ListAudioOutputDevices():Promise<Array<audio.Device>>;
|
||||
@@ -512,6 +519,8 @@ export function RunBackupNow():Promise<string>;
|
||||
|
||||
export function SaveADIFFile():Promise<string>;
|
||||
|
||||
export function SaveAlertRule(arg1:alerts.Rule):Promise<alerts.Rule>;
|
||||
|
||||
export function SaveAntGeniusSettings(arg1:main.AntGeniusSettings):Promise<void>;
|
||||
|
||||
export function SaveAudioSettings(arg1:main.AudioSettings):Promise<void>;
|
||||
@@ -574,6 +583,8 @@ export function SendEQSL(arg1:number,arg2:number,arg3:string):Promise<void>;
|
||||
|
||||
export function SendQSORecordingEmail(arg1:number):Promise<void>;
|
||||
|
||||
export function SetAlertEmailTo(arg1:string):Promise<void>;
|
||||
|
||||
export function SetCATFrequency(arg1:number):Promise<void>;
|
||||
|
||||
export function SetCATMode(arg1:string):Promise<void>;
|
||||
|
||||
@@ -142,6 +142,10 @@ export function DXCCName(arg1) {
|
||||
return window['go']['main']['App']['DXCCName'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteAlertRule(arg1) {
|
||||
return window['go']['main']['App']['DeleteAlertRule'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteAllQSO() {
|
||||
return window['go']['main']['App']['DeleteAllQSO']();
|
||||
}
|
||||
@@ -390,6 +394,10 @@ export function GetActiveProfile() {
|
||||
return window['go']['main']['App']['GetActiveProfile']();
|
||||
}
|
||||
|
||||
export function GetAlertEmailTo() {
|
||||
return window['go']['main']['App']['GetAlertEmailTo']();
|
||||
}
|
||||
|
||||
export function GetAntGeniusSettings() {
|
||||
return window['go']['main']['App']['GetAntGeniusSettings']();
|
||||
}
|
||||
@@ -674,6 +682,10 @@ export function LaunchAutostartPrograms() {
|
||||
return window['go']['main']['App']['LaunchAutostartPrograms']();
|
||||
}
|
||||
|
||||
export function ListAlertRules() {
|
||||
return window['go']['main']['App']['ListAlertRules']();
|
||||
}
|
||||
|
||||
export function ListAudioInputDevices() {
|
||||
return window['go']['main']['App']['ListAudioInputDevices']();
|
||||
}
|
||||
@@ -990,6 +1002,10 @@ export function SaveADIFFile() {
|
||||
return window['go']['main']['App']['SaveADIFFile']();
|
||||
}
|
||||
|
||||
export function SaveAlertRule(arg1) {
|
||||
return window['go']['main']['App']['SaveAlertRule'](arg1);
|
||||
}
|
||||
|
||||
export function SaveAntGeniusSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveAntGeniusSettings'](arg1);
|
||||
}
|
||||
@@ -1114,6 +1130,10 @@ export function SendQSORecordingEmail(arg1) {
|
||||
return window['go']['main']['App']['SendQSORecordingEmail'](arg1);
|
||||
}
|
||||
|
||||
export function SetAlertEmailTo(arg1) {
|
||||
return window['go']['main']['App']['SetAlertEmailTo'](arg1);
|
||||
}
|
||||
|
||||
export function SetCATFrequency(arg1) {
|
||||
return window['go']['main']['App']['SetCATFrequency'](arg1);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,53 @@ export namespace adif {
|
||||
|
||||
}
|
||||
|
||||
export namespace alerts {
|
||||
|
||||
export class Rule {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
calls?: string[];
|
||||
countries?: string[];
|
||||
continents?: string[];
|
||||
bands?: string[];
|
||||
modes?: string[];
|
||||
spotter_call?: string;
|
||||
spotter_continents?: string[];
|
||||
spotter_countries?: string[];
|
||||
sound: boolean;
|
||||
visual: boolean;
|
||||
email: boolean;
|
||||
again_after_min: number;
|
||||
skip_worked: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Rule(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.name = source["name"];
|
||||
this.enabled = source["enabled"];
|
||||
this.calls = source["calls"];
|
||||
this.countries = source["countries"];
|
||||
this.continents = source["continents"];
|
||||
this.bands = source["bands"];
|
||||
this.modes = source["modes"];
|
||||
this.spotter_call = source["spotter_call"];
|
||||
this.spotter_continents = source["spotter_continents"];
|
||||
this.spotter_countries = source["spotter_countries"];
|
||||
this.sound = source["sound"];
|
||||
this.visual = source["visual"];
|
||||
this.email = source["email"];
|
||||
this.again_after_min = source["again_after_min"];
|
||||
this.skip_worked = source["skip_worked"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace antgenius {
|
||||
|
||||
export class Antenna {
|
||||
|
||||
Reference in New Issue
Block a user