From 9033e8518c644119f8d81b8c6248184f93c63336 Mon Sep 17 00:00:00 2001 From: rouggy Date: Mon, 20 Jul 2026 22:58:05 +0200 Subject: [PATCH] feat: What's-new changelog dialog on first launch after an update (EN/FR) Embeds changelog.json (per-version EN + FR notes, curated from the release's commits). GetWhatsNew compares the stored last-seen version with the running build and returns the notes for every newer version, once, then advances the marker. A dialog shows them in the UI language on the first launch after update. Seeded with the 0.20.5 notes. --- changelog.go | 67 +++++++++++++++++++++++++++++++ changelog.json | 32 +++++++++++++++ frontend/src/App.tsx | 47 +++++++++++++++++++++- frontend/src/lib/i18n.tsx | 4 +- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 ++ frontend/wailsjs/go/models.ts | 18 +++++++++ 7 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 changelog.go create mode 100644 changelog.json diff --git a/changelog.go b/changelog.go new file mode 100644 index 0000000..f6de5cf --- /dev/null +++ b/changelog.go @@ -0,0 +1,67 @@ +package main + +import ( + _ "embed" + "encoding/json" + "strings" + + "hamlog/internal/applog" +) + +//go:embed changelog.json +var changelogJSON []byte + +// ChangelogEntry is one release's user-facing notes, in English and French. It's +// shown once on the first launch after an update (the "What's new" dialog), +// derived from the release's commit history. +type ChangelogEntry struct { + Version string `json:"version"` + Date string `json:"date"` + EN []string `json:"en"` + FR []string `json:"fr"` +} + +const keyChangelogSeen = "changelog.last_seen_version" + +func loadChangelog() []ChangelogEntry { + var out []ChangelogEntry + if err := json.Unmarshal(changelogJSON, &out); err != nil { + applog.Printf("changelog: parse failed: %v", err) + } + return out +} + +// GetWhatsNew returns the changelog entries the operator hasn't seen yet — the +// notes for every version newer than the last one they ran, up to the current +// build — and records the current version as seen so it pops exactly once per +// update. Empty when there's nothing new. +func (a *App) GetWhatsNew() []ChangelogEntry { + if a.settings == nil { + return nil + } + lastSeen, _ := a.settings.GetGlobal(a.ctx, keyChangelogSeen) + cur := appVersion + a.setSettingGlobal(keyChangelogSeen, cur) // advance the marker now + + out := []ChangelogEntry{} + for _, e := range loadChangelog() { + if strings.TrimSpace(e.Version) == "" { + continue + } + if versionLess(cur, e.Version) { + continue // don't preview notes for a version newer than we run + } + if lastSeen == "" { + // First run of the feature (fresh install, or upgrade from a build that + // predates it): show only the CURRENT version's notes, once. + if e.Version == cur { + out = append(out, e) + } + continue + } + if versionLess(lastSeen, e.Version) { + out = append(out, e) + } + } + return out +} diff --git a/changelog.json b/changelog.json new file mode 100644 index 0000000..c718e15 --- /dev/null +++ b/changelog.json @@ -0,0 +1,32 @@ +[ + { + "version": "0.20.5", + "date": "2026-07-20", + "en": [ + "New FlexRadio CWX CW keyer: key CW straight over the SmartSDR connection — no WinKeyer or SmartCAT needed. Type-ahead and mid-send backspace supported.", + "ESC now stops CW on whichever keyer is active (WinKeyer / Icom / Flex), and typing a callsign aborts a running macro.", + "New Amplifier settings section (was 'Power Genius'): control SPE Expert amps (1.3K / 1.5K / 2K) over USB serial or an RS232-to-Ethernet bridge — OPERATE/STANDBY and live status. PowerGenius XL still supported.", + "Denkovi USB relay boards supported (4/8 relays, FT245), plus generic CH340/LCUS USB-serial relay boards.", + "Rename your database from Settings without losing any configuration.", + "Awards: DDFM now finds the French department from the address postal code; refs are stored on each QSO for faster columns; 'Publish to catalog' shares an edited award with your team on the next release; award columns are remembered per profile.", + "Statistics: per-band mode split (CW / phone / data), and an activity chart with a Day / Week / Month / Year selector (rolling, real dates).", + "'Stations on air' widget updates within seconds and fits more stations.", + "The audio CW decoder was removed.", + "Fixes: Antenna Genius buttons ignored clicks while a macro was sending; File → Exit; and more.", + "This 'What's new' summary now appears on the first launch after each update." + ], + "fr": [ + "Nouveau keyer CW FlexRadio CWX : manipulation CW directement via la connexion SmartSDR — plus besoin de WinKeyer ni de SmartCAT. Type-ahead et retour arrière en cours d'envoi supportés.", + "ESC arrête maintenant le CW sur le moteur actif (WinKeyer / Icom / Flex), et taper un indicatif interrompt une macro en cours.", + "Nouvelle section Amplificateur (ex « Power Genius ») : pilotage des SPE Expert (1.3K / 1.5K / 2K) en USB série ou via un pont RS232→Ethernet — OPERATE/STANDBY et état en direct. PowerGenius XL toujours supporté.", + "Cartes relais USB Denkovi (4/8 relais, FT245), plus cartes USB-série génériques CH340/LCUS.", + "Renommer sa base depuis les réglages sans perdre la configuration.", + "Awards : DDFM trouve le département français depuis le code postal de l'adresse ; les réfs sont stockées sur chaque QSO (colonnes plus rapides) ; « Publier au catalogue » partage un award édité avec l'équipe à la prochaine mise à jour ; les colonnes d'award sont mémorisées par profil.", + "Statistiques : répartition des modes par bande (CW / phone / data), et un graphe d'activité avec sélecteur Jour / Semaine / Mois / Année (glissant, dates réelles).", + "Le widget « Stations on air » se met à jour en quelques secondes et affiche plus de stations.", + "Le décodeur CW audio a été retiré.", + "Corrections : les boutons Antenna Genius ignoraient les clics pendant l'envoi d'une macro ; Fichier → Quitter ; et d'autres.", + "Ce résumé « Nouveautés » s'affiche désormais au premier lancement après chaque mise à jour." + ] + } +] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 55b51c1..213f274 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,7 +13,7 @@ import { GetQSO, UpdateQSO, DeleteQSO, DeleteQSOs, DeleteAllQSO, UpdateQSOsFromCty, UpdateQSOsFromQRZ, UpdateQSOsFromClublog, UploadQSOsManual, SendQSORecordingEmail, LookupCallsign, GetStationSettings, GetListsSettings, - GetStartupStatus, CheckForUpdate, DownloadAndApplyUpdate, GetLiveStations, + GetStartupStatus, CheckForUpdate, DownloadAndApplyUpdate, GetLiveStations, GetWhatsNew, WorkedBefore, SetCompactMode, GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, FlexApplyBandAntenna, @@ -283,7 +283,7 @@ function computePrefix(call: string): string { } export default function App() { - const { t } = useI18n(); + const { t, lang } = useI18n(); // === Lists from settings (fallback for first paint) === const [bands, setBands] = useState(DEFAULT_BANDS); const [modes, setModes] = useState(DEFAULT_MODES); @@ -1147,6 +1147,12 @@ export default function App() { const [settingsSection, setSettingsSection] = useState(undefined); const [showDeleteAll, setShowDeleteAll] = useState(false); const [showAbout, setShowAbout] = useState(false); + // "What's new": the changelog for the version(s) since the operator last ran, + // shown once on the first launch after an update (EN/FR per the UI language). + const [whatsNew, setWhatsNew] = useState | null>(null); + useEffect(() => { + GetWhatsNew().then((e: any) => { if (Array.isArray(e) && e.length) setWhatsNew(e); }).catch(() => {}); + }, []); const [showDuplicates, setShowDuplicates] = useState(false); const [updateInfo, setUpdateInfo] = useState<{ latest: string; url: string; downloadUrl: string } | null>(null); const [updating, setUpdating] = useState(false); @@ -4132,6 +4138,43 @@ export default function App() { )} + {/* What's new — shown once on the first launch after an update. */} + {whatsNew && ( +
setWhatsNew(null)}> +
e.stopPropagation()}> +
+ +

