up
This commit is contained in:
@@ -2141,9 +2141,14 @@ func bandForHz(hz int64) string {
|
||||
// the operator assigns by hand. Such awards are read-only in the per-QSO editor.
|
||||
func isComputedAwardField(field string) bool {
|
||||
switch field {
|
||||
case "dxcc", "cqz", "ituz", "prefix", "callsign", "state", "cont", "country", "grid":
|
||||
// Purely derived from the callsign / cty.dat — never assigned by hand.
|
||||
case "dxcc", "cqz", "ituz", "prefix", "callsign", "cont", "country", "grid":
|
||||
return true
|
||||
}
|
||||
// NB: "state" and "cnty" are deliberately NOT computed. They are QSO fields
|
||||
// the operator often sets by hand (a QRZ lookup rarely fills the JA
|
||||
// prefecture or VE province), and they drive predefined-list awards
|
||||
// (WAS / RAC / WAJA / JCC). So they must be pickable in the per-QSO editor.
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2326,6 +2331,145 @@ func (a *App) HasBuiltinReferences(code string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// ── Award export / import ──────────────────────────────────────────────
|
||||
//
|
||||
// A self-contained JSON bundle of every award definition AND its reference
|
||||
// list. This is the backup users need so a reinstall / PC change never loses
|
||||
// the awards they built by hand. It is independent of the database file.
|
||||
|
||||
// AwardBundle is the on-disk format for an award export.
|
||||
type AwardBundle struct {
|
||||
Version int `json:"version"`
|
||||
ExportedAt string `json:"exported_at"`
|
||||
Awards []AwardBundleEntry `json:"awards"`
|
||||
}
|
||||
|
||||
// AwardBundleEntry pairs one award definition with its full reference list.
|
||||
type AwardBundleEntry struct {
|
||||
Def award.Def `json:"def"`
|
||||
References []awardref.Ref `json:"references"`
|
||||
}
|
||||
|
||||
// AwardImportResult summarises an award import for the UI.
|
||||
type AwardImportResult struct {
|
||||
Awards int `json:"awards"` // definitions added or updated
|
||||
References int `json:"references"` // references imported across all awards
|
||||
}
|
||||
|
||||
// ExportAwards shows a Save dialog and writes every award definition plus its
|
||||
// reference list to a JSON bundle. Returns the path written, or "" if the user
|
||||
// cancelled.
|
||||
func (a *App) ExportAwards() (string, error) {
|
||||
if a.awardRefs == nil {
|
||||
return "", fmt.Errorf("db not initialized")
|
||||
}
|
||||
defs := a.awardDefs()
|
||||
bundle := AwardBundle{
|
||||
Version: 1,
|
||||
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Awards: make([]AwardBundleEntry, 0, len(defs)),
|
||||
}
|
||||
for _, d := range defs {
|
||||
refs, err := a.awardRefs.List(a.ctx, d.Code)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("list references for %s: %w", d.Code, err)
|
||||
}
|
||||
bundle.Awards = append(bundle.Awards, AwardBundleEntry{Def: d, References: refs})
|
||||
}
|
||||
data, err := json.MarshalIndent(bundle, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path, err := wruntime.SaveFileDialog(a.ctx, wruntime.SaveDialogOptions{
|
||||
Title: "Export awards",
|
||||
DefaultFilename: "OpsLog_awards_" + time.Now().UTC().Format("20060102_150405") + ".json",
|
||||
Filters: []wruntime.FileFilter{
|
||||
{DisplayName: "Award bundle (*.json)", Pattern: "*.json"},
|
||||
{DisplayName: "All files (*.*)", Pattern: "*.*"},
|
||||
},
|
||||
})
|
||||
if err != nil || path == "" {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return "", fmt.Errorf("write %s: %w", path, err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// ImportAwards shows an Open dialog, reads an award bundle and merges it:
|
||||
// definitions are upserted by code, and any entry that carries references
|
||||
// replaces that award's list. Returns counts; the user cancelling yields a
|
||||
// zero result and no error.
|
||||
func (a *App) ImportAwards() (AwardImportResult, error) {
|
||||
var res AwardImportResult
|
||||
if a.awardRefs == nil || a.settings == nil {
|
||||
return res, fmt.Errorf("db not initialized")
|
||||
}
|
||||
path, err := wruntime.OpenFileDialog(a.ctx, wruntime.OpenDialogOptions{
|
||||
Title: "Import awards",
|
||||
Filters: []wruntime.FileFilter{
|
||||
{DisplayName: "Award bundle (*.json)", Pattern: "*.json"},
|
||||
{DisplayName: "All files (*.*)", Pattern: "*.*"},
|
||||
},
|
||||
})
|
||||
if err != nil || path == "" {
|
||||
return res, err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
var bundle AwardBundle
|
||||
if err := json.Unmarshal(data, &bundle); err != nil {
|
||||
return res, fmt.Errorf("parse award bundle: %w", err)
|
||||
}
|
||||
if len(bundle.Awards) == 0 {
|
||||
return res, fmt.Errorf("no awards in file")
|
||||
}
|
||||
|
||||
// Merge definitions: upsert by code (imported wins), keep the rest.
|
||||
defs := a.awardDefs()
|
||||
byCode := map[string]int{}
|
||||
for i, d := range defs {
|
||||
byCode[strings.ToUpper(d.Code)] = i
|
||||
}
|
||||
for _, e := range bundle.Awards {
|
||||
code := strings.ToUpper(strings.TrimSpace(e.Def.Code))
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
if i, ok := byCode[code]; ok {
|
||||
defs[i] = e.Def
|
||||
} else {
|
||||
byCode[code] = len(defs)
|
||||
defs = append(defs, e.Def)
|
||||
}
|
||||
res.Awards++
|
||||
}
|
||||
if migrated, changed := award.Migrate(defs); changed {
|
||||
defs = migrated
|
||||
}
|
||||
b, _ := json.Marshal(defs)
|
||||
if err := a.settings.Set(a.ctx, keyAwardDefs, string(b)); err != nil {
|
||||
return res, fmt.Errorf("save award defs: %w", err)
|
||||
}
|
||||
|
||||
// Replace reference lists for entries that carry them (skip empty so we
|
||||
// don't wipe built-in-seeded lists for a def exported without refs).
|
||||
for _, e := range bundle.Awards {
|
||||
if len(e.References) == 0 {
|
||||
continue
|
||||
}
|
||||
n, err := a.ReplaceAwardReferences(e.Def.Code, e.References)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("import references for %s: %w", e.Def.Code, err)
|
||||
}
|
||||
res.References += n
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// builtinRefsVersion is bumped whenever the built-in reference data changes
|
||||
// (e.g. the West Malaysia 155→299 fix) so existing installs re-seed the
|
||||
// derived lists. Bump this after correcting BuiltinRefs / the DXCC name table.
|
||||
@@ -2683,6 +2827,14 @@ func (a *App) SaveADIFFile() (string, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// ADIFFields returns the complete ADIF 3.1.7 QSO-field dictionary, for the
|
||||
// generic "ADIF fields" editor (so any standard field can be viewed/edited)
|
||||
// and for the export-mode help text.
|
||||
func (a *App) ADIFFields() []adif.FieldDef { return adif.Fields }
|
||||
|
||||
// ADIFVersion returns the ADIF spec version OpsLog conforms to (e.g. "3.1.7").
|
||||
func (a *App) ADIFVersion() string { return adif.ADIFVersion() }
|
||||
|
||||
// ExportADIF writes every QSO to the given file path in ADIF 3.1 format.
|
||||
// Streams from DB so memory stays flat even with 100k+ records.
|
||||
// includeAppFields=false → portable standard ADIF (for other loggers);
|
||||
|
||||
Reference in New Issue
Block a user