feat(ui): the FT decodes panel, two DXHunter themes, and the maps
Decodes: - one click SELECTS, two transmit. A single click handed the decode straight to WSJT-X as a reply, so brushing a row while reading the band started calling a station. - the list empties for a receiver that changes band, and a receiver column appears when more than one is feeding one merged list. - the period clock turns red while transmitting: it is the one thing on the screen that moves, so it is where the eye already is. - a WL badge, after the LoTW "L" — one letter, always in the same place, so the column does not shift from row to row. - the auto-call switch, its target and its count, and the chase list: naming the station you are waiting for is done while watching the band, not in a settings tree. Themes: DXHunter's slate with its own blue, and the same slate with OpsLog's orange. Counted across its sources rather than guessed from one panel — blue is 132 uses to violet's 25, and the violet is the PSK Reporter panel alone. Watchlist: drawn as DXHunter draws it — the callsign in the interface font rather than monospaced, which is the difference that shows with the two windows side by side. FT Map: arcs no longer run off the side of the map. The map shows one world, and a path crossing the antimeridian was drawn past 180° into the blank space beside it — from VK that is most of them. Cluster: "superfox", "fox/hound" and "F/H" in a comment are read as FT8. They are WSJT-X's DXpedition transmit modes, and the comment fell through to the band plan and came out DATA — which then decided the band+mode verdict. A decoder is named by what it IS: Nexus sends its packets as "Tempo", the engine inside it, and OpsLog showed a program nobody has heard of.
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, GetPSKTargetSettings, SavePSKTargetSettings, GetAutoCallSettings, SaveAutoCallSettings, GetWatchlistContestCalls, SetWatchlistContestCalls, GetWatchlistContestPattern, SetWatchlistContestPattern, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetSpotMax, SetSpotMax,
|
||||
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetPSKTargetSettings, SavePSKTargetSettings, GetAutoCallSettings, SaveAutoCallSettings, GetChaseNewBands, SetChaseNewBands, 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';
|
||||
@@ -405,16 +405,21 @@ interface TreeProps {
|
||||
flexAvailable?: boolean;
|
||||
}
|
||||
|
||||
function Tree({ selected, onSelect, flexAvailable }: TreeProps) {
|
||||
// The sidebar. Memoised and its tree built once per (language, radio): it is
|
||||
// sixty-odd items that depend on nothing an operator types, and it was rebuilt
|
||||
// and re-rendered on every keystroke in every field of every panel — the whole
|
||||
// dialog holds its state in one component, so one character redraws all of it.
|
||||
const Tree = memo(function Tree({ selected, onSelect, flexAvailable }: TreeProps) {
|
||||
const { t } = useI18n();
|
||||
const nodes = useMemo(() => buildTree(!!flexAvailable, t), [flexAvailable, t]);
|
||||
return (
|
||||
<nav className="text-sm">
|
||||
{buildTree(!!flexAvailable, t).map((node, i) => (
|
||||
{nodes.map((node, i) => (
|
||||
<TreeNodeView key={i} node={node} depth={0} selected={selected} onSelect={onSelect} />
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function TreeNodeView({
|
||||
node, depth, selected, onSelect,
|
||||
@@ -472,6 +477,11 @@ function TreeNodeView({
|
||||
// identity is STABLE across SettingsModal re-renders; defining them inside the
|
||||
// component would give each render a fresh function, remounting the Radix
|
||||
// Select and slamming the open dropdown shut on any ambient re-render.
|
||||
// The bands the Chase new filter offers, in band-plan order. Mirrors
|
||||
// ChaseNewBands in pskchase.go, which is what actually filters the feed —
|
||||
// a band added there and not here is simply one nobody can switch off.
|
||||
const CHASE_BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '4m', '2m', '70cm'];
|
||||
|
||||
const THEME_SWATCH: Record<Exclude<ThemeChoice, 'auto'>, { bg: string; card: string; accent: string }> = {
|
||||
'light-warm': { bg: '#e8dfc9', card: '#faf6ea', accent: '#b8410c' },
|
||||
'light-cool': { bg: '#f4f6f8', card: '#ffffff', accent: '#2563eb' },
|
||||
@@ -2089,11 +2099,18 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
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 });
|
||||
const [ac, setAc] = useState<any>({ enabled: false, only: '', attempts: 7, watched_attempts: 15, misses: 3, max_rounds: 3, rest_min: 2, on_screen_only: true });
|
||||
// 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('');
|
||||
// Which bands the Chase new panel shows. The station's own band list still
|
||||
// applies underneath — this one is "what am I watching tonight".
|
||||
const [chaseBands, setChaseBands] = useState<string[]>([]);
|
||||
const saveChaseBands = (next: string[]) => {
|
||||
setChaseBands(next);
|
||||
void SetChaseNewBands(next);
|
||||
};
|
||||
const [contestPattern, setContestPattern] = useState('');
|
||||
const saveAC = async (next: any) => {
|
||||
setAc(next);
|
||||
@@ -2132,6 +2149,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
try { setChaseNew(await GetChaseNew()); } catch { /* defaults stand */ }
|
||||
try { setPskTgt(await GetPSKTargetSettings()); } catch { /* defaults stand */ }
|
||||
try { setAc(await GetAutoCallSettings()); } catch { /* defaults stand */ }
|
||||
try { setChaseBands((await GetChaseNewBands()) ?? []); } catch { /* defaults stand */ }
|
||||
try { setContestCalls(await GetWatchlistContestCalls()); } catch { /* defaults stand */ }
|
||||
try { setContestPattern(await GetWatchlistContestPattern()); } catch { /* defaults stand */ }
|
||||
try { setLinkedAmps((await GetLinkedAmps()) ?? []); } catch { /* defaults stand */ }
|
||||
@@ -5402,6 +5420,34 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
<span className="text-xs text-muted-foreground">{t('chn.nearKmHint')}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Which bands the panel lists. Not the station's band list — that
|
||||
one says what this station can work at all and still applies
|
||||
underneath. This says what is worth watching tonight, which is
|
||||
a different question and changes far more often. */}
|
||||
{chaseNew && (
|
||||
<div className="pl-6 space-y-1">
|
||||
<span className="text-xs text-muted-foreground">{t('chn.bands')}</span>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{CHASE_BANDS.map((b) => {
|
||||
const on = chaseBands.includes(b);
|
||||
return (
|
||||
<button key={b} type="button"
|
||||
onClick={() => saveChaseBands(on ? chaseBands.filter((x) => x !== b) : [...chaseBands, b])}
|
||||
className={cn('h-6 px-2 rounded-full border text-[11px] font-medium transition-colors',
|
||||
on ? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border text-muted-foreground hover:bg-muted')}>
|
||||
{b}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button type="button" onClick={() => saveChaseBands([...CHASE_BANDS])}
|
||||
className="h-6 px-2 rounded-full border border-border text-[11px] text-muted-foreground hover:bg-muted">
|
||||
{t('chn.bandsAll')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('chn.bandsHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PSK Reporter analysis of the station being called. Same service as
|
||||
@@ -5505,6 +5551,20 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* A rule about what the transmitter may call, stated once. The
|
||||
decodes panel has a LoTW chip too, but that one is a way of
|
||||
READING the band — it is flicked on and off while looking
|
||||
around, and the panel can be closed. */}
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<Checkbox checked={ac.on_screen_only !== false}
|
||||
onCheckedChange={(c) => saveAC({ ...ac, on_screen_only: !!c })} />
|
||||
<span>{t('ac.onScreen')} <span className="text-muted-foreground">{t('ac.onScreenHint')}</span></span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<Checkbox checked={!!ac.trace}
|
||||
onCheckedChange={(c) => saveAC({ ...ac, trace: !!c })} />
|
||||
<span>{t('ac.trace')} <span className="text-muted-foreground">{t('ac.traceHint')}</span></span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user