diff --git a/src/PlanViewer.App/AboutWindow.axaml b/src/PlanViewer.App/AboutWindow.axaml
index 97d2223..71950bb 100644
--- a/src/PlanViewer.App/AboutWindow.axaml
+++ b/src/PlanViewer.App/AboutWindow.axaml
@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PlanViewer.App.AboutWindow"
Title="About Performance Studio"
- Width="450" Height="460"
+ Width="450" Height="500"
CanResize="False"
WindowStartupLocation="CenterOwner"
Icon="avares://PlanViewer.App/EDD.ico"
@@ -44,6 +44,17 @@
Text="www.erikdarling.com"
PointerPressed="DarlingDataLink_Click"
TextDecorations="Underline"/>
+
+
+
+
+
diff --git a/src/PlanViewer.App/AboutWindow.axaml.cs b/src/PlanViewer.App/AboutWindow.axaml.cs
index 66a487e..9ed0f3f 100644
--- a/src/PlanViewer.App/AboutWindow.axaml.cs
+++ b/src/PlanViewer.App/AboutWindow.axaml.cs
@@ -15,6 +15,7 @@
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using PlanViewer.App.Mcp;
+using PlanViewer.App.Services;
namespace PlanViewer.App;
@@ -72,6 +73,41 @@ private async void CopyMcpCommand_Click(object? sender, RoutedEventArgs e)
}
}
+ private string? _updateUrl;
+
+ private async void CheckUpdate_Click(object? sender, RoutedEventArgs e)
+ {
+ CheckUpdateButton.IsEnabled = false;
+ UpdateStatusText.Text = "Checking...";
+ UpdateLink.IsVisible = false;
+
+ var currentVersion = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0);
+ var result = await UpdateChecker.CheckAsync(currentVersion);
+
+ if (result.Error != null)
+ {
+ UpdateStatusText.Text = $"Error: {result.Error}";
+ }
+ else if (result.UpdateAvailable)
+ {
+ UpdateStatusText.Text = $"New version available:";
+ UpdateLink.Text = result.LatestVersion;
+ UpdateLink.IsVisible = true;
+ _updateUrl = result.ReleaseUrl;
+ }
+ else
+ {
+ UpdateStatusText.Text = $"You're up to date ({result.LatestVersion})";
+ }
+
+ CheckUpdateButton.IsEnabled = true;
+ }
+
+ private void UpdateLink_Click(object? sender, PointerPressedEventArgs e)
+ {
+ if (_updateUrl != null) OpenUrl(_updateUrl);
+ }
+
private void CloseButton_Click(object? sender, RoutedEventArgs e) => Close();
private static void OpenUrl(string url)
diff --git a/src/PlanViewer.App/Services/UpdateChecker.cs b/src/PlanViewer.App/Services/UpdateChecker.cs
new file mode 100644
index 0000000..df0e343
--- /dev/null
+++ b/src/PlanViewer.App/Services/UpdateChecker.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace PlanViewer.App.Services;
+
+public record UpdateCheckResult(bool UpdateAvailable, string? LatestVersion, string? ReleaseUrl, string? Error);
+
+public static class UpdateChecker
+{
+ private const string ReleasesApiUrl =
+ "https://api.github.com/repos/erikdarlingdata/PerformanceStudio/releases/latest";
+
+ private static readonly HttpClient Http = new()
+ {
+ DefaultRequestHeaders =
+ {
+ { "User-Agent", "PerformanceStudio-UpdateCheck" },
+ { "Accept", "application/vnd.github+json" }
+ },
+ Timeout = TimeSpan.FromSeconds(10)
+ };
+
+ public static async Task CheckAsync(Version currentVersion)
+ {
+ try
+ {
+ var json = await Http.GetStringAsync(ReleasesApiUrl);
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+
+ var tagName = root.GetProperty("tag_name").GetString();
+ var htmlUrl = root.GetProperty("html_url").GetString();
+
+ if (string.IsNullOrEmpty(tagName))
+ return new UpdateCheckResult(false, null, null, "No release tag found");
+
+ // Strip leading 'v' from tag (e.g. "v0.9.0" -> "0.9.0")
+ var versionStr = tagName.StartsWith('v') ? tagName[1..] : tagName;
+
+ if (!Version.TryParse(versionStr, out var latestVersion))
+ return new UpdateCheckResult(false, tagName, htmlUrl, $"Could not parse version: {tagName}");
+
+ var updateAvailable = latestVersion > currentVersion;
+ return new UpdateCheckResult(updateAvailable, tagName, htmlUrl, null);
+ }
+ catch (Exception ex)
+ {
+ return new UpdateCheckResult(false, null, null, ex.Message);
+ }
+ }
+}