chore: release v0.27.12
This commit is contained in:
@@ -60,7 +60,7 @@ import {
|
||||
GetFolderSync, SaveFolderSync, PickFolderSyncFolder, GetFolderSyncStatus, SyncFolderNow,
|
||||
GetRelayAuto, SaveRelayAuto, GetStationDevices,
|
||||
GetAwardDefs, GetTrackedAwards, SaveTrackedAwards,
|
||||
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetSpotMax, SetSpotMax,
|
||||
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetPSKTargetSettings, SavePSKTargetSettings, GetAutoCallSettings, SaveAutoCallSettings, GetWatchlistContestCalls, SetWatchlistContestCalls, GetWatchlistContestPattern, SetWatchlistContestPattern, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetSpotMax, SetSpotMax,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import type { profile as profileModels } from '../../wailsjs/go/models';
|
||||
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
@@ -484,6 +484,8 @@ const THEME_SWATCH: Record<Exclude<ThemeChoice, 'auto'>, { bg: string; card: str
|
||||
'dark-indigo': { bg: '#0e0f1f', card: '#181a30', accent: '#7c6cff' },
|
||||
'dark-teal': { bg: '#061c21', card: '#0d2c33', accent: '#22d3ee' },
|
||||
'dark-plum': { bg: '#180f1e', card: '#251830', accent: '#f472b6' },
|
||||
'dxhunter': { bg: '#0f172a', card: '#1e293b', accent: '#3b82f6' },
|
||||
'dxhunter-orange': { bg: '#0f172a', card: '#1e293b', accent: '#f97316' },
|
||||
'high-contrast': { bg: '#000000', card: '#0d0d0d', accent: '#ff7a1a' },
|
||||
};
|
||||
|
||||
@@ -2086,6 +2088,21 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
const [chaseStateOn, setChaseStateOn] = useState(() => localStorage.getItem('opslog.chaseState') !== '0');
|
||||
const [chaseSotaOn, setChaseSotaOn] = useState(() => localStorage.getItem('opslog.chaseSota') !== '0');
|
||||
const [chaseNew, setChaseNew] = useState(false);
|
||||
const [pskTgt, setPskTgt] = useState<any>({ enabled: false, scope: 'target' });
|
||||
const [ac, setAc] = useState<any>({ enabled: false, only: '', attempts: 7, watched_attempts: 15, misses: 3, max_rounds: 3, rest_min: 2 });
|
||||
// The named contest callsigns. Raw text in state, written on blur: it is a
|
||||
// multi-line list, and normalising it on every keystroke would fight the
|
||||
// Return key — the one key this box is built around.
|
||||
const [contestCalls, setContestCalls] = useState('');
|
||||
const [contestPattern, setContestPattern] = useState('');
|
||||
const saveAC = async (next: any) => {
|
||||
setAc(next);
|
||||
try { await SaveAutoCallSettings(next); } catch { /* the toolbar shows what the engine is doing */ }
|
||||
};
|
||||
const savePSKTgt = async (next: any) => {
|
||||
setPskTgt(next);
|
||||
try { await SavePSKTargetSettings(next); } catch { /* the panel itself reports what the feed is doing */ }
|
||||
};
|
||||
const [spotTTL, setSpotTTL] = useState(0);
|
||||
const [spotTTLText, setSpotTTLText] = useState('0');
|
||||
const [spotMaxText, setSpotMaxText] = useState('1000');
|
||||
@@ -2113,6 +2130,10 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
writeUiPref('opslog.chaseGrids', g ? '1' : '0');
|
||||
} catch { /* defaults stand */ }
|
||||
try { setChaseNew(await GetChaseNew()); } catch { /* defaults stand */ }
|
||||
try { setPskTgt(await GetPSKTargetSettings()); } catch { /* defaults stand */ }
|
||||
try { setAc(await GetAutoCallSettings()); } catch { /* defaults stand */ }
|
||||
try { setContestCalls(await GetWatchlistContestCalls()); } catch { /* defaults stand */ }
|
||||
try { setContestPattern(await GetWatchlistContestPattern()); } catch { /* defaults stand */ }
|
||||
try { setLinkedAmps((await GetLinkedAmps()) ?? []); } catch { /* defaults stand */ }
|
||||
try { const n = await GetSpotTTLMinutes(); setSpotTTL(n); setSpotTTLText(String(n)); } catch { /* defaults stand */ }
|
||||
try { const n = await GetSpotMax(); setSpotMaxText(String(n)); } catch { /* defaults stand */ }
|
||||
@@ -5357,6 +5378,135 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
onCheckedChange={(c) => { setChaseNew(!!c); SetChaseNew(!!c).catch(() => {}); }} />
|
||||
<span>{t('chn.option')} <span className="text-xs text-muted-foreground">{t('chn.optionHelp')}</span></span>
|
||||
</label>
|
||||
{/* The same radius the band-opening watch uses — one feed, one
|
||||
circle — but reachable from here, because an operator who only
|
||||
wants the chase list would otherwise have to find it inside a
|
||||
watch they never switched on. It is the setting that decides
|
||||
whether this list has anything in it at all: where stations are
|
||||
far apart, 300 km can hold no receivers whatsoever. */}
|
||||
{chaseNew && (
|
||||
<div className="flex items-center gap-2 flex-wrap pl-6">
|
||||
<span className="text-xs text-muted-foreground">{t('bo.nearKm')}</span>
|
||||
<Input
|
||||
type="number" min={25} max={3000} step={25}
|
||||
className="w-24 h-7 text-xs"
|
||||
defaultValue={bandOpen.near_km ?? 300}
|
||||
key={`cnk-${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>
|
||||
<span className="text-xs text-muted-foreground">{t('chn.nearKmHint')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PSK Reporter analysis of the station being called. Same service as
|
||||
the two options above, opposite question: those ask what is being
|
||||
heard around here, this asks whether ONE station can hear you. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={pskTgt.enabled} className="mt-0.5"
|
||||
onCheckedChange={(c) => savePSKTgt({ ...pskTgt, enabled: !!c })} />
|
||||
<span>{t('psk.setEnable')} <span className="text-xs text-muted-foreground">{t('psk.setEnableHint')}</span></span>
|
||||
</label>
|
||||
{pskTgt.enabled && (
|
||||
<div className="pl-6 space-y-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">{t('psk.setScope')}</span>
|
||||
<Select value={pskTgt.scope} onValueChange={(v) => savePSKTgt({ ...pskTgt, scope: v })}>
|
||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="target">{t('psk.setScopeTarget')}</SelectItem>
|
||||
<SelectItem value="band">{t('psk.setScopeBand')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('psk.setScopeHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contest — how a special-event fleet finds its way onto the watch
|
||||
list on its own. Two halves, because a fleet has two kinds of
|
||||
member. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<div className="text-sm font-medium">{t('wlc.title')}</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">{t('wlc.pattern')}</span>
|
||||
<Input className="h-7 w-32 text-xs font-mono uppercase"
|
||||
defaultValue={contestPattern} key={`wlcp-${contestPattern}`}
|
||||
placeholder={t('wlc.patternPh')}
|
||||
onBlur={(e) => {
|
||||
const v = e.target.value.toUpperCase().trim();
|
||||
if (v !== contestPattern) { setContestPattern(v); void SetWatchlistContestPattern(v); }
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }} />
|
||||
<span className="text-xs text-muted-foreground">{t('wlc.patternHint')}</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">{t('wlc.calls')}</span>
|
||||
<textarea
|
||||
className="w-full h-24 rounded-md border border-border bg-background p-2 text-xs font-mono uppercase"
|
||||
value={contestCalls}
|
||||
placeholder={t('wlc.callsPh')}
|
||||
onChange={(e) => setContestCalls(e.target.value)}
|
||||
onBlur={(e) => void SetWatchlistContestCalls(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('wlc.callsHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto-call. Last in this section, and behind a warning: it is the
|
||||
only setting in OpsLog that transmits without being asked to. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={ac.enabled} className="mt-0.5"
|
||||
onCheckedChange={(c) => saveAC({ ...ac, enabled: !!c })} />
|
||||
<span>{t('ac.enable')} <span className="text-xs text-muted-foreground">{t('ac.enableHint')}</span></span>
|
||||
</label>
|
||||
<p className="text-xs text-warning">{t('ac.warn')}</p>
|
||||
{ac.enabled && (
|
||||
<div className="pl-6 space-y-2">
|
||||
<p className="text-xs text-muted-foreground">{t('ac.ladder')}</p>
|
||||
{/* A LIST: several callsigns, spaces or commas. Committed on
|
||||
blur or Enter and kept as raw text while typing — binding the
|
||||
box to the parsed value is what makes the space key look dead,
|
||||
and space is the one key this field needs. */}
|
||||
<div className="flex items-start gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground mt-1.5">{t('ac.only')}</span>
|
||||
<Input className="h-7 w-72 text-xs font-mono uppercase"
|
||||
defaultValue={ac.only ?? ''} key={`aco-${ac.only ?? ''}`}
|
||||
placeholder={t('ac.onlyPh')}
|
||||
onBlur={(e) => saveAC({ ...ac, only: e.target.value.toUpperCase() })}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }} />
|
||||
<span className="text-xs text-muted-foreground mt-1.5">{t('ac.onlyHint')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{([
|
||||
['attempts', t('ac.attempts'), 1, 30],
|
||||
['watched_attempts', t('ac.watchedAttempts'), 1, 60],
|
||||
['misses', t('ac.misses'), 1, 10],
|
||||
['max_rounds', t('ac.rounds'), 1, 10],
|
||||
['rest_min', t('ac.rest'), 1, 60],
|
||||
] as [string, string, number, number][]).map(([k, label, min, max]) => (
|
||||
<span key={k} className="inline-flex items-center gap-1.5">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<Input type="number" min={min} max={max} className="h-7 w-16 text-xs"
|
||||
defaultValue={(ac as any)[k]} key={`ac-${k}-${(ac as any)[k]}`}
|
||||
onBlur={(e) => {
|
||||
const v = parseInt(e.target.value, 10);
|
||||
if (!isNaN(v) && v >= min && v <= max && v !== (ac as any)[k]) saveAC({ ...ac, [k]: v });
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -5601,7 +5751,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
<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}
|
||||
type="number" min={25} max={3000} step={25}
|
||||
className="w-24 h-7 text-xs"
|
||||
defaultValue={bandOpen.near_km ?? 300}
|
||||
key={`nk-${bandOpen.near_km ?? 300}`}
|
||||
|
||||
Reference in New Issue
Block a user