diff --git a/GenHub/GenHub.Core/Constants/ApiConstants.cs b/GenHub/GenHub.Core/Constants/ApiConstants.cs index d0643258..33de7099 100644 --- a/GenHub/GenHub.Core/Constants/ApiConstants.cs +++ b/GenHub/GenHub.Core/Constants/ApiConstants.cs @@ -47,7 +47,7 @@ public static class ApiConstants /// /// Format string for GitHub API Workflow Runs endpoint (owner, repo, branch). /// - public const string GitHubApiWorkflowRunsFormat = "https://api.github.com/repos/{0}/{1}/actions/runs?status=success&branch={2}&per_page=10"; + public const string GitHubApiWorkflowRunsFormat = "https://api.github.com/repos/{0}/{1}/actions/runs?status=success&branch={2}&event=push&per_page=10"; /// /// Format string for GitHub API Latest Workflow Runs endpoint (owner, repo). diff --git a/GenHub/GenHub.Core/Models/Common/UserSettings.cs b/GenHub/GenHub.Core/Models/Common/UserSettings.cs index 72583726..93d5dfff 100644 --- a/GenHub/GenHub.Core/Models/Common/UserSettings.cs +++ b/GenHub/GenHub.Core/Models/Common/UserSettings.cs @@ -111,6 +111,11 @@ public bool IsExplicitlySet(string propertyName) /// public int? SubscribedPrNumber { get; set; } + /// + /// Gets or sets the subscribed branch name for update notifications (e.g. "development"). + /// + public string? SubscribedBranch { get; set; } + /// /// Gets or sets the last dismissed update version to prevent repeated notifications. /// @@ -143,6 +148,7 @@ public object Clone() ApplicationDataPath = ApplicationDataPath, UpdateChannel = UpdateChannel, SubscribedPrNumber = SubscribedPrNumber, + SubscribedBranch = SubscribedBranch, DismissedUpdateVersion = DismissedUpdateVersion, ContentDirectories = ContentDirectories != null ? new List(ContentDirectories) : null, GitHubDiscoveryRepositories = GitHubDiscoveryRepositories != null ? new List(GitHubDiscoveryRepositories) : null, diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs index 764b0316..c7c2a97a 100644 --- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs +++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs @@ -69,6 +69,7 @@ public void Constructor_CreatesValidInstance() mockProfileEditorFacade.Object, mockVelopackUpdateManager.Object, CreateProfileResourceService(), + mockNotificationService.Object, mockLogger.Object); // Assert @@ -111,6 +112,7 @@ public void SelectTabCommand_SetsSelectedTab(NavigationTab tab) mockProfileEditorFacade.Object, mockVelopackUpdateManager.Object, CreateProfileResourceService(), + mockNotificationService.Object, mockLogger.Object); vm.SelectTabCommand.Execute(tab); Assert.Equal(tab, vm.SelectedTab); @@ -148,6 +150,7 @@ public async Task ScanAndCreateProfilesAsync_CanBeCalled() mockProfileEditorFacade.Object, mockVelopackUpdateManager.Object, CreateProfileResourceService(), + mockNotificationService.Object, mockLogger.Object); // Act & Assert @@ -189,6 +192,7 @@ public async Task InitializeAsync_MultipleCallsAreSafe() mockProfileEditorFacade.Object, mockVelopackUpdateManager.Object, CreateProfileResourceService(), + mockNotificationService.Object, mockLogger.Object); await vm.InitializeAsync(); // Should not throw Assert.True(true); @@ -229,6 +233,7 @@ public void CurrentTabViewModel_ReturnsCorrectViewModel(NavigationTab tab) mockProfileEditorFacade.Object, mockVelopackUpdateManager.Object, CreateProfileResourceService(), + mockNotificationService.Object, mockLogger.Object); vm.SelectTabCommand.Execute(tab); var currentViewModel = vm.CurrentTabViewModel; diff --git a/GenHub/GenHub/Common/ViewModels/MainViewModel.cs b/GenHub/GenHub/Common/ViewModels/MainViewModel.cs index 7885bc79..748e4811 100644 --- a/GenHub/GenHub/Common/ViewModels/MainViewModel.cs +++ b/GenHub/GenHub/Common/ViewModels/MainViewModel.cs @@ -4,14 +4,17 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using GenHub.Core.Constants; using GenHub.Core.Interfaces.Common; using GenHub.Core.Interfaces.GameInstallations; using GenHub.Core.Interfaces.GameProfiles; +using GenHub.Core.Interfaces.Notifications; using GenHub.Core.Models.Enums; using GenHub.Core.Models.GameProfile; +using GenHub.Core.Models.Notifications; using GenHub.Features.AppUpdate.Interfaces; using GenHub.Features.Downloads.ViewModels; using GenHub.Features.GameProfiles.Services; @@ -35,13 +38,13 @@ public partial class MainViewModel : ObservableObject, IDisposable private readonly IProfileEditorFacade _profileEditorFacade; private readonly IVelopackUpdateManager _velopackUpdateManager; private readonly ProfileResourceService _profileResourceService; + private readonly INotificationService _notificationService; private readonly CancellationTokenSource _initializationCts = new(); [ObservableProperty] private NavigationTab _selectedTab = NavigationTab.GameProfiles; - [ObservableProperty] - private bool _hasUpdateAvailable; + /// /// Initializes a new instance of the class. @@ -57,6 +60,7 @@ public partial class MainViewModel : ObservableObject, IDisposable /// Profile editor facade for automatic profile creation. /// The Velopack update manager for checking updates. /// Service for accessing profile resources. + /// Service for showing notifications. /// Logger instance. public MainViewModel( GameProfileLauncherViewModel gameProfilesViewModel, @@ -70,6 +74,7 @@ public MainViewModel( IProfileEditorFacade profileEditorFacade, IVelopackUpdateManager velopackUpdateManager, ProfileResourceService profileResourceService, + INotificationService notificationService, ILogger? logger = null) { GameProfilesViewModel = gameProfilesViewModel; @@ -83,6 +88,7 @@ public MainViewModel( _profileEditorFacade = profileEditorFacade ?? throw new ArgumentNullException(nameof(profileEditorFacade)); _velopackUpdateManager = velopackUpdateManager ?? throw new ArgumentNullException(nameof(velopackUpdateManager)); _profileResourceService = profileResourceService ?? throw new ArgumentNullException(nameof(profileResourceService)); + _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); _logger = logger; // Load initial settings using unified configuration @@ -181,30 +187,7 @@ public void SelectTab(NavigationTab tab) SelectedTab = tab; } - /// - /// Shows the update notification dialog. - /// - /// A task representing the asynchronous operation. - [RelayCommand] - public async Task ShowUpdateDialogAsync() - { - try - { - var mainWindow = GetMainWindow(); - if (mainWindow != null) - { - await GenHub.Features.AppUpdate.Views.UpdateNotificationWindow.ShowAsync(mainWindow); - } - else - { - _logger?.LogWarning("Cannot show update dialog - main window not found"); - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Failed to show update dialog"); - } - } + /// /// Performs asynchronous initialization for the shell and all tabs. @@ -372,115 +355,58 @@ private async Task CheckForUpdatesAsync(CancellationToken cancellationToken = de try { - // Check if subscribed to a PR - if so, check for PR artifact updates instead var settings = _userSettingsService.Get(); + + // Push settings to update manager (important context for other components) if (settings.SubscribedPrNumber.HasValue) { - _logger?.LogDebug("User subscribed to PR #{PrNumber}, checking for PR artifact updates", settings.SubscribedPrNumber); _velopackUpdateManager.SubscribedPrNumber = settings.SubscribedPrNumber; + } - // Fetch PR list to populate artifact info - var prs = await _velopackUpdateManager.GetOpenPullRequestsAsync(cancellationToken); - var subscribedPr = prs.FirstOrDefault(p => p.Number == settings.SubscribedPrNumber); + // Check if subscribed to a specific branch + if (!string.IsNullOrEmpty(settings.SubscribedBranch)) + { + _logger?.LogDebug("User subscribed to branch '{Branch}', checking for artifact updates", settings.SubscribedBranch); + _velopackUpdateManager.SubscribedBranch = settings.SubscribedBranch; + + // Ensure PR number is cleared to avoid ambiguity + _velopackUpdateManager.SubscribedPrNumber = null; + + var artifactUpdate = await _velopackUpdateManager.CheckForArtifactUpdatesAsync(cancellationToken); - if (subscribedPr?.LatestArtifact != null) + if (artifactUpdate != null) { - // Compare versions (strip build metadata) - var currentVersionBase = AppConstants.AppVersion.Split('+')[0]; - var prVersionBase = subscribedPr.LatestArtifact.Version.Split('+')[0]; + var newVersionBase = artifactUpdate.Version.Split('+')[0]; + var dismissedVersionBase = settings.DismissedUpdateVersion?.Split('+')[0]; - if (!string.Equals(prVersionBase, currentVersionBase, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(dismissedVersionBase) || + !string.Equals(newVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase)) { - // Check if this PR version was dismissed - var dismissedVersionBase = settings.DismissedUpdateVersion?.Split('+')[0]; + _logger?.LogInformation("Branch '{Branch}' artifact update available: {Version}", settings.SubscribedBranch, newVersionBase); - if (string.IsNullOrEmpty(dismissedVersionBase) || - !string.Equals(prVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase)) - { - _logger?.LogInformation("PR #{PrNumber} artifact update available: {Version}", subscribedPr.Number, prVersionBase); - HasUpdateAvailable = true; - return; - } - else + await Dispatcher.UIThread.InvokeAsync(() => { - _logger?.LogDebug("PR #{PrNumber} artifact update {Version} was dismissed", subscribedPr.Number, prVersionBase); - HasUpdateAvailable = false; - return; - } + _notificationService.Show(new NotificationMessage( + NotificationType.Info, + "Update Available", + $"A new version ({newVersionBase}) is available on branch '{settings.SubscribedBranch}'.", + null, + "View Updates", + () => { SettingsViewModel.OpenUpdateWindowCommand.Execute(null); })); + }); + return; } else { - _logger?.LogDebug("Already on latest PR #{PrNumber} artifact version", subscribedPr.Number); - HasUpdateAvailable = false; + _logger?.LogDebug("Branch '{Branch}' artifact update {Version} was dismissed", settings.SubscribedBranch, newVersionBase); return; } } - else - { - _logger?.LogDebug("PR #{PrNumber} has no artifacts or PR not found", settings.SubscribedPrNumber); - - // Fall through to check main branch updates - } - } - - // Check main branch updates (if not subscribed to PR or PR has no artifacts) - var updateInfo = await _velopackUpdateManager.CheckForUpdatesAsync(cancellationToken); - - // Check both UpdateInfo (from installed app) and GitHub API flag (works in debug too) - var hasUpdate = updateInfo != null || _velopackUpdateManager.HasUpdateAvailableFromGitHub; - - if (hasUpdate) - { - string? latestVersion = null; - - if (updateInfo != null) - { - latestVersion = updateInfo.TargetFullRelease.Version.ToString(); - _logger?.LogInformation("Update available: {Current} → {Latest}", AppConstants.AppVersion, latestVersion); - } - else if (_velopackUpdateManager.LatestVersionFromGitHub != null) - { - latestVersion = _velopackUpdateManager.LatestVersionFromGitHub; - _logger?.LogInformation("Update available from GitHub API: {Version}", latestVersion); - } - - // Strip build metadata for comparison (everything after '+') - var latestVersionBase = latestVersion?.Split('+')[0]; - var currentVersionBase = AppConstants.AppVersion.Split('+')[0]; - - // Check if this version was dismissed by the user - var settings2 = _userSettingsService.Get(); - var dismissedVersionBase = settings2.DismissedUpdateVersion?.Split('+')[0]; - - if (!string.IsNullOrEmpty(latestVersionBase) && - string.Equals(latestVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase)) - { - _logger?.LogDebug("Update {Version} was dismissed by user, hiding notification", latestVersionBase); - HasUpdateAvailable = false; - } - - // Also check if we're already on this version (ignoring build metadata) - else if (!string.IsNullOrEmpty(latestVersionBase) && - string.Equals(latestVersionBase, currentVersionBase, StringComparison.OrdinalIgnoreCase)) - { - _logger?.LogDebug("Already on version {Version} (ignoring build metadata), hiding notification", latestVersionBase); - HasUpdateAvailable = false; - } - else - { - HasUpdateAvailable = true; - } - } - else - { - _logger?.LogDebug("No updates available"); - HasUpdateAvailable = false; } } catch (Exception ex) { _logger?.LogError(ex, "Exception in CheckForUpdatesAsync"); - HasUpdateAvailable = false; } } diff --git a/GenHub/GenHub/Common/Views/MainView.axaml b/GenHub/GenHub/Common/Views/MainView.axaml index e7ad4d64..dd66a99e 100644 --- a/GenHub/GenHub/Common/Views/MainView.axaml +++ b/GenHub/GenHub/Common/Views/MainView.axaml @@ -87,29 +87,7 @@ - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - - - + + + + + + + diff --git a/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml b/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml index 69b602e1..1448a6f8 100644 --- a/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml +++ b/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml @@ -3,8 +3,8 @@ xmlns:views="using:GenHub.Features.AppUpdate.Views" xmlns:vm="using:GenHub.Features.AppUpdate.ViewModels" x:Class="GenHub.Features.AppUpdate.Views.UpdateNotificationWindow" - Width="580" Height="800" - MinWidth="500" MinHeight="580" + Width="900" Height="800" + MinWidth="750" MinHeight="580" Title="GenHub Updates" Icon="/Assets/Icons/generalshub-icon.png" WindowStartupLocation="CenterScreen" @@ -102,4 +102,4 @@ - \ No newline at end of file + diff --git a/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml b/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml index f31ee7a3..84a0090b 100644 --- a/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml +++ b/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml @@ -91,29 +91,7 @@ Foreground="#AAAAAA" HorizontalAlignment="Center" /> - - - - - - + diff --git a/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs b/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs index f2c71abf..fd6d531c 100644 --- a/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs +++ b/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs @@ -181,6 +181,9 @@ public partial class SettingsViewModel : ObservableObject, IDisposable [ObservableProperty] private UpdateChannel _selectedUpdateChannel = UpdateChannel.Prerelease; + [ObservableProperty] + private string _subscribedBranchInput = string.Empty; + [ObservableProperty] private string _gitHubPatInput = string.Empty; @@ -498,6 +501,7 @@ private void LoadSettings() CasRootPath = settings.CasConfiguration.CasRootPath; EnableAutomaticGc = settings.CasConfiguration.EnableAutomaticGc; SelectedUpdateChannel = settings.UpdateChannel; + SubscribedBranchInput = settings.SubscribedBranch ?? string.Empty; MaxCacheSizeGB = settings.CasConfiguration.MaxCacheSizeBytes / ConversionConstants.BytesPerGigabyte; CasMaxConcurrentOperations = settings.CasConfiguration.MaxConcurrentOperations; CasVerifyIntegrity = settings.CasConfiguration.VerifyIntegrity; @@ -539,6 +543,7 @@ private async Task SaveSettings() settings.EnableDetailedLogging = EnableDetailedLogging; settings.DefaultWorkspaceStrategy = DefaultWorkspaceStrategy; settings.UpdateChannel = SelectedUpdateChannel; + settings.SubscribedBranch = string.IsNullOrWhiteSpace(SubscribedBranchInput) ? null : SubscribedBranchInput; settings.DownloadBufferSize = (int)(DownloadBufferSizeKB * ConversionConstants.BytesPerKilobyte); // Convert KB to bytes settings.DownloadTimeoutSeconds = DownloadTimeoutSeconds; settings.DownloadUserAgent = DownloadUserAgent; diff --git a/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml b/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml index a06cf468..ea2a7286 100644 --- a/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml +++ b/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml @@ -287,7 +287,7 @@ - + - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - -