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:
+62
-3
@@ -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.
|
||||
|
||||
+6
-2
@@ -13,7 +13,9 @@
|
||||
"Watchlist: adding or removing a callsign now raises the same kind of notification as a new version, instead of a message inside the watchlist page — a call can be added from the cluster or the DXpeditions tab, where that message was never seen.",
|
||||
"QSO editor, QSL Info: a HamQTH channel and its row in the status table — sent only, the received column showing a dash since the site publishes no confirmations.",
|
||||
"QSO editor, QSL Info: the confirmation channels are listed paper QSL and LoTW first — the two that carry an ARRL award — then alphabetically, in both the picker and the status table.",
|
||||
"Band map: a chevron in the footer folds the colour legend away and brings it back — four lines of a short screen, remembered between sessions."
|
||||
"Band map: a chevron in the footer folds the colour legend away and brings it back — four lines of a short screen, remembered between sessions.",
|
||||
"Band map: ctrl+wheel no longer zooms it — that gesture is the window zoom everywhere else in OpsLog. The + and − buttons keep the zoom.",
|
||||
"DXpeditions: the news headlines carry a chase badge too — NEW DXCC or worked. Entity-level only, since a headline names no band: the finer verdicts stay in the announcements pane, which knows the bands and modes."
|
||||
],
|
||||
"fr": [
|
||||
"Changer de base de réglages n’affiche plus « OpsLog is already running » : la relance automatique attend désormais que l’instance qui se ferme libère son verrou au lieu de la prendre de vitesse.",
|
||||
@@ -26,7 +28,9 @@
|
||||
"Watchlist : ajouter ou retirer un indicatif déclenche désormais une notification du même type que celle des nouvelles versions, au lieu d’un message dans la page watchlist — un indicatif peut être ajouté depuis le cluster ou l’onglet DXpéditions, où ce message n’était jamais vu.",
|
||||
"Éditeur de QSO, onglet QSL : un canal HamQTH et sa ligne dans le tableau des statuts — envoi seulement, la colonne reçu affichant un tiret puisque le site ne publie aucune confirmation.",
|
||||
"Éditeur de QSO, onglet QSL : les canaux de confirmation sont classés QSL papier puis LoTW — les deux qui comptent pour un diplôme ARRL — puis par ordre alphabétique, dans le sélecteur comme dans le tableau.",
|
||||
"Band map : un chevron dans le pied de page replie la légende des couleurs et la fait revenir — quatre lignes gagnées sur un petit écran, mémorisé d’une session à l’autre."
|
||||
"Band map : un chevron dans le pied de page replie la légende des couleurs et la fait revenir — quatre lignes gagnées sur un petit écran, mémorisé d’une session à l’autre.",
|
||||
"Band map : ctrl+molette ne zoome plus la carte — ce geste est le zoom de la fenêtre partout ailleurs dans OpsLog. Les boutons + et − gardent le zoom.",
|
||||
"DXpéditions : les actualités portent aussi un badge de chasse — NOUVEAU DXCC ou contacté. Au niveau entité seulement, une manchette ne nommant aucune bande : les verdicts plus fins restent dans le volet des annonces, qui connaît bandes et modes."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -471,19 +471,10 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [band, containerH, currentFreqHz, range, lo, hi, pxPerKHz, fitToBand]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollerRef.current;
|
||||
if (!el) return;
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
if (!range) return;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault();
|
||||
changeZoom(e.deltaY > 0 ? -1 : 1);
|
||||
}
|
||||
};
|
||||
el.addEventListener('wheel', onWheel, { passive: false });
|
||||
return () => el.removeEventListener('wheel', onWheel);
|
||||
}, [range]);
|
||||
// No ctrl+wheel zoom here any more: ctrl+wheel is the WINDOW zoom everywhere
|
||||
// else in OpsLog (View ▸ Zoom in/out), and one gesture that resizes the whole
|
||||
// app over one panel and one band map over another is a gesture nobody can
|
||||
// trust. The + / − buttons keep the zoom, deliberately and visibly.
|
||||
|
||||
// Ctrl+↑ / Ctrl+↓ hop to the next spot above / below the rig frequency and tune
|
||||
// to it. Higher freq is UP on the map (see freqToY), so ↑ = next higher spot.
|
||||
|
||||
@@ -26,6 +26,10 @@ type DXped = {
|
||||
type News = {
|
||||
title: string; link: string; pub_date: string; excerpt: string;
|
||||
creator: string; image_url: string; tag: string; calls?: string[];
|
||||
// Entity-level only — a headline names no band, so the backend answers
|
||||
// 'new' or 'worked' and nothing finer.
|
||||
status_chase?: string;
|
||||
unconfirmed?: boolean;
|
||||
};
|
||||
|
||||
// One badge per expedition, the strongest verdict winning — the same palette
|
||||
@@ -180,6 +184,7 @@ export function DXpeditionsPanel() {
|
||||
)}
|
||||
{news.map((n, i) => {
|
||||
const newsCalls = n.calls ?? [];
|
||||
const newsBadge = CHASE_BADGE[n.status_chase ?? ''];
|
||||
const newsAllWatched = newsCalls.length > 0 && newsCalls.every((c) => watched.has(c.toUpperCase()));
|
||||
return (
|
||||
<article key={`${n.link}-${i}`} className="rounded-md border border-border bg-muted/20 p-2">
|
||||
@@ -193,6 +198,15 @@ export function DXpeditionsPanel() {
|
||||
{n.tag && (
|
||||
<span className="px-1 py-px rounded text-[9px] font-bold uppercase bg-primary/15 text-primary">{n.tag}</span>
|
||||
)}
|
||||
{newsBadge && (
|
||||
<span className="px-1 py-px rounded text-[9px] font-bold uppercase tracking-wide border shrink-0"
|
||||
title={n.unconfirmed ? t('dec.unconfTip') : undefined}
|
||||
style={n.unconfirmed
|
||||
? { color: newsBadge.colour, borderColor: newsBadge.colour, borderStyle: 'dashed', opacity: 0.6 }
|
||||
: { color: newsBadge.colour, borderColor: newsBadge.colour }}>
|
||||
{t(newsBadge.label)}
|
||||
</span>
|
||||
)}
|
||||
<button type="button" onClick={() => n.link && BrowserOpenURL(n.link)}
|
||||
className="text-xs font-medium text-left hover:underline">{n.title}</button>
|
||||
</div>
|
||||
|
||||
@@ -436,7 +436,7 @@ const en: Dict = {
|
||||
'bmp.statusWorked': 'Entity worked on this band and mode', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
|
||||
'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
|
||||
'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.ftx': 'FTx', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.widthTip': 'Drag to resize — double-click to reset', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge',
|
||||
'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.openUnusual': 'unusual for the season', 'bmp.legendWorked': 'Entity worked', 'bmp.legendNewPota': 'New POTA', 'bmp.legendNewCounty': 'New county', 'bmp.legendWorkedCall': 'Callsign already worked', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Data', 'bmp.legendPhone': 'Phone', 'bmp.legendHide': 'Hide the legend', 'bmp.legendShow': 'Show the legend', 'bmp.footerHint': 'scroll · ctrl+wheel = zoom · ◎ = jump to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)',
|
||||
'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.openUnusual': 'unusual for the season', 'bmp.legendWorked': 'Entity worked', 'bmp.legendNewPota': 'New POTA', 'bmp.legendNewCounty': 'New county', 'bmp.legendWorkedCall': 'Callsign already worked', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Data', 'bmp.legendPhone': 'Phone', 'bmp.legendHide': 'Hide the legend', 'bmp.legendShow': 'Show the legend', 'bmp.footerHint': 'scroll · ◎ = go to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)',
|
||||
'frm.welcome': 'Welcome to OpsLog', 'frm.intro': 'Set up your station to start logging. These fields stamp every QSO and can be changed later in Preferences → Station Information (and per profile).',
|
||||
'frm.callsign': 'Callsign', 'frm.locator': 'Locator', 'frm.operator': 'Operator', 'frm.operatorPh': 'same as callsign', 'frm.owner': 'Owner', 'frm.ownerPh': 'station owner callsign', 'frm.name': 'Name', 'frm.namePh': 'your first name',
|
||||
'frm.awardRefs': 'Award reference lists', 'frm.awardRefsHint': 'IOTA · POTA · WWFF · SOTA — names & totals for those awards (optional, can take a minute).', 'frm.downloading': 'Downloading…', 'frm.reDownload': 'Re-download', 'frm.download': 'Download', 'frm.required': 'Callsign and locator are required.', 'frm.saving': 'Saving…', 'frm.startLogging': 'Start logging',
|
||||
@@ -943,7 +943,7 @@ const fr: Dict = {
|
||||
'bmp.statusWorked': 'Entité contactée sur cette bande et ce mode', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
|
||||
'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
|
||||
'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.ftx': 'FTx', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.widthTip': 'Glisser pour redimensionner — double-clic pour réinitialiser', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande',
|
||||
'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.openUnusual': 'inhabituel pour la saison', 'bmp.legendWorked': 'Entité contactée', 'bmp.legendNewPota': 'Nouveau POTA', 'bmp.legendNewCounty': 'Nouveau comté', 'bmp.legendWorkedCall': 'Indicatif déjà contacté', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Numérique', 'bmp.legendPhone': 'Phonie', 'bmp.legendHide': 'Masquer la légende', 'bmp.legendShow': 'Afficher la légende', 'bmp.footerHint': 'défiler · ctrl+molette = zoom · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)',
|
||||
'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.openUnusual': 'inhabituel pour la saison', 'bmp.legendWorked': 'Entité contactée', 'bmp.legendNewPota': 'Nouveau POTA', 'bmp.legendNewCounty': 'Nouveau comté', 'bmp.legendWorkedCall': 'Indicatif déjà contacté', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Numérique', 'bmp.legendPhone': 'Phonie', 'bmp.legendHide': 'Masquer la légende', 'bmp.legendShow': 'Afficher la légende', 'bmp.footerHint': 'défiler · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)',
|
||||
'frm.welcome': 'Bienvenue dans OpsLog', 'frm.intro': 'Configure ta station pour commencer à logger. Ces champs sont inscrits sur chaque QSO et peuvent être modifiés plus tard dans Préférences → Informations station (et par profil).',
|
||||
'frm.callsign': 'Indicatif', 'frm.locator': 'Locator', 'frm.operator': 'Opérateur', 'frm.operatorPh': "identique à l'indicatif", 'frm.owner': 'Propriétaire', 'frm.ownerPh': 'indicatif du propriétaire de la station', 'frm.name': 'Nom', 'frm.namePh': 'ton prénom',
|
||||
'frm.awardRefs': 'Listes de références des diplômes', 'frm.awardRefsHint': 'IOTA · POTA · WWFF · SOTA — noms et totaux pour ces diplômes (optionnel, peut prendre une minute).', 'frm.downloading': 'Téléchargement…', 'frm.reDownload': 'Retélécharger', 'frm.download': 'Télécharger', 'frm.required': "L'indicatif et le locator sont obligatoires.", 'frm.saving': 'Enregistrement…', 'frm.startLogging': 'Commencer à logger',
|
||||
|
||||
Vendored
+1
-2
@@ -12,7 +12,6 @@ import {award} from '../models';
|
||||
import {awardref} from '../models';
|
||||
import {bandopen} from '../models';
|
||||
import {cluster} from '../models';
|
||||
import {dxped} from '../models';
|
||||
import {extsvc} from '../models';
|
||||
import {powergenius} from '../models';
|
||||
import {pskr} from '../models';
|
||||
@@ -477,7 +476,7 @@ export function GetDVKMessages():Promise<Array<main.DVKMessage>>;
|
||||
|
||||
export function GetDVKStatus():Promise<main.DVKStatus>;
|
||||
|
||||
export function GetDXWorldNews():Promise<Array<dxped.News>>;
|
||||
export function GetDXWorldNews():Promise<Array<main.DXNews>>;
|
||||
|
||||
export function GetDXpeditions():Promise<Array<main.DXpedition>>;
|
||||
|
||||
|
||||
@@ -1466,37 +1466,6 @@ export namespace contest {
|
||||
|
||||
}
|
||||
|
||||
export namespace dxped {
|
||||
|
||||
export class News {
|
||||
title: string;
|
||||
link: string;
|
||||
pub_date: string;
|
||||
excerpt: string;
|
||||
creator: string;
|
||||
image_url: string;
|
||||
tag: string;
|
||||
calls: string[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new News(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.title = source["title"];
|
||||
this.link = source["link"];
|
||||
this.pub_date = source["pub_date"];
|
||||
this.excerpt = source["excerpt"];
|
||||
this.creator = source["creator"];
|
||||
this.image_url = source["image_url"];
|
||||
this.tag = source["tag"];
|
||||
this.calls = source["calls"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace extsvc {
|
||||
|
||||
export class ServiceConfig {
|
||||
@@ -2705,6 +2674,36 @@ export namespace main {
|
||||
this.rec_slot = source["rec_slot"];
|
||||
}
|
||||
}
|
||||
export class DXNews {
|
||||
title: string;
|
||||
link: string;
|
||||
pub_date: string;
|
||||
excerpt: string;
|
||||
creator: string;
|
||||
image_url: string;
|
||||
tag: string;
|
||||
calls: string[];
|
||||
status_chase: string;
|
||||
unconfirmed: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new DXNews(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.title = source["title"];
|
||||
this.link = source["link"];
|
||||
this.pub_date = source["pub_date"];
|
||||
this.excerpt = source["excerpt"];
|
||||
this.creator = source["creator"];
|
||||
this.image_url = source["image_url"];
|
||||
this.tag = source["tag"];
|
||||
this.calls = source["calls"];
|
||||
this.status_chase = source["status_chase"];
|
||||
this.unconfirmed = source["unconfirmed"];
|
||||
}
|
||||
}
|
||||
export class DXpedition {
|
||||
dxcc: string;
|
||||
callsign: string;
|
||||
|
||||
Reference in New Issue
Block a user