Qsl
This commit is contained in:
+101
-9
@@ -31,6 +31,7 @@ import (
|
||||
const (
|
||||
keyQSLEmailSubject = "qsl.email_subject"
|
||||
keyQSLEmailBody = "qsl.email_body"
|
||||
keyQSLAutoSend = "qsl.auto_send" // "1" → render+send an eQSL on log when an e-mail and default template exist
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -73,7 +74,7 @@ type QSLPresetInfo struct {
|
||||
// so picking through the native dialog is the reliable route to real paths).
|
||||
func (a *App) QSLPickPhotos() ([]string, error) {
|
||||
return wruntime.OpenMultipleFilesDialog(a.ctx, wruntime.OpenDialogOptions{
|
||||
Title: "Choose card photos (1–6)",
|
||||
Title: "Choose card photos (1–3)",
|
||||
Filters: []wruntime.FileFilter{
|
||||
{DisplayName: "Photos (*.jpg;*.jpeg;*.png)", Pattern: "*.jpg;*.jpeg;*.png"},
|
||||
},
|
||||
@@ -86,8 +87,8 @@ func (a *App) QSLGenerateProposals(photoPaths []string) ([]string, error) {
|
||||
if len(photoPaths) == 0 {
|
||||
return nil, fmt.Errorf("no photos selected")
|
||||
}
|
||||
if len(photoPaths) > 6 {
|
||||
return nil, fmt.Errorf("at most 6 photos (got %d)", len(photoPaths))
|
||||
if len(photoPaths) > 3 {
|
||||
return nil, fmt.Errorf("at most 3 photos (got %d)", len(photoPaths))
|
||||
}
|
||||
photos := make([]qslcard.PhotoAnalysis, 0, len(photoPaths))
|
||||
for _, p := range photoPaths {
|
||||
@@ -433,10 +434,11 @@ func (a *App) SendEQSL(qsoID int64, templateID int64, jpegB64 string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// QSLEmailTemplates is the subject/body pair of the eQSL e-mail.
|
||||
// QSLEmailTemplates is the eQSL e-mail subject/body plus the auto-send toggle.
|
||||
type QSLEmailTemplates struct {
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
AutoSend bool `json:"auto_send"`
|
||||
}
|
||||
|
||||
// QSLGetEmailTemplates returns the eQSL e-mail templates (with defaults).
|
||||
@@ -445,7 +447,7 @@ func (a *App) QSLGetEmailTemplates() (QSLEmailTemplates, error) {
|
||||
if a.settings == nil {
|
||||
return out, nil
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx, keyQSLEmailSubject, keyQSLEmailBody)
|
||||
m, err := a.settings.GetMany(a.ctx, keyQSLEmailSubject, keyQSLEmailBody, keyQSLAutoSend)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -455,10 +457,11 @@ func (a *App) QSLGetEmailTemplates() (QSLEmailTemplates, error) {
|
||||
if b := m[keyQSLEmailBody]; b != "" {
|
||||
out.Body = b
|
||||
}
|
||||
out.AutoSend = m[keyQSLAutoSend] == "1"
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// QSLSaveEmailTemplates persists the eQSL e-mail templates.
|
||||
// QSLSaveEmailTemplates persists the eQSL e-mail templates and auto-send flag.
|
||||
func (a *App) QSLSaveEmailTemplates(t QSLEmailTemplates) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
@@ -466,7 +469,43 @@ func (a *App) QSLSaveEmailTemplates(t QSLEmailTemplates) error {
|
||||
if err := a.settings.Set(a.ctx, keyQSLEmailSubject, t.Subject); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.settings.Set(a.ctx, keyQSLEmailBody, t.Body)
|
||||
if err := a.settings.Set(a.ctx, keyQSLEmailBody, t.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
v := "0"
|
||||
if t.AutoSend {
|
||||
v = "1"
|
||||
}
|
||||
return a.settings.Set(a.ctx, keyQSLAutoSend, v)
|
||||
}
|
||||
|
||||
// maybeAutoSendEQSL fires an eQSL render+send for a freshly-logged QSO when the
|
||||
// user enabled auto-send and the prerequisites hold: a recipient e-mail, a
|
||||
// default template for the active profile, and the QSO not already confirmed.
|
||||
// Rasterization happens in the webview, so this only emits an event ("qsl:autosend")
|
||||
// the frontend acts on — never sends from here. Silent when any condition fails.
|
||||
func (a *App) maybeAutoSendEQSL(q qso.QSO) {
|
||||
if a.settings == nil || a.qslTemplates == nil || a.profiles == nil || a.ctx == nil {
|
||||
return
|
||||
}
|
||||
if v, _ := a.settings.Get(a.ctx, keyQSLAutoSend); v != "1" {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(q.Email) == "" || strings.EqualFold(q.EQSLSent, "Y") {
|
||||
return
|
||||
}
|
||||
p, err := a.profiles.Active(a.ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rec, err := a.qslTemplates.DefaultFor(a.ctx, p.ID)
|
||||
if err != nil {
|
||||
applog.Printf("qsl: auto-send skipped for %s — no template (%v)", q.Callsign, err)
|
||||
return
|
||||
}
|
||||
wruntime.EventsEmit(a.ctx, "qsl:autosend", map[string]any{
|
||||
"qsoId": q.ID, "templateId": rec.ID, "callsign": q.Callsign,
|
||||
})
|
||||
}
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────────
|
||||
@@ -493,6 +532,8 @@ func (a *App) qslProfileInfo() (qslcard.ProfileInfo, error) {
|
||||
Callsign: p.Callsign,
|
||||
Operator: p.OpName, // personal name for the QSL script (not the OPERATOR callsign)
|
||||
Grid: p.MyGrid,
|
||||
Rig: p.MyRig,
|
||||
Antenna: p.MyAntenna,
|
||||
}
|
||||
if p.MyCQZone != nil {
|
||||
info.CQZone = *p.MyCQZone
|
||||
@@ -510,9 +551,57 @@ func (a *App) qslProfileInfo() (qslcard.ProfileInfo, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Most users define rig/antenna in the operating manager (band defaults),
|
||||
// not the profile's MY_RIG/MY_ANTENNA text fields. Fall back to a band
|
||||
// default so the designer knows the station line has data to show.
|
||||
if (info.Rig == "" || info.Antenna == "") && a.operating != nil {
|
||||
for _, band := range []string{"20m", "40m", "80m", "10m", "2m", "160m"} {
|
||||
d, _, e := a.operating.BandDefault(a.ctx, p.ID, band)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
if info.Rig == "" {
|
||||
info.Rig = d.StationName
|
||||
}
|
||||
if info.Antenna == "" {
|
||||
info.Antenna = d.AntennaName
|
||||
}
|
||||
if info.Rig != "" && info.Antenna != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// qslRigAntenna resolves the rig and antenna names for a QSO: the values
|
||||
// stamped on it (MY_RIG/MY_ANTENNA), falling back to the operating manager's
|
||||
// default for the QSO's band (so the preview and back-entered QSOs aren't
|
||||
// blank).
|
||||
func (a *App) qslRigAntenna(q qso.QSO) (rig, ant string) {
|
||||
rig, ant = q.MyRig, q.MyAntenna
|
||||
if (rig != "" && ant != "") || a.operating == nil || a.profiles == nil {
|
||||
return rig, ant
|
||||
}
|
||||
p, err := a.profiles.Active(a.ctx)
|
||||
if err != nil {
|
||||
return rig, ant
|
||||
}
|
||||
band := q.Band
|
||||
if band == "" {
|
||||
band = "20m"
|
||||
}
|
||||
if d, _, e := a.operating.BandDefault(a.ctx, p.ID, band); e == nil {
|
||||
if rig == "" {
|
||||
rig = d.StationName
|
||||
}
|
||||
if ant == "" {
|
||||
ant = d.AntennaName
|
||||
}
|
||||
}
|
||||
return rig, ant
|
||||
}
|
||||
|
||||
// qslVars builds the full placeholder map (profile + QSO) and the country
|
||||
// block resolution for one render.
|
||||
func (a *App) qslVars(q qso.QSO) (map[string]string, qslcard.CountryInfo, error) {
|
||||
@@ -532,6 +621,8 @@ func (a *App) qslVars(q qso.QSO) (map[string]string, qslcard.CountryInfo, error)
|
||||
"profile.grid": info.Grid,
|
||||
"profile.cq_zone": zone(info.CQZone),
|
||||
"profile.itu_zone": zone(info.ITUZone),
|
||||
"profile.rig": info.Rig,
|
||||
"profile.antenna": info.Antenna,
|
||||
|
||||
"qso.callsign": q.Callsign,
|
||||
"qso.qso_date": q.QSODate.UTC().Format("2006-01-02"),
|
||||
@@ -543,6 +634,7 @@ func (a *App) qslVars(q qso.QSO) (map[string]string, qslcard.CountryInfo, error)
|
||||
"qso.qsl_msg": q.QSLMsg,
|
||||
"qso.name": q.Name,
|
||||
}
|
||||
vars["qso.my_rig"], vars["qso.my_antenna"] = a.qslRigAntenna(q)
|
||||
if q.FreqHz != nil {
|
||||
vars["qso.freq"] = strconv.FormatFloat(float64(*q.FreqHz)/1e6, 'f', 3, 64) + " MHz"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user