chore: release v0.26.0
This commit is contained in:
@@ -55,7 +55,7 @@ import {
|
||||
GetFolderSync, SaveFolderSync, PickFolderSyncFolder, GetFolderSyncStatus, SyncFolderNow,
|
||||
GetRelayAuto, SaveRelayAuto, GetStationDevices,
|
||||
GetAwardDefs, GetTrackedAwards, SaveTrackedAwards,
|
||||
GetBandOpenSettings, SaveBandOpenSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes,
|
||||
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import type { profile as profileModels } from '../../wailsjs/go/models';
|
||||
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import { loadAutoCall, autoCallKey, type AutoCallSettings, type AutoCallCriteria } from '@/lib/autocall';
|
||||
import { getDateFormat, setDateFormat, type DateFormat } from '@/lib/dateFormat';
|
||||
import { useI18n, FlagGB, FlagFR, type Lang } from '@/lib/i18n';
|
||||
import { useTheme, CONCRETE_THEMES, type ThemeChoice } from '@/lib/theme';
|
||||
@@ -202,6 +203,7 @@ type SectionId =
|
||||
| 'uscounties'
|
||||
| 'awards'
|
||||
| 'cat'
|
||||
| 'ftx'
|
||||
| 'rotator'
|
||||
| 'winkeyer'
|
||||
| 'antenna'
|
||||
@@ -306,6 +308,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
|
||||
{ kind: 'item', label: t('sec.modes'), id: 'lists-modes' },
|
||||
]},
|
||||
{ kind: 'item', label: t('sec.cluster'), id: 'cluster' },
|
||||
{ kind: 'item', label: t('sec.ftx'), id: 'ftx' },
|
||||
{ kind: 'item', label: t('sec.udp'), id: 'udp' },
|
||||
{ kind: 'item', label: t('sec.adifmon'), id: 'adifmon' },
|
||||
{ kind: 'item', label: t('sec.foldersync'), id: 'foldersync' },
|
||||
@@ -325,7 +328,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
|
||||
const SECTION_KEY: Partial<Record<SectionId, string>> = {
|
||||
station: 'sec.station', profiles: 'sec.profiles', operating: 'sec.operating', confirmations: 'sec.confirmations',
|
||||
'external-services': 'sec.external', appearance: 'sec.appearance', lookup: 'sec.lookup', 'lists-bands': 'sec.bands', 'lists-modes': 'sec.modes',
|
||||
cluster: 'sec.cluster', backup: 'sec.backup', database: 'sec.database', autostart: 'sec.autostart', udp: 'sec.udp',
|
||||
cluster: 'sec.cluster', ftx: 'sec.ftx', backup: 'sec.backup', database: 'sec.database', autostart: 'sec.autostart', udp: 'sec.udp',
|
||||
adifmon: 'sec.adifmon',
|
||||
foldersync: 'sec.foldersync',
|
||||
webpublish: 'sec.webpublish',
|
||||
@@ -1519,6 +1522,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [autofocusWB, setAutofocusWB] = useState(() => localStorage.getItem('opslog.autofocusWB') !== '0');
|
||||
const [checkUpdates, setCheckUpdates] = useState(() => localStorage.getItem('opslog.checkUpdates') !== '0');
|
||||
const [showBeamMap, setShowBeamMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
|
||||
// Compact rotor widget: the dial and the SP/LP readout only. Off by default —
|
||||
// the full widget is what the operator has today, and a setting that changes
|
||||
// a panel the moment you upgrade is a setting that gets blamed for it.
|
||||
const [rotorCompact, setRotorCompact] = useState(() => localStorage.getItem('opslog.rotorCompact') === '1');
|
||||
const [startEqEnd, setStartEqEnd] = useState(() => localStorage.getItem('opslog.startEqualsEnd') === '1');
|
||||
const [lookupOnBlur, setLookupOnBlur] = useState(() => localStorage.getItem('opslog.lookupOnBlur') === '1');
|
||||
const [groupDigital, setGroupDigital] = useState(() => localStorage.getItem('opslog.groupDigitalSlots') === '1');
|
||||
@@ -1724,6 +1731,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
// has side effects there — adding the RBN nodes, bringing the PSK Reporter
|
||||
// feed up or down — so the write has to go where those live.
|
||||
const [bandOpen, setBandOpen] = useState<any>({ enabled: false, bands: [], available: [] });
|
||||
// How a worked square is matched, and what counts as still wanted.
|
||||
const [autoCall, setAutoCall] = useState<AutoCallSettings>(loadAutoCall);
|
||||
const [gridScope, setGridScope] = useState<any>({ scope: 'mix_digi', hunt: 'new', scopes: [] });
|
||||
useEffect(() => { GetGridScopeSettings().then((g) => setGridScope(g as any)).catch(() => {}); }, []);
|
||||
const saveGridScope = async (next: any) => {
|
||||
setGridScope(next);
|
||||
try { await SaveGridScopeSettings(next); } catch { /* the panel keeps the choice either way */ }
|
||||
};
|
||||
const [chaseGrids, setChaseGrids] = useState(false);
|
||||
const [chaseNew, setChaseNew] = useState(false);
|
||||
const [spotTTL, setSpotTTL] = useState(0);
|
||||
@@ -4180,10 +4195,25 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* How much of the rotor widget to draw. Compact keeps the dial and the
|
||||
SP/LP azimuths and drops the controls — for an operator who turns
|
||||
the antenna from the bearing pill or from PstRotator and wants the
|
||||
dial to take a column, not a panel. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<div className="text-sm font-semibold">{t('rot.widget')}</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox checked={rotorCompact}
|
||||
onCheckedChange={(c) => { const v = !!c; setRotorCompact(v); writeUiPref('opslog.rotorCompact', v ? '1' : '0'); }} />
|
||||
{t('rot.compact')}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">{t('rot.compactHint')}</p>
|
||||
</div>
|
||||
|
||||
{/* Quick-turn buttons for the rotor widget. Their azimuths start out
|
||||
computed from the station square, so they are right for THIS QTH
|
||||
rather than copied from someone else's. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
rather than copied from someone else's. Hidden in compact mode —
|
||||
editing buttons that are not drawn is a trap. */}
|
||||
<div className={cn('border-t border-border/60 pt-3 space-y-2', rotorCompact && 'opacity-40')}>
|
||||
<div className="text-sm font-semibold">{t('rot.presets')}</div>
|
||||
<p className="text-xs text-muted-foreground">{t('rot.presetsHint')}</p>
|
||||
<div className="space-y-1.5">
|
||||
@@ -4560,6 +4590,91 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
setEditingServer(next);
|
||||
}
|
||||
|
||||
// FTx decodes: what OpsLog does on its own with the digital stream.
|
||||
//
|
||||
// Auto-call KEYS THE TRANSMITTER without anyone clicking, so the panel is
|
||||
// deliberately explicit about it: off by default, every criterion opt-in, and
|
||||
// a plain statement of what the machine will do once it is on.
|
||||
function FtxPanel() {
|
||||
const set = (patch: Partial<AutoCallSettings>) => {
|
||||
const next = { ...autoCall, ...patch };
|
||||
setAutoCall(next);
|
||||
writeUiPref(autoCallKey, JSON.stringify(next));
|
||||
};
|
||||
const crit = (which: 'criteria' | 'watchCriteria', k: keyof AutoCallCriteria) => (
|
||||
<label key={k} className="flex items-center gap-1.5 text-xs cursor-pointer">
|
||||
<Checkbox
|
||||
checked={!!autoCall[which][k]}
|
||||
onCheckedChange={(v) => set({ [which]: { ...autoCall[which], [k]: !!v } } as any)}
|
||||
/>
|
||||
{t(`ftx.c_${k}`)}
|
||||
</label>
|
||||
);
|
||||
const KEYS: (keyof AutoCallCriteria)[] = ['dxcc', 'band', 'mode', 'slot', 'grid', 'county', 'pota', 'pfx'];
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={t('sec.ftx')} hint={t('ftx.hint')} />
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border border-border p-3 space-y-3">
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={autoCall.enabled} className="mt-0.5"
|
||||
onCheckedChange={(v) => set({ enabled: !!v })} />
|
||||
<span>
|
||||
{t('ftx.enable')}{' '}
|
||||
<span className="text-xs text-muted-foreground">{t('ftx.enableHint')}</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{autoCall.enabled && (
|
||||
<div className="pl-6 space-y-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted-foreground mb-1.5">{t('ftx.callWhen')}</p>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">{KEYS.map((k) => crit('criteria', k))}</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-3">
|
||||
<p className="text-xs font-semibold text-muted-foreground mb-1">{t('ftx.watch')}</p>
|
||||
<p className="text-[11px] text-muted-foreground mb-1.5 leading-relaxed">{t('ftx.watchHint')}</p>
|
||||
<Textarea
|
||||
className="h-20 font-mono text-xs"
|
||||
placeholder={"4S7*\nTM0HQ\n*/P"}
|
||||
defaultValue={(autoCall.watch ?? []).join('\n')}
|
||||
key={`w-${(autoCall.watch ?? []).length}`}
|
||||
onBlur={(e) => set({
|
||||
watch: e.target.value.split('\n').map((x) => x.trim().toUpperCase()).filter(Boolean),
|
||||
})}
|
||||
/>
|
||||
{(autoCall.watch ?? []).length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-[11px] text-muted-foreground mb-1">{t('ftx.watchOnlyIf')}</p>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">{KEYS.map((k) => crit('watchCriteria', k))}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-3 flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">{t('ftx.cooldown')}</span>
|
||||
<Input type="number" min={10} max={3600} className="w-24 h-7 text-xs"
|
||||
defaultValue={autoCall.cooldownSec}
|
||||
key={`cd-${autoCall.cooldownSec}`}
|
||||
onBlur={(e) => {
|
||||
const v = parseInt(e.target.value, 10);
|
||||
if (!isNaN(v) && v >= 10 && v <= 3600) set({ cooldownSec: v });
|
||||
}} />
|
||||
<span className="text-xs text-muted-foreground">s</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] rounded border border-warning-border bg-warning-muted text-warning-muted-foreground px-2 py-1.5 leading-relaxed">
|
||||
{t('ftx.warn')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ClusterPanel() {
|
||||
const sorted = [...clusterServers].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
|
||||
return (
|
||||
@@ -4714,6 +4829,36 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{pskrStatus?.running ? ` · ${t('bo.feedUp', { n: pskrStatus.received ?? 0 })}` : ''}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* What counts as "I already have this square". There is no single
|
||||
right answer — a VUCC chaser counts a square per band, someone
|
||||
filling a wall map counts it once — so it is a choice, and the
|
||||
same six GridTracker offers, because a square wanted in one and
|
||||
not the other is a bug report every time. */}
|
||||
<div className="pl-6 space-y-1.5 pt-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground w-28 shrink-0">{t('gsc.scope')}</span>
|
||||
<Select value={gridScope.scope} onValueChange={(v) => saveGridScope({ ...gridScope, scope: v })}>
|
||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(gridScope.scopes ?? []).map((s: any) => (
|
||||
<SelectItem key={s.key} value={s.key}>{t(`gsc.scope_${s.key}`)}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground w-28 shrink-0">{t('gsc.hunt')}</span>
|
||||
<Select value={gridScope.hunt} onValueChange={(v) => saveGridScope({ ...gridScope, hunt: v })}>
|
||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="new">{t('gsc.huntNew')}</SelectItem>
|
||||
<SelectItem value="new_unconfirmed">{t('gsc.huntUnconf')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('gsc.hint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chase new — its own option, NOT nested under grid chasing. Chasing
|
||||
@@ -4754,6 +4899,28 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* How near a RECEIVER has to be. This is the whole premise of
|
||||
the watch — a report is evidence about YOUR path only if it
|
||||
was collected near you — and the right distance differs by
|
||||
band, so it is the operator's call. Committed on blur/Enter,
|
||||
not per keystroke: each save restarts the feed. */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">{t('bo.nearKm')}</span>
|
||||
<Input
|
||||
type="number" min={25} max={1000} step={25}
|
||||
className="w-24 h-7 text-xs"
|
||||
defaultValue={bandOpen.near_km ?? 300}
|
||||
key={`nk-${bandOpen.near_km ?? 300}`}
|
||||
onBlur={(e) => {
|
||||
const v = parseInt(e.target.value, 10);
|
||||
if (!isNaN(v) && v !== bandOpen.near_km) saveBandOpen({ ...bandOpen, near_km: v });
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">km</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('bo.nearKmHint')}</p>
|
||||
|
||||
{/* A live count, because a feed that is connected but silent looks
|
||||
exactly like one that is broken until a number moves. */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -6670,6 +6837,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
'lists-bands': BandsPanel,
|
||||
'lists-modes': ModesPanel,
|
||||
cluster: ClusterPanel,
|
||||
ftx: FtxPanel,
|
||||
udp: UDPIntegrationsPanelWrapper,
|
||||
// Module-scope components, wrapped so their props can be passed. The nested
|
||||
// panels below go through PanelHost instead — which is what now lets either
|
||||
|
||||
Reference in New Issue
Block a user