Skip to content

Commit ffc7cb9

Browse files
committed
fix: resolve branch subscription logic and Windows artifact selection
1 parent 755e532 commit ffc7cb9

File tree

15 files changed

+1011
-548
lines changed

15 files changed

+1011
-548
lines changed

GenHub/GenHub.Core/Constants/ApiConstants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static class ApiConstants
4747
/// <summary>
4848
/// Format string for GitHub API Workflow Runs endpoint (owner, repo, branch).
4949
/// </summary>
50-
public const string GitHubApiWorkflowRunsFormat = "https://api.github.com/repos/{0}/{1}/actions/runs?status=success&branch={2}&per_page=10";
50+
public const string GitHubApiWorkflowRunsFormat = "https://api.github.com/repos/{0}/{1}/actions/runs?status=success&branch={2}&event=push&per_page=10";
5151

5252
/// <summary>
5353
/// Format string for GitHub API Latest Workflow Runs endpoint (owner, repo).

GenHub/GenHub.Core/Models/Common/UserSettings.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ public bool IsExplicitlySet(string propertyName)
111111
/// </summary>
112112
public int? SubscribedPrNumber { get; set; }
113113

114+
/// <summary>
115+
/// Gets or sets the subscribed branch name for update notifications (e.g. "development").
116+
/// </summary>
117+
public string? SubscribedBranch { get; set; }
118+
114119
/// <summary>
115120
/// Gets or sets the last dismissed update version to prevent repeated notifications.
116121
/// </summary>
@@ -143,6 +148,7 @@ public object Clone()
143148
ApplicationDataPath = ApplicationDataPath,
144149
UpdateChannel = UpdateChannel,
145150
SubscribedPrNumber = SubscribedPrNumber,
151+
SubscribedBranch = SubscribedBranch,
146152
DismissedUpdateVersion = DismissedUpdateVersion,
147153
ContentDirectories = ContentDirectories != null ? new List<string>(ContentDirectories) : null,
148154
GitHubDiscoveryRepositories = GitHubDiscoveryRepositories != null ? new List<string>(GitHubDiscoveryRepositories) : null,

GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public void Constructor_CreatesValidInstance()
6969
mockProfileEditorFacade.Object,
7070
mockVelopackUpdateManager.Object,
7171
CreateProfileResourceService(),
72+
mockNotificationService.Object,
7273
mockLogger.Object);
7374

7475
// Assert
@@ -111,6 +112,7 @@ public void SelectTabCommand_SetsSelectedTab(NavigationTab tab)
111112
mockProfileEditorFacade.Object,
112113
mockVelopackUpdateManager.Object,
113114
CreateProfileResourceService(),
115+
mockNotificationService.Object,
114116
mockLogger.Object);
115117
vm.SelectTabCommand.Execute(tab);
116118
Assert.Equal(tab, vm.SelectedTab);
@@ -148,6 +150,7 @@ public async Task ScanAndCreateProfilesAsync_CanBeCalled()
148150
mockProfileEditorFacade.Object,
149151
mockVelopackUpdateManager.Object,
150152
CreateProfileResourceService(),
153+
mockNotificationService.Object,
151154
mockLogger.Object);
152155

153156
// Act & Assert
@@ -189,6 +192,7 @@ public async Task InitializeAsync_MultipleCallsAreSafe()
189192
mockProfileEditorFacade.Object,
190193
mockVelopackUpdateManager.Object,
191194
CreateProfileResourceService(),
195+
mockNotificationService.Object,
192196
mockLogger.Object);
193197
await vm.InitializeAsync(); // Should not throw
194198
Assert.True(true);
@@ -229,6 +233,7 @@ public void CurrentTabViewModel_ReturnsCorrectViewModel(NavigationTab tab)
229233
mockProfileEditorFacade.Object,
230234
mockVelopackUpdateManager.Object,
231235
CreateProfileResourceService(),
236+
mockNotificationService.Object,
232237
mockLogger.Object);
233238
vm.SelectTabCommand.Execute(tab);
234239
var currentViewModel = vm.CurrentTabViewModel;

GenHub/GenHub/Common/ViewModels/MainViewModel.cs

