From 9f81e314aa0d4fe43d9cb9a5a10ac62fe2ffbc2e Mon Sep 17 00:00:00 2001 From: ehsan amiri Date: Tue, 10 Nov 2026 08:24:27 +0330 Subject: [PATCH 1/4] feat(responsive,update): add responsive navbar breakpoints, splash release check, and status bar update link --- App.axaml.cs | 17 +- Components/Navigation/NavbarComponent.axaml | 283 +++++++++++------- .../Navigation/NavbarComponent.axaml.cs | 29 ++ .../Navigation/StatusBarComponent.axaml | 65 +++- Program.cs | 46 +++ Services/AppUpdateService.cs | 134 +++++++-- ViewModels/MainViewModel.cs | 19 +- Views/MainWindow.axaml | 2 +- Views/SplashScreenWindow.axaml.cs | 23 +- 9 files changed, 452 insertions(+), 166 deletions(-) diff --git a/App.axaml.cs b/App.axaml.cs index fca489a..dd5bd4f 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; +using Quell.Services; using Quell.ViewModels; using Quell.Views; @@ -25,11 +26,12 @@ public override void OnFrameworkInitializationCompleted() { if (DisableSplash || desktop.Args?.Contains("--no-splash") == true) { - Services.DatabaseStorageService.Instance.PreloadStartupDataAsync().GetAwaiter().GetResult(); + DatabaseStorageService.Instance.PreloadStartupDataAsync().GetAwaiter().GetResult(); + var vm = new MainViewModel(); desktop.MainWindow = new MainWindow { - DataContext = new MainViewModel(), + DataContext = vm, }; } else @@ -42,7 +44,7 @@ public override void OnFrameworkInitializationCompleted() { try { - // 1. Real Phase: Storage, schema, graphics, appearance, shortcuts & keywords (0% -> 88%) + // 1. Real Phase: Storage, schema, graphics, appearance, shortcuts & keywords + GitHub updates check await splash.RunStartupSequenceAsync(completeTo100: false); // 2. Real Phase: Constructing Studio Workspace (89% -> 94%) @@ -70,6 +72,13 @@ await Dispatcher.UIThread.InvokeAsync(async () => desktop.MainWindow = mainWindow; mainWindow.Show(); splash.Close(); + + // If a GitHub release update was detected during Splash Screen, open the update modal! + if (AppUpdateService.Instance.IsUpdateAvailable && AppUpdateService.Instance.LatestRelease != null) + { + vm.UpdateModal.ApplyRelease(AppUpdateService.Instance.LatestRelease); + vm.UpdateModal.Open(); + } } catch (Exception uiEx) { @@ -89,7 +98,7 @@ await Dispatcher.UIThread.InvokeAsync(async () => } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView) { - Services.DatabaseStorageService.Instance.PreloadStartupDataAsync().GetAwaiter().GetResult(); + DatabaseStorageService.Instance.PreloadStartupDataAsync().GetAwaiter().GetResult(); singleView.MainView = new MainView { diff --git a/Components/Navigation/NavbarComponent.axaml b/Components/Navigation/NavbarComponent.axaml index 4f0742e..c5b4e08 100644 --- a/Components/Navigation/NavbarComponent.axaml +++ b/Components/Navigation/NavbarComponent.axaml @@ -205,6 +205,52 @@ + + + + + + + + + + + + + + + + + + + + + @@ -213,9 +259,10 @@ BorderThickness="0,0,0,1" Height="36" PointerPressed="OnTitleBarPointerPressed"> - + + - + @@ -226,7 +273,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -621,8 +674,8 @@ - - + + + @@ -37,7 +82,7 @@ BorderBrush="{DynamicResource AppAccentBrush}" BorderThickness="1" Cursor="Hand" - ToolTip.Tip="A new version of Quell Studio is ready. Click to update."> + ToolTip.Tip="A new version of Quell Studio is ready. Click to view release details."> diff --git a/Program.cs b/Program.cs index 8c7919b..07361f2 100644 --- a/Program.cs +++ b/Program.cs @@ -248,6 +248,36 @@ public static async Task Main(string[] args) } Console.WriteLine("[Success] Hamburger menu animation, navbar item expansion, GPU pipeline and shortcuts verified!"); + // 5c. Test Navbar Responsive Layout Adaptation & Non-Clipping Window Controls + Console.WriteLine("\n[Action] Verifying Navbar Responsive Adaptive Layout..."); + navbarComp.UpdateResponsiveLayout(1400); + if (navbarComp.Classes.Contains("responsive-medium") || navbarComp.Classes.Contains("responsive-compact") || navbarComp.Classes.Contains("responsive-minimal")) + { + throw new Exception("Navbar at 1400px should not have responsive compact classes!"); + } + + navbarComp.UpdateResponsiveLayout(1050); + if (!navbarComp.Classes.Contains("responsive-medium")) + { + throw new Exception("Navbar at 1050px should have responsive-medium class!"); + } + + navbarComp.UpdateResponsiveLayout(900); + if (!navbarComp.Classes.Contains("responsive-compact")) + { + throw new Exception("Navbar at 900px should have responsive-compact class!"); + } + + navbarComp.UpdateResponsiveLayout(700); + if (!navbarComp.Classes.Contains("responsive-minimal")) + { + throw new Exception("Navbar at 700px should have responsive-minimal class!"); + } + + // Restore normal width layout + navbarComp.UpdateResponsiveLayout(1300); + Console.WriteLine("[Success] Navbar responsive adaptive breakpoints and pinned window controls verified!"); + // Test Disconnect Command triggering ConfirmModal vm.DisconnectCommand.Execute(null); Console.WriteLine($"ConfirmModal opened by Disconnect: {vm.ConfirmModal.IsOpen}, Target = {vm.ConfirmModal.TargetName}"); @@ -1918,6 +1948,22 @@ await Quell.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( } Console.WriteLine($"[Success] New update modal auto-opened: Version={vm.UpdateModal.LatestVersion}, Title='{vm.UpdateModal.ReleaseTitle}'!"); + // Verify Bottom-Left Status Bar "Get Update" and GitHub Release Action + Console.WriteLine("[Action] Verifying Update Status Bar Command & Release URL Link..."); + if (vm.UpdateModal.OpenReleaseUrlCommand == null) + { + throw new Exception("UpdateModal.OpenReleaseUrlCommand must not be null!"); + } + if (!vm.UpdateModal.IsUpdateAvailable) + { + throw new Exception("vm.UpdateModal.IsUpdateAvailable must be True when update is detected!"); + } + if (string.IsNullOrEmpty(vm.UpdateModal.ReleaseUrl) || !vm.UpdateModal.ReleaseUrl.Contains("github.com")) + { + throw new Exception($"ReleaseUrl '{vm.UpdateModal.ReleaseUrl}' must be a valid GitHub Releases URL!"); + } + Console.WriteLine($"[Success] Status bar 'Get Update' command verified with target URL: '{vm.UpdateModal.ReleaseUrl}'!"); + // Simulate user dismissing / clicking (X) on the modal Console.WriteLine("[Action] User dismissed update modal with (X)..."); vm.UpdateModal.Dismiss(); diff --git a/Services/AppUpdateService.cs b/Services/AppUpdateService.cs index 39ad275..ee04b6f 100644 --- a/Services/AppUpdateService.cs +++ b/Services/AppUpdateService.cs @@ -4,6 +4,7 @@ using System.Net.Http.Headers; using System.Reflection; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Quell.Models; @@ -21,8 +22,11 @@ public class AppUpdateService public static readonly Version CurrentVersion = new(1, 0, 0); public static string CurrentVersionString => $"v{CurrentVersion.Major}.{CurrentVersion.Minor}.{CurrentVersion.Build}"; + public static readonly DateTime AppBuildDate = new(2026, 11, 1, 0, 0, 0, DateTimeKind.Utc); + public const string GitHubRepo = "cipherunits/Quell"; public const string ReleasesApiUrl = $"https://api.github.com/repos/{GitHubRepo}/releases/latest"; + public const string AllReleasesApiUrl = $"https://api.github.com/repos/{GitHubRepo}/releases"; public const string DefaultReleasesPageUrl = $"https://github.com/{GitHubRepo}/releases"; private readonly HttpClient _httpClient; @@ -37,7 +41,7 @@ public AppUpdateService(HttpClient? client = null) { _httpClient = client ?? new HttpClient { - Timeout = TimeSpan.FromSeconds(4) + Timeout = TimeSpan.FromSeconds(5) }; if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent")) { @@ -53,44 +57,51 @@ public AppUpdateService(HttpClient? client = null) { try { + // 1. First attempt: Query releases/latest using var response = await _httpClient.GetAsync(ReleasesApiUrl).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) + 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) + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + var release = ParseReleaseElement(doc.RootElement); + if (release != null && release.IsNewer) { LatestRelease = release; IsUpdateAvailable = true; UpdateAvailable?.Invoke(this, release); + return release; } + } - return release; + // 2. Second attempt: Query full releases list to find any newer version tag + using var allResponse = await _httpClient.GetAsync(AllReleasesApiUrl).ConfigureAwait(false); + if (allResponse.IsSuccessStatusCode) + { + var allJson = await allResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + using var allDoc = JsonDocument.Parse(allJson); + if (allDoc.RootElement.ValueKind == JsonValueKind.Array) + { + UpdateReleaseModel? bestCandidate = null; + foreach (var elem in allDoc.RootElement.EnumerateArray()) + { + var candidate = ParseReleaseElement(elem); + if (candidate != null && candidate.IsNewer) + { + if (bestCandidate == null || candidate.ParsedVersion > bestCandidate.ParsedVersion) + { + bestCandidate = candidate; + } + } + } + + if (bestCandidate != null) + { + LatestRelease = bestCandidate; + IsUpdateAvailable = true; + UpdateAvailable?.Invoke(this, bestCandidate); + return bestCandidate; + } + } } } catch (Exception ex) @@ -101,6 +112,54 @@ public AppUpdateService(HttpClient? client = null) return null; } + /// + /// Parses a GitHub Release JSON element into an UpdateReleaseModel. + /// + private UpdateReleaseModel? ParseReleaseElement(JsonElement root) + { + 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() ?? "" : ""; + string updatedAt = root.TryGetProperty("updated_at", out var upElem) ? upElem.GetString() ?? "" : publishedAt; + + Version? remoteVer = null; + bool hasVersion = TryParseVersion(tagName, out remoteVer) || + TryParseVersionFromNameOrBody(name, out remoteVer) || + TryParseVersionFromNameOrBody(body, out remoteVer); + + bool isNewer = false; + if (hasVersion && remoteVer != null) + { + isNewer = remoteVer > CurrentVersion; + } + else if (string.Equals(tagName, "latest", StringComparison.OrdinalIgnoreCase)) + { + // If the repository uses continuous delivery tagged 'latest', compare release date to AppBuildDate + if (DateTime.TryParse(updatedAt, out var dt) && dt > AppBuildDate) + { + isNewer = true; + remoteVer = new Version(1, 0, 1); + if (string.IsNullOrWhiteSpace(name) || name == "latest") + { + name = $"Quell Continuous Update ({dt:yyyy-MM-dd})"; + } + } + } + + return new UpdateReleaseModel + { + TagName = string.IsNullOrWhiteSpace(tagName) ? "v1.0.1" : tagName, + Name = string.IsNullOrWhiteSpace(name) ? "Quell Studio Update" : name, + Body = body, + HtmlUrl = htmlUrl, + PublishedAt = publishedAt, + ParsedVersion = remoteVer ?? new Version(1, 0, 1), + IsNewer = isNewer + }; + } + /// /// Parses version string from release tag name (e.g., 'v1.1.0' -> 1.1.0). /// @@ -121,6 +180,19 @@ public static bool TryParseVersion(string? tag, out Version? version) return Version.TryParse(clean, out version); } + private static bool TryParseVersionFromNameOrBody(string? text, out Version? version) + { + version = null; + if (string.IsNullOrWhiteSpace(text)) return false; + + var match = Regex.Match(text, @"\bv?(\d+\.\d+(\.\d+)?)\b", RegexOptions.IgnoreCase); + if (match.Success) + { + return TryParseVersion(match.Groups[1].Value, out version); + } + return false; + } + /// /// Helper to trigger update availability (used for verification testing and offline preview). /// diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 54e2005..071938f 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -223,11 +223,24 @@ private async Task InitializeStartupAsync() await RefreshAvailableDatabasesAsync(); await Sidebar.RefreshAsync(); - // Check for updates asynchronously from GitHub releases (non-blocking) + // If an update was discovered during splash screen, surface the modal + if (AppUpdateService.Instance.IsUpdateAvailable && AppUpdateService.Instance.LatestRelease != null) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + UpdateModal.ApplyRelease(AppUpdateService.Instance.LatestRelease); + UpdateModal.Open(); + }); + } + + // Secondary non-blocking background check in case network was slow at launch _ = Task.Run(async () => { - await Task.Delay(2500).ConfigureAwait(false); - await AppUpdateService.Instance.CheckForUpdatesAsync().ConfigureAwait(false); + await Task.Delay(2000).ConfigureAwait(false); + if (!AppUpdateService.Instance.IsUpdateAvailable) + { + await AppUpdateService.Instance.CheckForUpdatesAsync().ConfigureAwait(false); + } }); } diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 3a2b0b0..8b90bc9 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -72,7 +72,7 @@ - + Date: Tue, 10 Nov 2026 08:38:03 +0330 Subject: [PATCH 2/4] fix(ui): remove redundant bottom-right Get Updates badge and status bar button --- Components/Modals/UpdateModalComponent.axaml | 2 +- .../Navigation/StatusBarComponent.axaml | 20 +--------- Program.cs | 20 ++++++---- ViewModels/UpdateModalViewModel.cs | 6 +-- Views/MainView.axaml | 37 ------------------- Views/MainWindow.axaml | 37 ------------------- 6 files changed, 15 insertions(+), 107 deletions(-) diff --git a/Components/Modals/UpdateModalComponent.axaml b/Components/Modals/UpdateModalComponent.axaml index d768a0c..b80433b 100644 --- a/Components/Modals/UpdateModalComponent.axaml +++ b/Components/Modals/UpdateModalComponent.axaml @@ -141,7 +141,7 @@ - - - + - - - - - diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 8b90bc9..eaaa0d4 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -74,43 +74,6 @@ - - - - - From 9d47884933c88c25f96de1ab16766bc3a69d4a07 Mon Sep 17 00:00:00 2001 From: ehsan amiri Date: Tue, 10 Nov 2026 08:45:12 +0330 Subject: [PATCH 3/4] fix(update): strictly require remote version > current version to eliminate false positives --- Program.cs | 36 +++++++++++++++++++++++++++ Services/AppUpdateService.cs | 47 ++++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/Program.cs b/Program.cs index b0db06b..c10dbe5 100644 --- a/Program.cs +++ b/Program.cs @@ -1925,6 +1925,42 @@ await Quell.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( 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.1b Test GitHub Release JSON Parsing: No false positives on same/older version (e.g. current 1.0.0 on GitHub) + string currentGitHubJson = @"{ + ""tag_name"": ""latest"", + ""name"": ""Quell Continuous Build"", + ""assets"": [ + { ""name"": ""Quell-1.0.0-win-x64-portable.zip"" }, + { ""name"": ""Quell-Setup-win-x64.exe"" } + ] + }"; + using (var currentDoc = System.Text.Json.JsonDocument.Parse(currentGitHubJson)) + { + var parsedCurrent = Quell.Services.AppUpdateService.Instance.ParseReleaseElement(currentDoc.RootElement); + if (parsedCurrent == null || parsedCurrent.IsNewer) + { + throw new Exception("GitHub continuous release with 1.0.0 assets should NOT be marked as newer than 1.0.0!"); + } + } + Console.WriteLine("[Success] Verified zero false-positive on current 1.0.0 GitHub release!"); + + string newerGitHubJson = @"{ + ""tag_name"": ""v1.1.0"", + ""name"": ""Quell Studio 1.1.0 Release"", + ""assets"": [ + { ""name"": ""Quell-1.1.0-win-x64-portable.zip"" } + ] + }"; + using (var newerDoc = System.Text.Json.JsonDocument.Parse(newerGitHubJson)) + { + var parsedNewer = Quell.Services.AppUpdateService.Instance.ParseReleaseElement(newerDoc.RootElement); + if (parsedNewer == null || !parsedNewer.IsNewer) + { + throw new Exception("GitHub release with v1.1.0 should be marked as newer!"); + } + } + Console.WriteLine("[Success] Verified newer release detection on v1.1.0!"); + // 23.2 Test Update Modal & Corner "Get Updates" Box Lifecycle vm.UpdateModal.IsOpen = false; vm.UpdateModal.IsCornerBadgeVisible = false; diff --git a/Services/AppUpdateService.cs b/Services/AppUpdateService.cs index ee04b6f..3eb3926 100644 --- a/Services/AppUpdateService.cs +++ b/Services/AppUpdateService.cs @@ -1,8 +1,9 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; -using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -11,7 +12,7 @@ namespace Quell.Services; /// -/// Service responsible for GitHub release versioning checks, semantic version comparisons, +/// Singleton service managing GitHub Releases checks, semantic version comparisons, /// and update notifications across Quell Studio. /// public class AppUpdateService @@ -22,8 +23,6 @@ public class AppUpdateService public static readonly Version CurrentVersion = new(1, 0, 0); public static string CurrentVersionString => $"v{CurrentVersion.Major}.{CurrentVersion.Minor}.{CurrentVersion.Build}"; - public static readonly DateTime AppBuildDate = new(2026, 11, 1, 0, 0, 0, DateTimeKind.Utc); - public const string GitHubRepo = "cipherunits/Quell"; public const string ReleasesApiUrl = $"https://api.github.com/repos/{GitHubRepo}/releases/latest"; public const string AllReleasesApiUrl = $"https://api.github.com/repos/{GitHubRepo}/releases"; @@ -52,6 +51,7 @@ public AppUpdateService(HttpClient? client = null) /// /// Checks GitHub repository releases for a newer version than CurrentVersion. /// Runs asynchronously and fails gracefully on network errors or timeouts. + /// Only returns a release if a strictly newer version (> CurrentVersion) is found. /// public async Task CheckForUpdatesAsync(bool isManualCheck = false) { @@ -109,53 +109,58 @@ public AppUpdateService(HttpClient? client = null) Console.WriteLine($"[AppUpdateService] CheckForUpdates error: {ex.Message}"); } + // No newer release found on GitHub; ensure update state is clean + IsUpdateAvailable = false; + LatestRelease = null; return null; } /// /// Parses a GitHub Release JSON element into an UpdateReleaseModel. + /// Resolves the actual semantic version from tag_name, title, body, or asset filenames. + /// IsNewer is only true if the resolved version is strictly greater than CurrentVersion. /// - private UpdateReleaseModel? ParseReleaseElement(JsonElement root) + public UpdateReleaseModel? ParseReleaseElement(JsonElement root) { 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() ?? "" : ""; - string updatedAt = root.TryGetProperty("updated_at", out var upElem) ? upElem.GetString() ?? "" : publishedAt; Version? remoteVer = null; bool hasVersion = TryParseVersion(tagName, out remoteVer) || TryParseVersionFromNameOrBody(name, out remoteVer) || TryParseVersionFromNameOrBody(body, out remoteVer); - bool isNewer = false; - if (hasVersion && remoteVer != null) - { - isNewer = remoteVer > CurrentVersion; - } - else if (string.Equals(tagName, "latest", StringComparison.OrdinalIgnoreCase)) + // If not found in tag, name or body, check asset names (e.g., Quell-1.0.0-win-x64-portable.zip) + if (!hasVersion && root.TryGetProperty("assets", out var assetsElem) && assetsElem.ValueKind == JsonValueKind.Array) { - // If the repository uses continuous delivery tagged 'latest', compare release date to AppBuildDate - if (DateTime.TryParse(updatedAt, out var dt) && dt > AppBuildDate) + foreach (var asset in assetsElem.EnumerateArray()) { - isNewer = true; - remoteVer = new Version(1, 0, 1); - if (string.IsNullOrWhiteSpace(name) || name == "latest") + if (asset.TryGetProperty("name", out var assetNameElem)) { - name = $"Quell Continuous Update ({dt:yyyy-MM-dd})"; + string assetName = assetNameElem.GetString() ?? ""; + if (TryParseVersionFromNameOrBody(assetName, out remoteVer)) + { + hasVersion = true; + break; + } } } } + // Only mark as newer if a valid version was resolved AND it strictly exceeds CurrentVersion + bool isNewer = hasVersion && remoteVer != null && remoteVer > CurrentVersion; + return new UpdateReleaseModel { - TagName = string.IsNullOrWhiteSpace(tagName) ? "v1.0.1" : tagName, + TagName = string.IsNullOrWhiteSpace(tagName) ? (remoteVer != null ? $"v{remoteVer}" : CurrentVersionString) : tagName, Name = string.IsNullOrWhiteSpace(name) ? "Quell Studio Update" : name, Body = body, HtmlUrl = htmlUrl, PublishedAt = publishedAt, - ParsedVersion = remoteVer ?? new Version(1, 0, 1), + ParsedVersion = remoteVer ?? CurrentVersion, IsNewer = isNewer }; } @@ -180,7 +185,7 @@ public static bool TryParseVersion(string? tag, out Version? version) return Version.TryParse(clean, out version); } - private static bool TryParseVersionFromNameOrBody(string? text, out Version? version) + public static bool TryParseVersionFromNameOrBody(string? text, out Version? version) { version = null; if (string.IsNullOrWhiteSpace(text)) return false; From 1686d4e693f2fa56fec2fc2cb4e0b24dd3934379 Mon Sep 17 00:00:00 2001 From: ehsan amiri Date: Tue, 10 Nov 2026 09:06:58 +0330 Subject: [PATCH 4/4] docs: add comprehensive CONTRIBUTING.md guidelines --- CONTRIBUTING.md | 221 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3ff401a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,221 @@ +# Contributing to Quell + +Thank you for your interest in contributing to **Quell**! Quell is a modern, cross-platform, high-performance database management tool and SQL IDE built with **Avalonia UI** and **.NET 10**. + +Whether you are fixing a bug, implementing a new database dialect, adding UI enhancements, or improving documentation, your contributions are warmly welcomed. + +--- + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Prerequisites & Development Environment](#prerequisites--development-environment) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Verification & Automated Test Suite](#verification--automated-test-suite) +- [Coding Guidelines & Architectural Principles](#coding-guidelines--architectural-principles) + - [1. MVVM & Separation of Concerns](#1-mvvm--separation-of-concerns) + - [2. Semantic Theming & Dynamic Tokens](#2-semantic-theming--dynamic-tokens) + - [3. Vector Graphics Only (No Emojis)](#3-vector-graphics-only-no-emojis) + - [4. Responsive Layout & Window Controls](#4-responsive-layout--window-controls) + - [5. Hardware Graphics & Motion Pipeline](#5-hardware-graphics--motion-pipeline) + - [6. Thread Affinity & UI Dispatching](#6-thread-affinity--ui-dispatching) +- [Commit Conventions](#commit-conventions) +- [Submitting a Pull Request](#submitting-a-pull-request) +- [Reporting Issues](#reporting-issues) + +--- + +## Code of Conduct + +We are committed to providing a welcoming, friendly, and harassment-free experience for everyone. Please be respectful, constructive, and collaborative in all issues, pull requests, and discussions. + +--- + +## Prerequisites & Development Environment + +To build, debug, and run Quell locally, make sure you have installed: + +- **[.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)** (v10.0.x or later) +- **Git** +- An IDE or editor of your choice: + - **JetBrains Rider** (Recommended for cross-platform Avalonia UI development) + - **Visual Studio 2022** (v17.12+ with .NET 10 workload) + - **Visual Studio Code** with the *C# Dev Kit* and *Avalonia for VS Code* extensions + +--- + +## Getting Started + +1. **Fork the Repository** on GitHub: + Navigate to [https://github.com/cipherunits/Quell](https://github.com/cipherunits/Quell) and click **Fork**. + +2. **Clone your fork locally**: + ```bash + git clone https://github.com//Quell.git + cd Quell + ``` + +3. **Check out the `dev` branch**: + Active development and pull requests target the `dev` branch. + ```bash + git checkout dev + ``` + +4. **Restore dependencies**: + ```bash + dotnet restore Quell.csproj + ``` + +5. **Build the project**: + ```bash + dotnet build Quell.csproj --configuration Release + ``` + +6. **Run Quell**: + ```bash + dotnet run --configuration Release + ``` + *(To bypass the splash screen during rapid UI debugging, pass `--no-splash`: `dotnet run -- --no-splash`)* + +--- + +## Development Workflow + +1. **Create a topic branch**: + Always branch off from `dev`: + ```bash + git checkout dev + git pull origin dev + git checkout -b feat/your-feature-name + # or + git checkout -b fix/issue-description + ``` + +2. **Make your changes**: + Keep your edits focused and modular. If you add or modify UI components, refer to [`COMPONENTS.md`](COMPONENTS.md) to adhere to the existing component taxonomy. + +3. **Verify builds with zero warnings**: + Ensure your code compiles cleanly without any compiler warnings or errors: + ```bash + dotnet build Quell.csproj --configuration Release + ``` + +4. **Run the Automated Verification Suite**: + Before committing, always run the full automated verification test suite: + ```bash + dotnet run --configuration Release --no-build -- --verify + ``` + +--- + +## Verification & Automated Test Suite + +Quell features a built-in headless layout and logic verification engine in `Program.cs`. It validates: + +- Window chrome and layout dynamics (Sidebar toggles, Footer collapse, Star column expansion). +- Focus visual suppression, modal lifecycles, and dialog states. +- Hardware graphics adapter detection and DirectX 11 / ANGLE GPU pipelines. +- Multi-dialect SQLite migrations, table provisioning, and persistence. +- Results table batch cell editing, multi-row deletion, and column dropping. +- Foreign key detection and arrow navigation. +- Terminal session lifecycle and database CLI resolution (`psql`, `mysql`, `sqlcmd`, `sqlite3`). +- SemVer comparison, GitHub release detection, and status bar update triggers. + +**Running the test suite:** +```bash +dotnet run --configuration Release --no-build -- --verify +``` +All pull requests must achieve a **100% pass rate** on the verification suite. + +--- + +## Coding Guidelines & Architectural Principles + +### 1. MVVM & Separation of Concerns +- Follow the Model-View-ViewModel pattern using **CommunityToolkit.Mvvm** (`ObservableObject`, `[ObservableProperty]`, `[RelayCommand]`). +- Keep Views/UserControls (`.axaml` and `.axaml.cs`) focused on visual presentation and user gestures. +- Encapsulate business logic, database queries, and async task orchestration in dedicated Services (`/Services`) or ViewModels (`/ViewModels`). + +### 2. Semantic Theming & Dynamic Tokens +- **Never hardcode hex color values** (e.g., `#1E293B`, `#FFFFFF`) directly in UserControls or styles. +- Always use semantic resource bindings: + ```xml + Background="{DynamicResource AppBackgroundBrush}" + Foreground="{DynamicResource AppTextPrimaryBrush}" + BorderBrush="{DynamicResource AppBorderBrush}" + ``` +- All global theme tokens are defined in `Styles/ThemeTokens.axaml`, with specific values declared in `Styles/LightTheme.axaml` and `Styles/DarkTheme.axaml`. + +### 3. Vector Graphics Only (No Emojis) +- Quell exclusively uses clean, professional vector icons. **Do not use Unicode emoji characters** (e.g., 💾, 🚀, ❌) in UI buttons, menus, or labels. +- Use `FluentAvalonia.UI.Controls.FASymbolIcon` (e.g., ``) or native Avalonia `PathIcon` geometry. + +### 4. Responsive Layout & Window Controls +- Maintain flexible Grid column definitions (`Auto, *, Auto, Auto`). +- **Never push native window controls off-screen**: Minimize, Maximize/Restore, and Close buttons must always remain pinned to the far right. +- Use adaptive breakpoints (`responsive-medium`, `responsive-compact`, `responsive-minimal`) to hide text labels and compact pills on narrower viewport widths while preserving icon actions and tooltips. + +### 5. Hardware Graphics & Motion Pipeline +- Quell integrates hardware graphics detection via `HardwareGraphicsService`. +- Visual transitions should check or utilize `:is(TopLevel).gpu-mode` / `UserControl.gpu-mode` selectors so animations remain fluid and lightweight across both GPU-accelerated and CPU fallback rendering environments. + +### 6. Thread Affinity & UI Dispatching +- Background operations (I/O, database queries, GitHub API calls) should execute on background threads via `ConfigureAwait(false)`. +- When updating observable properties or interacting with visual controls from asynchronous tasks, dispatch safely to the Avalonia UI thread: + ```csharp + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + MyProperty = newValue; + }); + ``` + +--- + +## Commit Conventions + +We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +- `feat:` A new feature (e.g., `feat(editor): add multi-cursor support`) +- `fix:` A bug fix (e.g., `fix(navbar): resolve responsive overflow on compact widths`) +- `docs:` Documentation changes (e.g., `docs: update COMPONENTS.md`) +- `style:` Formatting, whitespace, or visual styling updates +- `refactor:` Code restructuring without behavior changes +- `perf:` Performance improvements +- `test:` Adding or updating tests and verification routines +- `chore:` Maintenance, package updates, or CI/CD workflow updates + +--- + +## Submitting a Pull Request + +1. **Push your branch to your GitHub fork**: + ```bash + git push -u origin feat/your-feature-name + ``` +2. **Open a Pull Request**: + - Target the **`dev`** branch as the base repository branch. + - Provide a clear, descriptive title following Conventional Commits. + - Describe the problem you are solving and summarize the changes made. + - Attach screenshots or GIFs if your changes involve UI modifications. +3. **CI Checks**: + - Ensure GitHub Actions CI/CD passes (`Run Verification & Integrity Suite` on Windows, Linux, and macOS). + - Address any reviewer feedback promptly. + +--- + +## Reporting Issues + +If you encounter a bug or have a suggestion: + +1. **Search existing issues** to prevent duplicates: [Quell Issues](https://github.com/cipherunits/Quell/issues). +2. **Provide detailed information**: + - Operating System and architecture (e.g., Windows 11 x64, Ubuntu 24.04, macOS Sequoia M-series). + - .NET SDK version (`dotnet --version`). + - Clear steps to reproduce the issue. + - Expected vs. actual behavior. + - Relevant error logs, stack traces, or console outputs. + +--- + +*Thank you for contributing to make Quell faster, more reliable, and a joy to use for database engineers everywhere!*