{t('whatsnew.title')}

+ + +
+
+ {whatsNew.map((e) => ( +
+
+ v{e.version} + {e.date && {e.date}} +
+
    + {(lang === 'fr' ? e.fr : e.en).map((line, i) => ( +
  • + + {line} +
  • + ))} +
+
+ ))} +
+
+ +
+
+
+ )} + {/* Transient toasts (bottom-right). Errors stack on top of the green success toast; both auto-dismiss. */} {/* Unlock encrypted passwords (set via Settings → Security). Dismissable: diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index d81c3c4..46014a6 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -54,7 +54,7 @@ const en: Dict = { 'btn.logQso': 'Log QSO', 'btn.clear': 'Clear', 'btn.spot': 'Spot', 'btn.saving': '…', // Language chooser 'lang.choose': 'Choose your language', 'lang.chooseHint': 'You can change this later in Settings → General.', - 'lang.english': 'English', 'lang.french': 'Français', + 'lang.english': 'English', 'lang.french': 'Français', 'whatsnew.title': "What's new", 'whatsnew.close': 'Got it', 'settings.language': 'Language', 'settings.languageHint': 'Interface language.', 'stats.tab': 'Statistics', 'stats.title': 'Logbook statistics', 'stats.loading': 'Crunching the log…', 'stats.noData': 'No data', 'stats.charts': 'Charts', 'stats.table': 'Table', 'stats.refresh': 'Refresh', @@ -368,7 +368,7 @@ const fr: Dict = { 'field.startUtc': 'Début UTC', 'field.endUtc': 'Fin UTC', 'field.snt': 'Env', 'field.rcv': 'Reç', 'btn.logQso': 'Enregistrer', 'btn.clear': 'Effacer', 'btn.spot': 'Spot', 'btn.saving': '…', 'lang.choose': 'Choisissez votre langue', 'lang.chooseHint': 'Modifiable plus tard dans Réglages → Général.', - 'lang.english': 'English', 'lang.french': 'Français', + 'lang.english': 'English', 'lang.french': 'Français', 'whatsnew.title': 'Nouveautés', 'whatsnew.close': 'Compris', 'settings.language': 'Langue', 'settings.languageHint': "Langue de l'interface.", 'stats.tab': 'Statistiques', 'stats.title': 'Statistiques du journal', 'stats.loading': 'Analyse du journal…', 'stats.noData': 'Aucune donnée', 'stats.charts': 'Graphiques', 'stats.table': 'Tableau', 'stats.refresh': 'Rafraîchir', diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 7580fdb..6c62e48 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -427,6 +427,8 @@ export function GetUltrabeamSettings():Promise; export function GetUltrabeamStatus():Promise; +export function GetWhatsNew():Promise>; + export function GetWinkeyerSettings():Promise; export function GetWinkeyerStatus():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 5cd8100..29ebbb1 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -810,6 +810,10 @@ export function GetUltrabeamStatus() { return window['go']['main']['App']['GetUltrabeamStatus'](); } +export function GetWhatsNew() { + return window['go']['main']['App']['GetWhatsNew'](); +} + export function GetWinkeyerSettings() { return window['go']['main']['App']['GetWinkeyerSettings'](); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 94fe425..1af2b98 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1728,6 +1728,24 @@ export namespace main { this.path = source["path"]; } } + export class ChangelogEntry { + version: string; + date: string; + en: string[]; + fr: string[]; + + static createFrom(source: any = {}) { + return new ChangelogEntry(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.version = source["version"]; + this.date = source["date"]; + this.en = source["en"]; + this.fr = source["fr"]; + } + } export class ChatMessage { id: number; operator: string;