Lines changed: 39 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
using System.Threading;
55
using System.Threading.Tasks;
66
using Avalonia.Controls;
7+
using Avalonia.Threading;
78
using CommunityToolkit.Mvvm.ComponentModel;
89
using CommunityToolkit.Mvvm.Input;
910
using GenHub.Core.Constants;
1011
using GenHub.Core.Interfaces.Common;
1112
using GenHub.Core.Interfaces.GameInstallations;
1213
using GenHub.Core.Interfaces.GameProfiles;
14+
using GenHub.Core.Interfaces.Notifications;
1315
using GenHub.Core.Models.Enums;
1416
using GenHub.Core.Models.GameProfile;
17+
using GenHub.Core.Models.Notifications;
1518
using GenHub.Features.AppUpdate.Interfaces;
1619
using GenHub.Features.Downloads.ViewModels;
1720
using GenHub.Features.GameProfiles.Services;
@@ -35,13 +38,13 @@ public partial class MainViewModel : ObservableObject, IDisposable
3538
private readonly IProfileEditorFacade _profileEditorFacade;
3639
private readonly IVelopackUpdateManager _velopackUpdateManager;
3740
private readonly ProfileResourceService _profileResourceService;
41+
private readonly INotificationService _notificationService;
3842
private readonly CancellationTokenSource _initializationCts = new();
3943

4044
[ObservableProperty]
4145
private NavigationTab _selectedTab = NavigationTab.GameProfiles;
4246

43-
[ObservableProperty]
44-
private bool _hasUpdateAvailable;
47+
4548

