diff --git a/build-installer.ps1 b/build-installer.ps1 index 10f565c..c607264 100644 --- a/build-installer.ps1 +++ b/build-installer.ps1 @@ -24,8 +24,12 @@ if ($LASTEXITCODE -ne 0) { throw 'publish failed' } $license = (Get-Content installer\license-notice.md -Raw) + "`n`n---`n`n" + (Get-Content LICENSE -Raw) Set-Content artifacts\license.md $license -Encoding utf8 +# The release notes go into the update feed: the app shows them before it installs the update. +$notes = "release-notes\v$Version.md" +$notesArgs = if (Test-Path $notes) { @('--releaseNotes', $notes) } else { Write-Host "No $notes; the update has no notes"; @() } + # --framework: the installer installs the .NET desktop runtime first when the PC does not have it. -dotnet vpk pack ` +dotnet vpk pack @notesArgs ` --packId OrthoStreamXP.App ` --packVersion $Version ` --packTitle OrthoStreamXP ` diff --git a/release-notes/README.md b/release-notes/README.md index 3589514..9bdc8bc 100644 --- a/release-notes/README.md +++ b/release-notes/README.md @@ -4,6 +4,9 @@ One Markdown file per release, named after its tag: `v1.2.0.md` for the tag `v1. When the tag is pushed, the [release workflow](../.github/workflows/release.yml) puts the file's text at the top of the GitHub Release. GitHub's generated list of changes (merged pull requests, new contributors) follows it. +`build-installer.ps1` also packs the file into the update feed: the app shows it in its update window before +installing (headings, bullets, paragraphs and **bold**; links show their text only). For an update packed without +notes, the app reads them from the GitHub Release instead, up to GitHub's generated "What's Changed" list. ## Releasing diff --git a/release-notes/v1.2.3.md b/release-notes/v1.2.3.md new file mode 100644 index 0000000..9bf733a --- /dev/null +++ b/release-notes/v1.2.3.md @@ -0,0 +1,9 @@ +## What's new + +- **Updates install only when you choose.** When a new version is ready, the banner on the **Home** page stays until you act on it. Closing and reopening OrthoStreamXP no longer installs the update on its own. +- **See what's new before updating.** **Update…** on the banner opens a window with the new version's release notes. Choose **Update and restart** to install it now, or **Not now** to keep the version you have. + +## Good to know + +- This is the last update that may install by itself when OrthoStreamXP starts, because the version you have now still works the old way. From 1.2.3 on, updates wait for you. +- As before, updates can only be installed while OrthoStreamXP is stopped, and your settings, cache and flight history stay. diff --git a/src/OrthoStreamXP/Gui/Home/ReleaseNotes.cs b/src/OrthoStreamXP/Gui/Home/ReleaseNotes.cs new file mode 100644 index 0000000..1c68bbb --- /dev/null +++ b/src/OrthoStreamXP/Gui/Home/ReleaseNotes.cs @@ -0,0 +1,111 @@ +using System.Text; + +namespace OrthoStreamXP.Gui; + +/// What a line of the release notes is. +internal enum NoteKind +{ + Heading, + Bullet, + Paragraph, +} + +/// A piece of a line's text, bold or not. +internal sealed record NoteSpan(string Text, bool Bold); + +/// One heading, bullet or paragraph of the release notes. +internal sealed record NoteBlock(NoteKind Kind, IReadOnlyList Spans) +{ + public string Text => string.Concat(Spans.Select(s => s.Text)); +} + +/// +/// Turns the release notes' Markdown (release-notes/vX.Y.Z.md: headings, bullets, paragraphs, bold) into blocks the update +/// window shows. Links keep their text, inline code its content; anything else is shown as written. +/// +internal static class ReleaseNotes +{ + public static IReadOnlyList Parse(string? markdown) + { + var blocks = new List(); + if (string.IsNullOrWhiteSpace(markdown)) return blocks; + + var paragraph = new StringBuilder(); + void EndParagraph() + { + if (paragraph.Length == 0) return; + blocks.Add(new NoteBlock(NoteKind.Paragraph, Inline(paragraph.ToString()))); + paragraph.Clear(); + } + + foreach (string raw in markdown.ReplaceLineEndings("\n").Split('\n')) + { + string line = raw.Trim(); + if (line.Length == 0 || IsRule(line)) + { + EndParagraph(); + } + else if (line.StartsWith('#')) + { + EndParagraph(); + blocks.Add(new NoteBlock(NoteKind.Heading, Inline(line.TrimStart('#').Trim()))); + } + else if (line.Length > 1 && line[0] is '-' or '*' or '+' && line[1] == ' ') + { + EndParagraph(); + blocks.Add(new NoteBlock(NoteKind.Bullet, Inline(line[2..].Trim()))); + } + else if (blocks.Count > 0 && blocks[^1].Kind == NoteKind.Bullet && paragraph.Length == 0 && char.IsWhiteSpace(raw[0])) + { + // An indented line goes on with the bullet above it. + var bullet = blocks[^1]; + blocks[^1] = bullet with { Spans = [.. bullet.Spans, .. Inline(" " + line)] }; + } + else + { + if (paragraph.Length > 0) paragraph.Append(' '); + paragraph.Append(line); + } + } + EndParagraph(); + return blocks; + } + + /// A horizontal rule: ---, *** or ___. + private static bool IsRule(string line) => line.Length >= 3 && line[0] is '-' or '*' or '_' && line.All(c => c == line[0]); + + /// Splits a line at its **bold** markers, and drops the link and code markup. + private static List Inline(string text) + { + var spans = new List(); + string[] parts = Plain(text).Split("**"); + for (int i = 0; i < parts.Length; i++) + { + // An unclosed ** stays as written. + bool bold = i % 2 == 1 && i < parts.Length - 1; + string part = i % 2 == 1 && !bold ? "**" + parts[i] : parts[i]; + if (part.Length > 0) spans.Add(new NoteSpan(part, bold)); + } + return spans; + } + + /// [text](url) becomes text, `code` becomes code. + private static string Plain(string text) + { + var plain = new StringBuilder(text.Length); + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '[' && text.IndexOf(']', i) is var close and > 0 && close + 1 < text.Length && text[close + 1] == '(' + && text.IndexOf(')', close) is var end and > 0) + { + plain.Append(text, i + 1, close - i - 1); + i = end; + } + else if (text[i] != '`') + { + plain.Append(text[i]); + } + } + return plain.ToString(); + } +} diff --git a/src/OrthoStreamXP/Gui/Home/UpdateOffer.cs b/src/OrthoStreamXP/Gui/Home/UpdateOffer.cs new file mode 100644 index 0000000..bae0dc6 --- /dev/null +++ b/src/OrthoStreamXP/Gui/Home/UpdateOffer.cs @@ -0,0 +1,4 @@ +namespace OrthoStreamXP.Gui; + +/// A downloaded new version, as the update window shows it before the person installs it. +internal sealed record UpdateOffer(string Version, string? CurrentVersion, IReadOnlyList Notes, string ReleasePage); diff --git a/src/OrthoStreamXP/Gui/MainWindow.xaml.cs b/src/OrthoStreamXP/Gui/MainWindow.xaml.cs index dfcc866..d4b82b3 100644 --- a/src/OrthoStreamXP/Gui/MainWindow.xaml.cs +++ b/src/OrthoStreamXP/Gui/MainWindow.xaml.cs @@ -13,6 +13,11 @@ public MainWindow(MainViewModel main) _main = main; DataContext = main; InitializeComponent(); + // The Home page's Update button: the release notes first, installed only when confirmed. + _main.Home.UpdateOffered += (_, offer) => + { + if (new UpdateDialog(offer) { Owner = this }.ShowDialog() == true) _main.Home.InstallConfirmedUpdate(); + }; } /// Closing while the scenery is mounted asks first, then unmounts before the window goes. diff --git a/src/OrthoStreamXP/Gui/ViewModels/HomeViewModel.cs b/src/OrthoStreamXP/Gui/ViewModels/HomeViewModel.cs index bbcb12c..a8726b8 100644 --- a/src/OrthoStreamXP/Gui/ViewModels/HomeViewModel.cs +++ b/src/OrthoStreamXP/Gui/ViewModels/HomeViewModel.cs @@ -45,8 +45,8 @@ public HomeViewModel(MainViewModel main) ShowReviewOf(null); }); _timer.Tick += (_, _) => Update(); - // Installed only when chosen, and only while the scenery is not streaming: OrthoStreamXP restarts to do it. - InstallUpdate = new RelayCommand(_updates.ApplyAndRestart, () => _updates.Ready is not null && Mount.State == MountState.Stopped); + // Installed only when chosen (after the release notes), and only while the scenery is not streaming: OrthoStreamXP restarts to do it. + InstallUpdate = new RelayCommand(OfferUpdate, () => _updates.Ready is not null && Mount.State == MountState.Stopped); _updateTimer.Tick += async (_, _) => { _updateTimer.Interval = TimeSpan.FromHours(6); @@ -97,8 +97,23 @@ public HomeViewModel(MainViewModel main) public ICommand OpenLog { get; } public ICommand RestartMount { get; } - /// Installs the downloaded version and restarts (only while the streaming is stopped). + /// Shows the downloaded version's release notes, to install it from there (only while the streaming is stopped). public ICommand InstallUpdate { get; } + + /// The person asked to install the downloaded version: the window shows its notes and asks to confirm. + public event EventHandler? UpdateOffered; + + private void OfferUpdate() + { + if (_updates.ReadyVersion is not { } version) return; + UpdateOffered?.Invoke(this, new UpdateOffer(version, _updates.CurrentVersion, ReleaseNotes.Parse(_updates.ReadyNotes), AppUpdates.ReleasePage(version))); + } + + /// Installs the downloaded version and restarts, once confirmed; not while the scenery streams. + public void InstallConfirmedUpdate() + { + if (Mount.State == MountState.Stopped) _updates.ApplyAndRestart(); + } public ICommand ApplyReview { get; } public ICommand DismissReview { get; } @@ -535,8 +550,8 @@ private List BuildNotices() if (_updates.ReadyVersion is { } version) notices.Add(Mount.State == MountState.Stopped ? new Notice(Severity.Info, $"OrthoStreamXP {version} is ready to install", - $"A new version was downloaded (you have {_updates.CurrentVersion}). OrthoStreamXP restarts to install it; your settings, cache and flight history stay.", - "Update and restart", InstallUpdate) + $"A new version was downloaded (you have {_updates.CurrentVersion}). See what's new and install it when you choose; your settings, cache and flight history stay.", + "Update…", InstallUpdate) : new Notice(Severity.Info, $"OrthoStreamXP {version} is ready to install", $"A new version was downloaded (you have {_updates.CurrentVersion}). Stop the streaming when you have finished flying, and install it from here.")); if (_xplaneProblem is not null) notices.Add(_xplaneProblem); diff --git a/src/OrthoStreamXP/Gui/Views/UpdateDialog.xaml b/src/OrthoStreamXP/Gui/Views/UpdateDialog.xaml new file mode 100644 index 0000000..07de185 --- /dev/null +++ b/src/OrthoStreamXP/Gui/Views/UpdateDialog.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + +