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/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!* 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 @@ + + + + + + + + + + + + + + + + + + + + + @@ -213,9 +259,10 @@ BorderThickness="0,0,0,1" Height="36" PointerPressed="OnTitleBarPointerPressed"> - + + - + @@ -226,7 +273,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -621,8 +674,8 @@ - - + + + + + + + -/// 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 @@ -23,6 +25,7 @@ public class AppUpdateService 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 +40,7 @@ public AppUpdateService(HttpClient? client = null) { _httpClient = client ?? new HttpClient { - Timeout = TimeSpan.FromSeconds(4) + Timeout = TimeSpan.FromSeconds(5) }; if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent")) { @@ -48,49 +51,57 @@ 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) { 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) @@ -98,9 +109,62 @@ 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. + /// + 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() ?? "" : ""; + + Version? remoteVer = null; + bool hasVersion = TryParseVersion(tagName, out remoteVer) || + TryParseVersionFromNameOrBody(name, out remoteVer) || + TryParseVersionFromNameOrBody(body, out remoteVer); + + // 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) + { + foreach (var asset in assetsElem.EnumerateArray()) + { + if (asset.TryGetProperty("name", out var assetNameElem)) + { + 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) ? (remoteVer != null ? $"v{remoteVer}" : CurrentVersionString) : tagName, + Name = string.IsNullOrWhiteSpace(name) ? "Quell Studio Update" : name, + Body = body, + HtmlUrl = htmlUrl, + PublishedAt = publishedAt, + ParsedVersion = remoteVer ?? CurrentVersion, + IsNewer = isNewer + }; + } + /// /// Parses version string from release tag name (e.g., 'v1.1.0' -> 1.1.0). /// @@ -121,6 +185,19 @@ public static bool TryParseVersion(string? tag, out Version? version) return Version.TryParse(clean, out version); } + public 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/ViewModels/UpdateModalViewModel.cs b/ViewModels/UpdateModalViewModel.cs index aba05e2..45de2f9 100644 --- a/ViewModels/UpdateModalViewModel.cs +++ b/ViewModels/UpdateModalViewModel.cs @@ -80,11 +80,7 @@ public void Open() public void Dismiss() { IsOpen = false; - // If an update is available, closing the modal surfaces the corner "Get Updates" badge - if (IsUpdateAvailable) - { - IsCornerBadgeVisible = true; - } + IsCornerBadgeVisible = false; } [RelayCommand] diff --git a/Views/MainView.axaml b/Views/MainView.axaml index ce25a06..cdec993 100644 --- a/Views/MainView.axaml +++ b/Views/MainView.axaml @@ -67,43 +67,6 @@ - - - - - diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 3a2b0b0..eaaa0d4 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -72,44 +72,7 @@ - - - - - - + diff --git a/Views/SplashScreenWindow.axaml.cs b/Views/SplashScreenWindow.axaml.cs index 1a421d5..fda6c25 100644 --- a/Views/SplashScreenWindow.axaml.cs +++ b/Views/SplashScreenWindow.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; @@ -82,9 +82,28 @@ await DatabaseStorageService.Instance.PreloadStartupDataAsync(async (progress, s { UpdateProgress(progress, status, subStatus); // Brief micro-delay so the UI thread renders this real step's progress and the user can see it - await Task.Delay(40).ConfigureAwait(false); + await Task.Delay(35).ConfigureAwait(false); }).ConfigureAwait(false); + // Check for updates from GitHub releases directly during Splash Screen + UpdateProgress(78, "Checking for Updates...", "Connecting to GitHub releases..."); + try + { + var updateRelease = await AppUpdateService.Instance.CheckForUpdatesAsync().ConfigureAwait(false); + if (updateRelease != null && updateRelease.IsNewer) + { + UpdateProgress(85, $"Update Available: {updateRelease.TagName}", "New version ready on GitHub."); + } + else + { + UpdateProgress(85, "Version Check Complete", "Quell is up to date."); + } + } + catch (Exception ex) + { + Console.WriteLine($"[SplashScreen] Update check error: {ex.Message}"); + } + _isDatabaseReady = true; if (completeTo100)