4649
/// <summary>
4750
/// Initializes a new instance of the <see cref="MainViewModel"/> class.
@@ -57,6 +60,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
5760
/// <param name="profileEditorFacade">Profile editor facade for automatic profile creation.</param>
5861
/// <param name="velopackUpdateManager">The Velopack update manager for checking updates.</param>
5962
/// <param name="profileResourceService">Service for accessing profile resources.</param>
63+
/// <param name="notificationService">Service for showing notifications.</param>
6064
/// <param name="logger">Logger instance.</param>
6165
public MainViewModel(
6266
GameProfileLauncherViewModel gameProfilesViewModel,
@@ -70,6 +74,7 @@ public MainViewModel(
7074
IProfileEditorFacade profileEditorFacade,
7175
IVelopackUpdateManager velopackUpdateManager,
7276
ProfileResourceService profileResourceService,
77+
INotificationService notificationService,
7378
ILogger<MainViewModel>? logger = null)
7479
{
7580
GameProfilesViewModel = gameProfilesViewModel;
@@ -83,6 +88,7 @@ public MainViewModel(
8388
_profileEditorFacade = profileEditorFacade ?? throw new ArgumentNullException(nameof(profileEditorFacade));
8489
_velopackUpdateManager = velopackUpdateManager ?? throw new ArgumentNullException(nameof(velopackUpdateManager));
8590
_profileResourceService = profileResourceService ?? throw new ArgumentNullException(nameof(profileResourceService));
91+
_notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
8692
_logger = logger;
8793

8894
// Load initial settings using unified configuration
@@ -181,30 +187,7 @@ public void SelectTab(NavigationTab tab)
181187
SelectedTab = tab;
182188
}
183189

184-
/// <summary>
185-
/// Shows the update notification dialog.
186-
/// </summary>
187-
/// <returns>A task representing the asynchronous operation.</returns>
188-
[RelayCommand]
189-
public async Task ShowUpdateDialogAsync()
190-
{
191-
try
192-
{
193-
var mainWindow = GetMainWindow();
194-
if (mainWindow != null)
195-
{
196-
await GenHub.Features.AppUpdate.Views.UpdateNotificationWindow.ShowAsync(mainWindow);
197-
}
198-
else
199-
{
200-
_logger?.LogWarning("Cannot show update dialog - main window not found");
201-
}
202-
}
203-
catch (Exception ex)
204-
{
205-
_logger?.LogError(ex, "Failed to show update dialog");
206-
}
207-
}
190+
208191

209192
/// <summary>
210193
/// Performs asynchronous initialization for the shell and all tabs.
@@ -372,115 +355,58 @@ private async Task CheckForUpdatesAsync(CancellationToken cancellationToken = de
372355

373356
try
374357
{
375-
// Check if subscribed to a PR - if so, check for PR artifact updates instead
376358
var settings = _userSettingsService.Get();
359+
360+
// Push settings to update manager (important context for other components)
377361
if (settings.SubscribedPrNumber.HasValue)
378362
{
379-
_logger?.LogDebug("User subscribed to PR #{PrNumber}, checking for PR artifact updates", settings.SubscribedPrNumber);
380363
_velopackUpdateManager.SubscribedPrNumber = settings.SubscribedPrNumber;
364+
}
381365

382-
// Fetch PR list to populate artifact info
383-
var prs = await _velopackUpdateManager.GetOpenPullRequestsAsync(cancellationToken);
384-
var subscribedPr = prs.FirstOrDefault(p => p.Number == settings.SubscribedPrNumber);
366+
// Check if subscribed to a specific branch
367+
if (!string.IsNullOrEmpty(settings.SubscribedBranch))
368+
{
369+
_logger?.LogDebug("User subscribed to branch '{Branch}', checking for artifact updates", settings.SubscribedBranch);
370+
_velopackUpdateManager.SubscribedBranch = settings.SubscribedBranch;
371+
372+
// Ensure PR number is cleared to avoid ambiguity
373+
_velopackUpdateManager.SubscribedPrNumber = null;
374+
375+
var artifactUpdate = await _velopackUpdateManager.CheckForArtifactUpdatesAsync(cancellationToken);
385376

386-
if (subscribedPr?.LatestArtifact != null)
377+
if (artifactUpdate != null)
387378
{
388-
// Compare versions (strip build metadata)
389-
var currentVersionBase = AppConstants.AppVersion.Split('+')[0];
390-
var prVersionBase = subscribedPr.LatestArtifact.Version.Split('+')[0];
379+
var newVersionBase = artifactUpdate.Version.Split('+')[0];
380+
var dismissedVersionBase = settings.DismissedUpdateVersion?.Split('+')[0];
391381

392-
if (!string.Equals(prVersionBase, currentVersionBase, StringComparison.OrdinalIgnoreCase))
382+
if (string.IsNullOrEmpty(dismissedVersionBase) ||
383+
!string.Equals(newVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase))
393384
{
394-
// Check if this PR version was dismissed
395-
var dismissedVersionBase = settings.DismissedUpdateVersion?.Split('+')[0];
385+
_logger?.LogInformation("Branch '{Branch}' artifact update available: {Version}", settings.SubscribedBranch, newVersionBase);
396386

397-
if (string.IsNullOrEmpty(dismissedVersionBase) ||
398-
!string.Equals(prVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase))
399-
{
400-
_logger?.LogInformation("PR #{PrNumber} artifact update available: {Version}", subscribedPr.Number, prVersionBase);
401-
HasUpdateAvailable = true;
402-
return;
403-
}
404-
else
387+
await Dispatcher.UIThread.InvokeAsync(() =>
405388
{
406-
_logger?.LogDebug("PR #{PrNumber} artifact update {Version} was dismissed", subscribedPr.Number, prVersionBase);
407-
HasUpdateAvailable = false;
408-
return;
409-
}
389+
_notificationService.Show(new NotificationMessage(
390+
NotificationType.Info,
391+
"Update Available",
392+
$"A new version ({newVersionBase}) is available on branch '{settings.SubscribedBranch}'.",
393+
null,
394+
"View Updates",
395+
() => { SettingsViewModel.OpenUpdateWindowCommand.Execute(null); }));
396+
});
397+
return;
410398
}
411399
else
412400
{
413-
_logger?.LogDebug("Already on latest PR #{PrNumber} artifact version", subscribedPr.Number);
414-
HasUpdateAvailable = false;
401+
_logger?.LogDebug("Branch '{Branch}' artifact update {Version} was dismissed", settings.SubscribedBranch, newVersionBase);
415402
return;
416403
}
417404
}
418-
else
419-
{
420-
_logger?.LogDebug("PR #{PrNumber} has no artifacts or PR not found", settings.SubscribedPrNumber);
421-
422-
// Fall through to check main branch updates
423-
}
424-
}
425-
426-
// Check main branch updates (if not subscribed to PR or PR has no artifacts)
427-
var updateInfo = await _velopackUpdateManager.CheckForUpdatesAsync(cancellationToken);
428-
429-
// Check both UpdateInfo (from installed app) and GitHub API flag (works in debug too)
430-
var hasUpdate = updateInfo != null || _velopackUpdateManager.HasUpdateAvailableFromGitHub;
431-
432-
if (hasUpdate)
433-
{
434-
string? latestVersion = null;
435-
436-
if (updateInfo != null)
437-
{
438-
latestVersion = updateInfo.TargetFullRelease.Version.ToString();
439-
_logger?.LogInformation("Update available: {Current} → {Latest}", AppConstants.AppVersion, latestVersion);
440-
}
441-
else if (_velopackUpdateManager.LatestVersionFromGitHub != null)
442-
{
443-
latestVersion = _velopackUpdateManager.LatestVersionFromGitHub;
444-
_logger?.LogInformation("Update available from GitHub API: {Version}", latestVersion);
445-
}
446-
447-
// Strip build metadata for comparison (everything after '+')
448-
var latestVersionBase = latestVersion?.Split('+')[0];
449-
var currentVersionBase = AppConstants.AppVersion.Split('+')[0];
450-
451-
// Check if this version was dismissed by the user
452-
var settings2 = _userSettingsService.Get();
453-
var dismissedVersionBase = settings2.DismissedUpdateVersion?.Split('+')[0];
454-
455-
if (!string.IsNullOrEmpty(latestVersionBase) &&
456-
string.Equals(latestVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase))
457-
{
458-
_logger?.LogDebug("Update {Version} was dismissed by user, hiding notification", latestVersionBase);
459-
HasUpdateAvailable = false;
460-
}
461-
462-
// Also check if we're already on this version (ignoring build metadata)
463-
else if (!string.IsNullOrEmpty(latestVersionBase) &&
464-
string.Equals(latestVersionBase, currentVersionBase, StringComparison.OrdinalIgnoreCase))
465-
{
466-
_logger?.LogDebug("Already on version {Version} (ignoring build metadata), hiding notification", latestVersionBase);
467-
HasUpdateAvailable = false;
468-
}
469-
else
470-
{
471-
HasUpdateAvailable = true;
472-
}
473-
}
474-
else
475-
{
476-
_logger?.LogDebug("No updates available");
477-
HasUpdateAvailable = false;
478405
}
479406
}
480407
catch (Exception ex)
481408
{
482409
_logger?.LogError(ex, "Exception in CheckForUpdatesAsync");
483-
HasUpdateAvailable = false;
484410
}
485411
}
486412

GenHub/GenHub/Common/Views/MainView.axaml

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -87,29 +87,7 @@
8787
<Setter Property="RenderTransform" Value="none" />
8888
</Style>
8989

90-
<!-- Update notification button style -->
91-
<Style Selector="Button.update-notification">
92-
<Setter Property="Background" Value="Transparent" />
93-
<Setter Property="BorderThickness" Value="0" />
94-
<Setter Property="CornerRadius" Value="20" />
95-
<Setter Property="Padding" Value="12" />
96-
<Setter Property="Width" Value="44" />
97-
<Setter Property="Height" Value="44" />
98-
<Setter Property="Cursor" Value="Hand" />
99-
<Setter Property="Transitions">
100-
<Setter.Value>
101-
<Transitions>
102-
<BrushTransition Property="Background" Duration="0:0:0.2" />
103-
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2" />
104-
</Transitions>
105-
</Setter.Value>
106-
</Setter>
107-
</Style>
10890

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

11492
<!-- Header gradient background -->
11593
<Style Selector="Border.header-background">
@@ -190,14 +168,7 @@
190168
</StackPanel>
191169
<!-- branding and update button - Right side -->
192170
<Grid Grid.Column="2" ColumnDefinitions="Auto,Auto" VerticalAlignment="Center">
193-
<!-- Update notification button - Left side -->
194-
<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">
195-
<Panel>
196-
<Ellipse Width="20" Height="20" Fill="#4CAF50" HorizontalAlignment="Center" VerticalAlignment="Center" />
197-
<PathIcon Data="M7,10L12,15L17,10H7Z" Width="12" Height="12" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
198-
<Ellipse Width="8" Height="8" Fill="#FF4500" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,-2,-2,0" />
199-
</Panel>
200-
</Button>
171+
201172
<!-- Logo and title section - Right side -->
202173
<StackPanel Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,0,0,0">
203174
<Image Source="avares://GenHub/Assets/Logos/generalshub-logo.png" Width="90" Height="90" HorizontalAlignment="Center" Margin="0,-10,0,-15" />

GenHub/GenHub/Features/AppUpdate/Interfaces/IVelopackUpdateManager.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ public interface IVelopackUpdateManager
2828
/// <returns>ArtifactUpdateInfo if an artifact update is available, otherwise null.</returns>
2929
Task<ArtifactUpdateInfo?> CheckForArtifactUpdatesAsync(CancellationToken cancellationToken = default);
3030

31+
/// <summary>
32+
/// Gets a list of available branches from the repository.
33+
/// Requires a GitHub PAT with repo access.
34+
/// </summary>
35+
/// <param name="cancellationToken">Cancellation token.</param>
36+
/// <returns>List of branch names.</returns>
37+
Task<IReadOnlyList<string>> GetBranchesAsync(CancellationToken cancellationToken = default);
38+
3139
/// <summary>
3240
/// Gets a list of open pull requests with available CI artifacts.
3341
/// Requires a GitHub PAT with repo access.
@@ -104,6 +112,15 @@ public interface IVelopackUpdateManager
104112
/// </summary>
105113
bool IsPrMergedOrClosed { get; }
106114

115+
/// <summary>
116+
/// Downloads and installs a specific artifact.
117+
/// </summary>
118+
/// <param name="artifactInfo">The artifact information to install.</param>
119+
/// <param name="progress">Progress reporter.</param>
120+
/// <param name="cancellationToken">Cancellation token.</param>
121+
/// <returns>A task representing the installation operation.</returns>
122+
Task InstallArtifactAsync(ArtifactUpdateInfo artifactInfo, IProgress<UpdateProgress>? progress = null, CancellationToken cancellationToken = default);
123+
107124
/// <summary>
108125
/// Downloads and installs a PR artifact.
109126
/// </summary>

0 commit comments

Comments
 (0)