feat(bandmap,dxped): drop the ctrl+wheel zoom, badge the news

ctrl+wheel is the WINDOW zoom everywhere else in OpsLog, and one gesture
that resizes the whole app over one panel and one band map over another
is a gesture nobody can trust. The + and - buttons keep the zoom, and
the footer hint stops advertising what is gone.

The DX-World headlines now carry a chase badge like the announcements —
but ENTITY-LEVEL only. A headline names no band, and asking the slot
question without one answers 'new band' for every entity ever worked:
the one mistake that costs a QSO. So the news says NEW DXCC or worked,
and the sharper verdicts stay where the bands and modes are known.
This commit is contained in:
2026-08-31 18:57:44 +02:00
parent 4a017f6290
commit 2b6f1ba9d7
7 changed files with 119 additions and 53 deletions
+62 -3
View File
@@ -119,9 +119,22 @@ func (a *App) judgeDXpeditions(list []DXpedition) {
}
}
// DXNews is one DX-World post plus what its callsigns are worth here.
type DXNews struct {
dxped.News
// Status is ENTITY-LEVEL only, and deliberately so: a headline announces no
// bands or modes, and asking the slot question without them would answer
// "new band" for every entity ever worked — the one mistake that costs a
// QSO. So it says "new" (entity never worked) or "worked", nothing finer;
// the announcements pane, which HAS the bands and modes, is where the
// sharper verdicts belong.
Status string `json:"status_chase"`
Unconfirmed bool `json:"unconfirmed"`
}
// GetDXWorldNews returns the DX-World headlines, with the callsigns mined out
// of each one so the reader can act on them.
func (a *App) GetDXWorldNews() ([]dxped.News, error) {
// of each one and judged against the log.
func (a *App) GetDXWorldNews() ([]DXNews, error) {
if a.dxped == nil {
a.dxped = dxped.New()
}
@@ -132,7 +145,53 @@ func (a *App) GetDXWorldNews() ([]dxped.News, error) {
return nil, err
}
}
return news, nil
out := make([]DXNews, 0, len(news))
for _, n := range news {
out = append(out, DXNews{News: n})
}
a.judgeNews(out)
return out, nil
}
// judgeNews fills the entity-level verdict for each headline.
//
// One query per callsign with NO band and NO mode: the only verdict that
// means anything in that state is "this entity has never been worked". Every
// finer answer the spot judge can give without a band is an artefact of the
// missing band, so it is flattened to "worked" rather than repeated.
func (a *App) judgeNews(list []DXNews) {
if a.qso == nil || len(list) == 0 {
return
}
var queries []SpotQuery
var owners []int
for i, n := range list {
for _, call := range n.Calls {
queries = append(queries, SpotQuery{Call: call})
owners = append(owners, i)
}
}
if len(queries) == 0 {
return
}
res := a.ClusterSpotStatuses(queries)
for i, r := range res {
if i >= len(owners) {
break
}
row := owners[i]
st := ""
switch {
case r.Status == "new":
st = "new"
case r.Status != "":
st = "worked"
}
if chaseRank[st] > chaseRank[list[row].Status] {
list[row].Status = st
list[row].Unconfirmed = r.UnconfStatus
}
}
}
// RefreshDXpeditions drops both caches so the next read goes to the network.