Add Update Checker

This commit is contained in:
Canon
2026-02-10 18:30:04 +08:00
parent fedc202aee
commit 806adafd90
6 changed files with 141 additions and 4 deletions
+6 -1
View File
@@ -28,12 +28,17 @@ public class ScanCommand : AsyncCommand<ModPathSettings>
var gitHubApiService = new GitHubApiService(); var gitHubApiService = new GitHubApiService();
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService); var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
// Start update check in background (non-blocking)
var updateCheckService = new UpdateCheckService(gitHubApiService.HttpClient);
var updateCheckTask = updateCheckService.CheckForUpdateAsync(cancellationToken);
// Run interactive menu // Run interactive menu
var menu = new InteractiveMenu( var menu = new InteractiveMenu(
gitService, gitService,
gitHubApiService, gitHubApiService,
modDiscoveryService, modDiscoveryService,
modsDirectory); modsDirectory,
updateCheckTask);
await menu.RunAsync(cancellationToken); await menu.RunAsync(cancellationToken);
+15
View File
@@ -31,6 +31,10 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
var gitHubApiService = new GitHubApiService(); var gitHubApiService = new GitHubApiService();
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService); var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
// Start update check in background (non-blocking)
var updateCheckService = new UpdateCheckService(gitHubApiService.HttpClient);
var updateCheckTask = updateCheckService.CheckForUpdateAsync(cancellationToken);
// Discover mods // Discover mods
var mods = await ProgressReporter.WithStatusAsync( var mods = await ProgressReporter.WithStatusAsync(
"Scanning mods directory...", "Scanning mods directory...",
@@ -76,6 +80,17 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
ModTableRenderer.RenderUpdateSummary(results); ModTableRenderer.RenderUpdateSummary(results);
// Show update notification if available
var updateResult = await updateCheckTask;
if (updateResult != null)
{
AnsiConsole.WriteLine();
var label = updateResult.IsPrerelease ? "Pre-release available" : "Update available";
AnsiConsole.MarkupLine(
$"[yellow bold]{label}: v{updateResult.LatestVersion}[/] [grey](current: {updateResult.CurrentVersion})[/]");
AnsiConsole.MarkupLine($"[grey]Download: {updateResult.ReleaseUrl}[/]");
}
var failCount = results.Count(r => !r.Success); var failCount = results.Count(r => !r.Success);
return failCount > 0 ? 1 : 0; return failCount > 0 ? 1 : 0;
} }
+2 -1
View File
@@ -15,13 +15,14 @@ public class GitHubApiService : IGitHubApiService, IDisposable
private IReadOnlyList<RemoteRepo>? _cachedRepos; private IReadOnlyList<RemoteRepo>? _cachedRepos;
private DateTimeOffset _cacheTime = DateTimeOffset.MinValue; private DateTimeOffset _cacheTime = DateTimeOffset.MinValue;
public HttpClient HttpClient => _httpClient;
public int? RemainingRateLimit { get; private set; } public int? RemainingRateLimit { get; private set; }
public DateTimeOffset? RateLimitReset { get; private set; } public DateTimeOffset? RateLimitReset { get; private set; }
public GitHubApiService(HttpClient? httpClient = null) public GitHubApiService(HttpClient? httpClient = null)
{ {
_httpClient = httpClient ?? new HttpClient(); _httpClient = httpClient ?? new HttpClient();
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("FCPModUpdater/1.0"); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"FCPModUpdater/{AppVersion.SemanticVersion}");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json"); _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
+96
View File
@@ -0,0 +1,96 @@
using System.Net.Http.Json;
using FCPModUpdater.Models;
namespace FCPModUpdater.Services;
public class UpdateCheckService
{
private const string RepoOwner = "FalloutCollaborationProject";
private const string RepoName = "FCP-Mod-Updater";
private const string BaseUrl = "https://api.github.com";
private readonly HttpClient _httpClient;
public UpdateCheckService(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Check if a newer version is available. Returns null if check fails or no update needed.
/// Checks both stable and prerelease versions.
/// </summary>
public async Task<UpdateCheckResult?> CheckForUpdateAsync(CancellationToken ct = default)
{
try
{
var url = $"{BaseUrl}/repos/{RepoOwner}/{RepoName}/releases?per_page=15";
var releases = await _httpClient.GetFromJsonAsync<List<ReleaseInfo>>(url, ct);
if (releases is null || releases.Count == 0)
return null;
// Find the release with the highest semver (stable or prerelease)
ReleaseInfo? newest = null;
Version? newestVersion = null;
foreach (var release in releases)
{
if (release.Draft)
continue;
var tagBase = GetBaseVersion(release.TagName.TrimStart('v'));
if (!Version.TryParse(tagBase, out var ver))
continue;
if (newestVersion is null || ver > newestVersion)
{
newestVersion = ver;
newest = release;
}
}
if (newest is null || newestVersion is null)
return null;
var currentBase = GetBaseVersion(AppVersion.SemanticVersion);
if (!Version.TryParse(currentBase, out var currentVer))
return null;
if (newestVersion > currentVer)
{
var latestTag = newest.TagName.TrimStart('v');
return new UpdateCheckResult(
CurrentVersion: AppVersion.InformationalVersion,
LatestVersion: latestTag,
ReleaseUrl: newest.HtmlUrl,
ReleaseName: newest.Name,
PublishedAt: newest.PublishedAt,
IsPrerelease: newest.Prerelease
);
}
return null;
}
catch
{
// Update check is non-critical — fail silently
return null;
}
}
private static string GetBaseVersion(string version)
{
var dashIndex = version.IndexOf('-');
return dashIndex >= 0 ? version[..dashIndex] : version;
}
}
public record UpdateCheckResult(
string CurrentVersion,
string LatestVersion,
string ReleaseUrl,
string ReleaseName,
DateTimeOffset PublishedAt,
bool IsPrerelease
);
+20 -1
View File
@@ -10,19 +10,23 @@ public class InteractiveMenu
private readonly IGitHubApiService _gitHubApiService; private readonly IGitHubApiService _gitHubApiService;
private readonly IModDiscoveryService _modDiscoveryService; private readonly IModDiscoveryService _modDiscoveryService;
private readonly string _modsDirectory; private readonly string _modsDirectory;
private readonly Task<UpdateCheckResult?> _updateCheckTask;
private IReadOnlyList<InstalledMod> _mods = []; private IReadOnlyList<InstalledMod> _mods = [];
private bool _updateNotificationShown;
public InteractiveMenu( public InteractiveMenu(
IGitService gitService, IGitService gitService,
IGitHubApiService gitHubApiService, IGitHubApiService gitHubApiService,
IModDiscoveryService modDiscoveryService, IModDiscoveryService modDiscoveryService,
string modsDirectory) string modsDirectory,
Task<UpdateCheckResult?> updateCheckTask)
{ {
_gitService = gitService; _gitService = gitService;
_gitHubApiService = gitHubApiService; _gitHubApiService = gitHubApiService;
_modDiscoveryService = modDiscoveryService; _modDiscoveryService = modDiscoveryService;
_modsDirectory = modsDirectory; _modsDirectory = modsDirectory;
_updateCheckTask = updateCheckTask;
} }
public async Task RunAsync(CancellationToken ct = default) public async Task RunAsync(CancellationToken ct = default)
@@ -35,6 +39,21 @@ public class InteractiveMenu
AnsiConsole.Clear(); AnsiConsole.Clear();
ModTableRenderer.RenderModTable(_mods, _gitHubApiService.RemainingRateLimit, ModTableRenderer.RenderModTable(_mods, _gitHubApiService.RemainingRateLimit,
_gitHubApiService.RateLimitReset); _gitHubApiService.RateLimitReset);
// Show update notification once, non-blocking
if (!_updateNotificationShown && _updateCheckTask.IsCompleted)
{
_updateNotificationShown = true;
var updateResult = await _updateCheckTask;
if (updateResult != null)
{
var label = updateResult.IsPrerelease ? "Pre-release available" : "Update available";
AnsiConsole.MarkupLine(
$"[yellow bold]{label}: v{updateResult.LatestVersion}[/] [grey](current: {updateResult.CurrentVersion})[/]");
AnsiConsole.MarkupLine($"[grey]Download: {updateResult.ReleaseUrl}[/]");
}
}
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();
var choice = AnsiConsole.Prompt( var choice = AnsiConsole.Prompt(
+2 -1
View File
@@ -29,6 +29,7 @@
"Spectre.Console": "0.53.1" "Spectre.Console": "0.53.1"
} }
} }
} },
"net10.0/linux-x64": {}
} }
} }