feat: day batch — upload results surfaced as toasts (silent LoTW failures from the right-click send looked like nothing was sent); live selection count + Select all/Unselect all toggle in the grid toolbar; active filters bypass the row limit (all matches shown, 10k safety cap); NET Control: same right-click menu on worked-before + drag&drop roster<->on-air (drop on roster logs the QSO); compact mode restores previous window size/position; advanced-filter field renamed Station callsign; optional DXCC-style digital-mode grouping (Settings->General) for matrix badges + cluster colouring; changelog 0.20.9 entries (version NOT bumped)
This commit is contained in:
@@ -530,6 +530,13 @@ type App struct {
|
||||
// must be allowed through.
|
||||
shuttingDown bool
|
||||
compact bool // window is in the compact one-row mode (skip saving its tiny size as the normal geometry)
|
||||
// Geometry captured when ENTERING compact mode, restored on the way back —
|
||||
// otherwise leaving compact snapped the window to a hardcoded 1400×900 at
|
||||
// wherever the compact strip had been dragged.
|
||||
preCompactW, preCompactH int
|
||||
preCompactX, preCompactY int
|
||||
preCompactMax bool
|
||||
preCompactValid bool
|
||||
|
||||
// Cached operator location used to compute distance/bearing for
|
||||
// cluster spots. Refreshed on profile activation; zero means
|
||||
@@ -1811,6 +1818,17 @@ func (a *App) ResetDatabaseToDefault() error {
|
||||
// widths, sort…) in the DB settings table under a "ui." namespace, so they
|
||||
// travel with the logbook and survive a reinstall — unlike the WebView's
|
||||
// localStorage. Values are opaque JSON blobs owned by the frontend.
|
||||
// groupDigitalSlots reports the "digital modes count as one" preference
|
||||
// (Settings → General; stored as a portable ui.* pref so the frontend matrix
|
||||
// and this backend cluster logic read the same switch).
|
||||
func (a *App) groupDigitalSlots() bool {
|
||||
if a.settings == nil {
|
||||
return false
|
||||
}
|
||||
v, _ := a.settings.Get(a.ctx, "ui.opslog.groupDigitalSlots")
|
||||
return v == "1"
|
||||
}
|
||||
|
||||
func (a *App) GetUIPref(key string) (string, error) {
|
||||
if a.settings == nil {
|
||||
return "", nil
|
||||
@@ -5204,6 +5222,19 @@ func (a *App) SetCompactMode(on bool) {
|
||||
if a.ctx == nil {
|
||||
return
|
||||
}
|
||||
if on && !a.compact {
|
||||
// Capture the normal geometry BEFORE shrinking, so leaving compact puts
|
||||
// the window back exactly where and how big it was.
|
||||
a.preCompactMax = wruntime.WindowIsMaximised(a.ctx)
|
||||
if a.preCompactMax {
|
||||
// Unmaximise first: the size/position of a maximised window are the
|
||||
// monitor's, not the user's chosen geometry.
|
||||
wruntime.WindowUnmaximise(a.ctx)
|
||||
}
|
||||
a.preCompactW, a.preCompactH = wruntime.WindowGetSize(a.ctx)
|
||||
a.preCompactX, a.preCompactY = wruntime.WindowGetPosition(a.ctx)
|
||||
a.preCompactValid = a.preCompactW >= normalMinW && a.preCompactH >= normalMinH
|
||||
}
|
||||
a.compact = on
|
||||
if on {
|
||||
// Lock the window to the compact size by pinning min == max. Without
|
||||
@@ -5218,7 +5249,16 @@ func (a *App) SetCompactMode(on bool) {
|
||||
// Release the lock first (raise the max) before growing back.
|
||||
wruntime.WindowSetMaxSize(a.ctx, maxW, maxH)
|
||||
wruntime.WindowSetMinSize(a.ctx, normalMinW, normalMinH)
|
||||
wruntime.WindowSetSize(a.ctx, normalW, normalH)
|
||||
if a.preCompactValid {
|
||||
wruntime.WindowSetSize(a.ctx, a.preCompactW, a.preCompactH)
|
||||
wruntime.WindowSetPosition(a.ctx, a.preCompactX, a.preCompactY)
|
||||
if a.preCompactMax {
|
||||
wruntime.WindowMaximise(a.ctx)
|
||||
}
|
||||
} else {
|
||||
// Nothing captured (compact since launch) — fall back to the default.
|
||||
wruntime.WindowSetSize(a.ctx, normalW, normalH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8160,6 +8200,12 @@ func (a *App) runManualUpload(svc extsvc.Service, ids []int64, cfg extsvc.Extern
|
||||
msg = err.Error()
|
||||
}
|
||||
emit("LoTW upload failed: " + msg)
|
||||
// The qslmgr:log console is only visible in the QSL Manager — a failure
|
||||
// triggered from the Recent QSOs right-click was completely silent, which
|
||||
// read as "send to LoTW does nothing". Surface it as a toast too.
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "toast", "LoTW upload failed: "+msg)
|
||||
}
|
||||
} else {
|
||||
for _, id := range ids {
|
||||
a.markExtUploaded(svc, id, "")
|
||||
@@ -8314,6 +8360,16 @@ func (a *App) runManualUpload(svc extsvc.Service, ids []int64, cfg extsvc.Extern
|
||||
}
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "qslmgr:done", map[string]any{"uploaded": uploaded, "total": len(ids)})
|
||||
// Always end with a visible summary — the right-click "Send to …" runs in
|
||||
// the background and the per-QSO log lives in the QSL Manager console.
|
||||
label := map[extsvc.Service]string{
|
||||
extsvc.ServiceQRZ: "QRZ.com", extsvc.ServiceClublog: "Club Log", extsvc.ServiceHRDLog: "HRDLog",
|
||||
extsvc.ServiceLoTW: "LoTW", extsvc.ServiceEQSL: "eQSL",
|
||||
}[svc]
|
||||
if label == "" {
|
||||
label = string(svc)
|
||||
}
|
||||
wruntime.EventsEmit(a.ctx, "toast", fmt.Sprintf("%s: %d/%d QSO uploaded", label, uploaded, len(ids)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13065,7 +13121,14 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
entities, err := a.qso.EntitySlotMap(a.ctx, keyFor)
|
||||
// Optional digital-mode grouping (Settings → General): with it on, FT8/FT4/
|
||||
// RTTY… all count as ONE "DIG" mode, so an FT4 spot on a band where FT8 was
|
||||
// worked shows "worked", not "new-slot" — DXCC-style mode classes.
|
||||
var normMode func(string) string
|
||||
if a.groupDigitalSlots() {
|
||||
normMode = qso.GroupDigitalMode
|
||||
}
|
||||
entities, err := a.qso.EntitySlotMap(a.ctx, keyFor, normMode)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
@@ -13133,12 +13196,17 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
|
||||
}
|
||||
// Band already worked. If this MODE was never worked on the entity (any
|
||||
// band) → new-mode. If the mode was worked elsewhere but not on THIS
|
||||
// band+mode → new-slot. Otherwise → worked.
|
||||
if _, m := e.Modes[out[i].Mode]; !m {
|
||||
// band+mode → new-slot. Otherwise → worked. The check mode goes through
|
||||
// the same normaliser as the slot map (digital grouping).
|
||||
checkMode := out[i].Mode
|
||||
if normMode != nil {
|
||||
checkMode = normMode(checkMode)
|
||||
}
|
||||
if _, m := e.Modes[checkMode]; !m {
|
||||
out[i].Status = "new-mode"
|
||||
continue
|
||||
}
|
||||
if _, ok := e.Slots[out[i].Band][out[i].Mode]; !ok {
|
||||
if _, ok := e.Slots[out[i].Band][checkMode]; !ok {
|
||||
out[i].Status = "new-slot"
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user