diff --git a/Components/Modals/UpdateModalComponent.axaml b/Components/Modals/UpdateModalComponent.axaml
new file mode 100644
index 0000000..d768a0c
--- /dev/null
+++ b/Components/Modals/UpdateModalComponent.axaml
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Components/Modals/UpdateModalComponent.axaml.cs b/Components/Modals/UpdateModalComponent.axaml.cs
new file mode 100644
index 0000000..fbfd25c
--- /dev/null
+++ b/Components/Modals/UpdateModalComponent.axaml.cs
@@ -0,0 +1,21 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Quell.ViewModels;
+
+namespace Quell.Components;
+
+public partial class UpdateModalComponent : UserControl
+{
+ public UpdateModalComponent()
+ {
+ InitializeComponent();
+ }
+
+ private void OnBackdropPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (DataContext is UpdateModalViewModel vm)
+ {
+ vm.DismissCommand.Execute(null);
+ }
+ }
+}
diff --git a/Components/Navigation/StatusBarComponent.axaml b/Components/Navigation/StatusBarComponent.axaml
index 2f97597..0517c79 100644
--- a/Components/Navigation/StatusBarComponent.axaml
+++ b/Components/Navigation/StatusBarComponent.axaml
@@ -2,8 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="using:Quell.ViewModels"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="24"
- x:Class="Quell.Components.StatusBarComponent">
+ x:Class="Quell.Components.StatusBarComponent"
+ x:DataType="vm:MainViewModel">
-
-
+
+
+
+
+
+
+/// Model representing a GitHub release payload and version metadata.
+///
+public class UpdateReleaseModel
+{
+ public string TagName { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public string Body { get; set; } = string.Empty;
+ public string HtmlUrl { get; set; } = string.Empty;
+ public string PublishedAt { get; set; } = string.Empty;
+ public Version? ParsedVersion { get; set; }
+ public bool IsNewer { get; set; }
+}
diff --git a/Program.cs b/Program.cs
index e5dbfb4..a3c39e3 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1807,6 +1807,66 @@ await Quell.Services.DatabaseStorageService.Instance.UpdateConnectionAsync(
// Restore defaults
await Quell.Services.Terminal.TerminalSettingsService.Instance.SaveSettingsAsync(new Quell.Models.TerminalSettingsModel());
+ // 23. Test Application Versioning Architecture, GitHub Releases Checker & Corner "Get Updates" Box
+ Console.WriteLine("\n[Action] Verifying Semantic Versioning, GitHub Releases Service & Corner 'Get Updates' Notification...");
+
+ // 23.1 Test Version Parsing & SemVer Comparisons
+ bool p1 = Quell.Services.AppUpdateService.TryParseVersion("v1.2.0", out var v1);
+ bool p2 = Quell.Services.AppUpdateService.TryParseVersion("1.0.0-rc1", out var v2);
+ bool p3 = Quell.Services.AppUpdateService.TryParseVersion("v0.9.8", out var v3);
+ if (!p1 || v1 == null || v1 != new Version(1, 2, 0)) throw new Exception("Version parsing for v1.2.0 failed!");
+ if (!p2 || v2 == null || v2 != new Version(1, 0, 0)) throw new Exception("Version parsing for 1.0.0-rc1 failed!");
+ if (!p3 || v3 == null || v3 >= Quell.Services.AppUpdateService.CurrentVersion) throw new Exception("SemVer comparison failed: v0.9.8 should be less than current version!");
+ if (v1 <= Quell.Services.AppUpdateService.CurrentVersion) throw new Exception("SemVer comparison failed: v1.2.0 should be greater than current version!");
+ Console.WriteLine($"[Success] Semantic version parsing & comparison verified (Current: {Quell.Services.AppUpdateService.CurrentVersionString})!");
+
+ // 23.2 Test Update Modal & Corner "Get Updates" Box Lifecycle
+ vm.UpdateModal.IsOpen = false;
+ vm.UpdateModal.IsCornerBadgeVisible = false;
+
+ // Simulate newer release notification
+ Quell.Services.AppUpdateService.Instance.SimulateUpdateAvailable(
+ newVersion: "v1.1.0",
+ releaseTitle: "Quell Studio 1.1.0 - Next-Gen SQL Performance",
+ notes: "• Automatic GitHub Releases version detection & notification\n• GPU-Accelerated Fluid Modal Animations"
+ );
+ vm.UpdateModal.ApplyRelease(Quell.Services.AppUpdateService.Instance.LatestRelease!);
+ vm.UpdateModal.Open();
+
+ if (!vm.UpdateModal.IsOpen || vm.UpdateModal.IsCornerBadgeVisible)
+ {
+ throw new Exception("UpdateModal should be Open, and CornerBadge should be hidden while modal is open!");
+ }
+ if (vm.UpdateModal.LatestVersion != "v1.1.0" || !vm.UpdateModal.IsUpdateAvailable)
+ {
+ throw new Exception("UpdateModal version binding mismatch!");
+ }
+ Console.WriteLine($"[Success] New update modal auto-opened: Version={vm.UpdateModal.LatestVersion}, Title='{vm.UpdateModal.ReleaseTitle}'!");
+
+ // Simulate user dismissing / clicking (X) on the modal
+ Console.WriteLine("[Action] User dismissed update modal with (X)...");
+ vm.UpdateModal.Dismiss();
+
+ if (vm.UpdateModal.IsOpen || !vm.UpdateModal.IsCornerBadgeVisible)
+ {
+ throw new Exception("Upon modal dismissal, UpdateModal.IsOpen must be False and IsCornerBadgeVisible MUST be True!");
+ }
+ Console.WriteLine("[Success] Modal closed; Corner 'Get Updates' box is now active and visible!");
+
+ // Simulate user clicking on corner "Get Updates" box anytime later
+ Console.WriteLine("[Action] User clicked on corner 'Get Updates' box...");
+ vm.OpenUpdateModalCommand.Execute(null);
+
+ if (!vm.UpdateModal.IsOpen || vm.UpdateModal.IsCornerBadgeVisible)
+ {
+ throw new Exception("Re-opening from corner box must show UpdateModal and hide the corner badge!");
+ }
+ Console.WriteLine("[Success] Update modal successfully re-opened from corner 'Get Updates' box!");
+
+ // Reset state
+ vm.UpdateModal.Dismiss();
+ Quell.Services.AppUpdateService.Instance.Reset();
+
Console.WriteLine("\n>>> ALL TESTS (LAYOUT, TABLES, MODALS, TABS, SQL AUTOCOMPLETE, MULTI-DIALECT SQLITE, VECTOR ICONS, SPLASH SCREEN, STORAGE, DYNAMIC DATABASE PROVIDERS, ISOLATED STARTUP, HIERARCHICAL TREE VIEW, REAL QUERY, TARGET DATABASE ROUTING, DISCONNECT/DELETE, GRAPHICAL FOOTER CELL UPDATE, PRIMARY KEY NEUTRAL STYLING, MULTI-ROW BATCH DELETION, MULTI-COLUMN DROP, FOOTER REFRESH/FILTER, SIMULTANEOUS BATCH CELL EDITING, FOREIGN KEY NAVIGATION, HARDWARE GPU/CPU RENDERING PIPELINE & INTEGRATED TERMINAL) PASSED! <<<");
return;
}
diff --git a/Services/AppUpdateService.cs b/Services/AppUpdateService.cs
new file mode 100644
index 0000000..39ad275
--- /dev/null
+++ b/Services/AppUpdateService.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Quell.Models;
+
+namespace Quell.Services;
+
+///
+/// Service responsible for GitHub release versioning checks, semantic version comparisons,
+/// and update notifications across Quell Studio.
+///
+public class AppUpdateService
+{
+ private static AppUpdateService? _instance;
+ public static AppUpdateService Instance => _instance ??= new AppUpdateService();
+
+ public static readonly Version CurrentVersion = new(1, 0, 0);
+ public static string CurrentVersionString => $"v{CurrentVersion.Major}.{CurrentVersion.Minor}.{CurrentVersion.Build}";
+
+ public const string GitHubRepo = "cipherunits/Quell";
+ public const string ReleasesApiUrl = $"https://api.github.com/repos/{GitHubRepo}/releases/latest";
+ public const string DefaultReleasesPageUrl = $"https://github.com/{GitHubRepo}/releases";
+
+ private readonly HttpClient _httpClient;
+
+ public bool IsUpdateAvailable { get; private set; }
+ public bool IsCornerBadgeVisible { get; set; }
+ public UpdateReleaseModel? LatestRelease { get; private set; }
+
+ public event EventHandler? UpdateAvailable;
+
+ public AppUpdateService(HttpClient? client = null)
+ {
+ _httpClient = client ?? new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(4)
+ };
+ if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent"))
+ {
+ _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Quell-Studio", CurrentVersionString));
+ }
+ }
+
+ ///
+ /// Checks GitHub repository releases for a newer version than CurrentVersion.
+ /// Runs asynchronously and fails gracefully on network errors or timeouts.
+ ///
+ public async Task CheckForUpdatesAsync(bool isManualCheck = false)
+ {
+ try
+ {
+ using var response = await _httpClient.GetAsync(ReleasesApiUrl).ConfigureAwait(false);
+ if (!response.IsSuccessStatusCode)
+ {
+ return null;
+ }
+
+ var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+
+ string tagName = root.TryGetProperty("tag_name", out var tagElem) ? tagElem.GetString() ?? "" : "";
+ string name = root.TryGetProperty("name", out var nameElem) ? nameElem.GetString() ?? tagName : tagName;
+ string body = root.TryGetProperty("body", out var bodyElem) ? bodyElem.GetString() ?? "" : "";
+ string htmlUrl = root.TryGetProperty("html_url", out var urlElem) ? urlElem.GetString() ?? "" : DefaultReleasesPageUrl;
+ string publishedAt = root.TryGetProperty("published_at", out var pubElem) ? pubElem.GetString() ?? "" : "";
+
+ if (TryParseVersion(tagName, out var remoteVer) && remoteVer != null)
+ {
+ bool isNewer = remoteVer > CurrentVersion;
+ var release = new UpdateReleaseModel
+ {
+ TagName = tagName,
+ Name = string.IsNullOrWhiteSpace(name) ? tagName : name,
+ Body = body,
+ HtmlUrl = htmlUrl,
+ PublishedAt = publishedAt,
+ ParsedVersion = remoteVer,
+ IsNewer = isNewer
+ };
+
+ if (isNewer)
+ {
+ LatestRelease = release;
+ IsUpdateAvailable = true;
+ UpdateAvailable?.Invoke(this, release);
+ }
+
+ return release;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[AppUpdateService] CheckForUpdates error: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Parses version string from release tag name (e.g., 'v1.1.0' -> 1.1.0).
+ ///
+ public static bool TryParseVersion(string? tag, out Version? version)
+ {
+ version = null;
+ if (string.IsNullOrWhiteSpace(tag)) return false;
+
+ string clean = tag.Trim().TrimStart('v', 'V');
+ int dashIndex = clean.IndexOf('-');
+ if (dashIndex > 0) clean = clean[..dashIndex];
+
+ // Ensure 3 segments for System.Version if only 1 or 2 are present
+ int dotCount = clean.Count(c => c == '.');
+ if (dotCount == 0) clean += ".0.0";
+ else if (dotCount == 1) clean += ".0";
+
+ return Version.TryParse(clean, out version);
+ }
+
+ ///
+ /// Helper to trigger update availability (used for verification testing and offline preview).
+ ///
+ public void SimulateUpdateAvailable(string newVersion = "v1.1.0", string? releaseTitle = null, string? notes = null, string? releaseUrl = null)
+ {
+ TryParseVersion(newVersion, out var ver);
+ var release = new UpdateReleaseModel
+ {
+ TagName = newVersion,
+ Name = releaseTitle ?? $"Quell Studio {newVersion} - Performance & ER Engine Update",
+ Body = notes ?? "• Automatic GitHub Releases version detection & notification\n• Hardware GPU-accelerated fluid modal pop-in animations\n• Real-time startup sequence linked to actual sub-systems\n• Schema properties navigation & multi-dialect SQL autocomplete",
+ HtmlUrl = releaseUrl ?? DefaultReleasesPageUrl,
+ ParsedVersion = ver ?? new Version(1, 1, 0),
+ IsNewer = true
+ };
+
+ LatestRelease = release;
+ IsUpdateAvailable = true;
+ UpdateAvailable?.Invoke(this, release);
+ }
+
+ ///
+ /// Resets update detection state (e.g. for testing).
+ ///
+ public void Reset()
+ {
+ IsUpdateAvailable = false;
+ IsCornerBadgeVisible = false;
+ LatestRelease = null;
+ }
+}
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index d3dce2a..54e2005 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -42,6 +42,9 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty]
private SettingsModalViewModel _settingsModal;
+ [ObservableProperty]
+ private UpdateModalViewModel _updateModal;
+
[ObservableProperty]
private QueryResultTableViewModel _sampleQueryResult;
@@ -65,6 +68,7 @@ public MainViewModel()
_exportModal = new ExportDataModalViewModel();
_diagramModal = new DatabaseDiagramModalViewModel();
_settingsModal = new SettingsModalViewModel();
+ _updateModal = new UpdateModalViewModel();
_sampleQueryResult = new QueryResultTableViewModel();
_sampleSchema = new SchemaPropertiesTableViewModel();
@@ -218,6 +222,13 @@ private async Task InitializeStartupAsync()
await Quell.Services.DatabaseStorageService.Instance.InitializeAsync();
await RefreshAvailableDatabasesAsync();
await Sidebar.RefreshAsync();
+
+ // Check for updates asynchronously from GitHub releases (non-blocking)
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(2500).ConfigureAwait(false);
+ await AppUpdateService.Instance.CheckForUpdatesAsync().ConfigureAwait(false);
+ });
}
private bool _isUpdatingTargetSilently;
@@ -1466,6 +1477,12 @@ private List GetSampleDiagramTables()
#endregion
+ [RelayCommand]
+ public void OpenUpdateModal()
+ {
+ UpdateModal.Open();
+ }
+
[RelayCommand]
public void Exit()
{
diff --git a/ViewModels/UpdateModalViewModel.cs b/ViewModels/UpdateModalViewModel.cs
new file mode 100644
index 0000000..aba05e2
--- /dev/null
+++ b/ViewModels/UpdateModalViewModel.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Quell.Models;
+using Quell.Services;
+
+namespace Quell.ViewModels;
+
+public partial class UpdateModalViewModel : ObservableObject
+{
+ [ObservableProperty]
+ private bool _isOpen;
+
+ [ObservableProperty]
+ private bool _isCornerBadgeVisible;
+
+ [ObservableProperty]
+ private string _currentVersion = AppUpdateService.CurrentVersionString;
+
+ [ObservableProperty]
+ private string _latestVersion = "v1.0.0";
+
+ [ObservableProperty]
+ private string _releaseTitle = "New Quell Studio Update Available";
+
+ [ObservableProperty]
+ private string _releaseNotes = "A new release is ready on GitHub with performance improvements and new features.";
+
+ [ObservableProperty]
+ private string _releaseUrl = AppUpdateService.DefaultReleasesPageUrl;
+
+ [ObservableProperty]
+ private string _publishedAt = string.Empty;
+
+ [ObservableProperty]
+ private bool _isUpdateAvailable;
+
+ [ObservableProperty]
+ private bool _isChecking;
+
+ [ObservableProperty]
+ private string _statusMessage = string.Empty;
+
+ public UpdateModalViewModel()
+ {
+ AppUpdateService.Instance.UpdateAvailable += OnUpdateAvailable;
+ }
+
+ private void OnUpdateAvailable(object? sender, UpdateReleaseModel release)
+ {
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ ApplyRelease(release);
+ Open();
+ });
+ }
+
+ public void ApplyRelease(UpdateReleaseModel release)
+ {
+ LatestVersion = release.TagName;
+ ReleaseTitle = release.Name;
+ ReleaseNotes = string.IsNullOrWhiteSpace(release.Body)
+ ? "• General performance enhancements and stability fixes.\n• View full release notes on GitHub."
+ : release.Body;
+ ReleaseUrl = release.HtmlUrl;
+ PublishedAt = string.IsNullOrWhiteSpace(release.PublishedAt) ? "" : $"Published: {release.PublishedAt}";
+ IsUpdateAvailable = release.IsNewer;
+ }
+
+ [RelayCommand]
+ public void Open()
+ {
+ IsOpen = true;
+ IsCornerBadgeVisible = false; // Hide corner badge while the modal itself is open
+ }
+
+ [RelayCommand]
+ public void Dismiss()
+ {
+ IsOpen = false;
+ // If an update is available, closing the modal surfaces the corner "Get Updates" badge
+ if (IsUpdateAvailable)
+ {
+ IsCornerBadgeVisible = true;
+ }
+ }
+
+ [RelayCommand]
+ public void OpenReleaseUrl()
+ {
+ try
+ {
+ string url = string.IsNullOrWhiteSpace(ReleaseUrl) ? AppUpdateService.DefaultReleasesPageUrl : ReleaseUrl;
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = url,
+ UseShellExecute = true
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[UpdateModalViewModel] Error opening release URL: {ex.Message}");
+ }
+
+ Dismiss();
+ }
+
+ [RelayCommand]
+ public async Task CheckForUpdatesAsync()
+ {
+ IsChecking = true;
+ StatusMessage = "Checking GitHub for updates...";
+ try
+ {
+ var release = await AppUpdateService.Instance.CheckForUpdatesAsync(isManualCheck: true).ConfigureAwait(false);
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ if (release != null && release.IsNewer)
+ {
+ ApplyRelease(release);
+ StatusMessage = $"New version {release.TagName} is available!";
+ Open();
+ }
+ else
+ {
+ StatusMessage = "Quell Studio is up to date!";
+ }
+ });
+ }
+ finally
+ {
+ Avalonia.Threading.Dispatcher.UIThread.Post(() => IsChecking = false);
+ }
+ }
+}
diff --git a/Views/MainView.axaml b/Views/MainView.axaml
index b089e23..ce25a06 100644
--- a/Views/MainView.axaml
+++ b/Views/MainView.axaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Quell.ViewModels"
xmlns:components="using:Quell.Components"
+ xmlns:ui="using:FluentAvalonia.UI.Controls"
x:Class="Quell.Views.MainView"
x:DataType="vm:MainViewModel"
Background="{DynamicResource AppBackgroundBrush}">
@@ -66,11 +67,49 @@
+
+
+
+
+
+
diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml
index 8d9778a..3a2b0b0 100644
--- a/Views/MainWindow.axaml
+++ b/Views/MainWindow.axaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Quell.ViewModels"
xmlns:components="using:Quell.Components"
+ xmlns:ui="using:FluentAvalonia.UI.Controls"
x:Class="Quell.Views.MainWindow"
x:DataType="vm:MainViewModel"
Icon="/Assets/quell_icon.ico"
@@ -73,6 +74,43 @@
+
+
+
+
+
@@ -80,5 +118,6 @@
+