Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GenHub/GenHub.Core/Constants/ApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static class ApiConstants
/// <summary>
/// Format string for GitHub API Workflow Runs endpoint (owner, repo, branch).
/// </summary>
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";

/// <summary>
/// Format string for GitHub API Latest Workflow Runs endpoint (owner, repo).
Expand Down
6 changes: 6 additions & 0 deletions GenHub/GenHub.Core/Models/Common/UserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ public bool IsExplicitlySet(string propertyName)
/// </summary>
public int? SubscribedPrNumber { get; set; }

/// <summary>
/// Gets or sets the subscribed branch name for update notifications (e.g. "development").
/// </summary>
public string? SubscribedBranch { get; set; }

/// <summary>
/// Gets or sets the last dismissed update version to prevent repeated notifications.
/// </summary>
Expand Down Expand Up @@ -143,6 +148,7 @@ public object Clone()
ApplicationDataPath = ApplicationDataPath,
UpdateChannel = UpdateChannel,
SubscribedPrNumber = SubscribedPrNumber,
SubscribedBranch = SubscribedBranch,
DismissedUpdateVersion = DismissedUpdateVersion,
ContentDirectories = ContentDirectories != null ? new List<string>(ContentDirectories) : null,
GitHubDiscoveryRepositories = GitHubDiscoveryRepositories != null ? new List<string>(GitHubDiscoveryRepositories) : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public void Constructor_CreatesValidInstance()
mockProfileEditorFacade.Object,
mockVelopackUpdateManager.Object,
CreateProfileResourceService(),
mockNotificationService.Object,
mockLogger.Object);

// Assert
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -148,6 +150,7 @@ public async Task ScanAndCreateProfilesAsync_CanBeCalled()
mockProfileEditorFacade.Object,
mockVelopackUpdateManager.Object,
CreateProfileResourceService(),
mockNotificationService.Object,
mockLogger.Object);

// Act & Assert
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
152 changes: 39 additions & 113 deletions GenHub/GenHub/Common/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,13 +38,13 @@
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;

Check warning on line 46 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

Check warning on line 46 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

Check warning on line 46 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

Check warning on line 46 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Linux

Check warning on line 46 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Linux

[ObservableProperty]
private bool _hasUpdateAvailable;


/// <summary>
/// Initializes a new instance of the <see cref="MainViewModel"/> class.
Expand All @@ -57,6 +60,7 @@
/// <param name="profileEditorFacade">Profile editor facade for automatic profile creation.</param>
/// <param name="velopackUpdateManager">The Velopack update manager for checking updates.</param>
/// <param name="profileResourceService">Service for accessing profile resources.</param>
/// <param name="notificationService">Service for showing notifications.</param>
/// <param name="logger">Logger instance.</param>
public MainViewModel(
GameProfileLauncherViewModel gameProfilesViewModel,
Expand All @@ -70,6 +74,7 @@
IProfileEditorFacade profileEditorFacade,
IVelopackUpdateManager velopackUpdateManager,
ProfileResourceService profileResourceService,
INotificationService notificationService,
ILogger<MainViewModel>? logger = null)
{
GameProfilesViewModel = gameProfilesViewModel;
Expand All @@ -83,6 +88,7 @@
_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
Expand Down Expand Up @@ -180,31 +186,8 @@
{
SelectedTab = tab;
}

Check warning on line 189 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

Check warning on line 189 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

Check warning on line 189 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

Check warning on line 189 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Linux

Check warning on line 189 in GenHub/GenHub/Common/ViewModels/MainViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Linux

/// <summary>
/// Shows the update notification dialog.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
[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");
}
}


/// <summary>
/// Performs asynchronous initialization for the shell and all tabs.
Expand Down Expand Up @@ -372,115 +355,58 @@

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;
}
}

Expand Down
31 changes: 1 addition & 30 deletions GenHub/GenHub/Common/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,29 +87,7 @@
<Setter Property="RenderTransform" Value="none" />
</Style>

<!-- Update notification button style -->
<Style Selector="Button.update-notification">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="20" />
<Setter Property="Padding" Value="12" />
<Setter Property="Width" Value="44" />
<Setter Property="Height" Value="44" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Transitions">
<Setter.Value>
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.2" />
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2" />
</Transitions>
</Setter.Value>
</Setter>
</Style>

<Style Selector="Button.update-notification:pointerover">
<Setter Property="Background" Value="#2A2A2A" />
<Setter Property="RenderTransform" Value="scale(1.1)" />
</Style>

<!-- Header gradient background -->
<Style Selector="Border.header-background">
Expand Down Expand Up @@ -190,14 +168,7 @@
</StackPanel>
<!-- branding and update button - Right side -->
<Grid Grid.Column="2" ColumnDefinitions="Auto,Auto" VerticalAlignment="Center">
<!-- Update notification button - Left side -->
<Button Grid.Column="0" Classes="update-notification" Command="{Binding ShowUpdateDialogCommand}" IsVisible="{Binding HasUpdateAvailable}" ToolTip.Tip="Update available - click to download" VerticalAlignment="Center" Margin="0,0,12,0">
<Panel>
<Ellipse Width="20" Height="20" Fill="#4CAF50" HorizontalAlignment="Center" VerticalAlignment="Center" />
<PathIcon Data="M7,10L12,15L17,10H7Z" Width="12" Height="12" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Ellipse Width="8" Height="8" Fill="#FF4500" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,-2,-2,0" />
</Panel>
</Button>

<!-- Logo and title section - Right side -->
<StackPanel Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,0,0,0">
<Image Source="avares://GenHub/Assets/Logos/generalshub-logo.png" Width="90" Height="90" HorizontalAlignment="Center" Margin="0,-10,0,-15" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public interface IVelopackUpdateManager
/// <returns>ArtifactUpdateInfo if an artifact update is available, otherwise null.</returns>
Task<ArtifactUpdateInfo?> CheckForArtifactUpdatesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets a list of available branches from the repository.
/// Requires a GitHub PAT with repo access.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of branch names.</returns>
Task<IReadOnlyList<string>> GetBranchesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets a list of open pull requests with available CI artifacts.
/// Requires a GitHub PAT with repo access.
Expand Down Expand Up @@ -104,6 +112,15 @@ public interface IVelopackUpdateManager
/// </summary>
bool IsPrMergedOrClosed { get; }

/// <summary>
/// Downloads and installs a specific artifact.
/// </summary>
/// <param name="artifactInfo">The artifact information to install.</param>
/// <param name="progress">Progress reporter.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the installation operation.</returns>
Task InstallArtifactAsync(ArtifactUpdateInfo artifactInfo, IProgress<UpdateProgress>? progress = null, CancellationToken cancellationToken = default);

/// <summary>
/// Downloads and installs a PR artifact.
/// </summary>
Expand Down
Loading
Loading