mirror of
https://github.com/FalloutCollaborationProject/FCP-Mod-Updater.git
synced 2026-07-27 17:04:05 -07:00
Add Update Checker
This commit is contained in:
@@ -28,12 +28,17 @@ public class ScanCommand : AsyncCommand<ModPathSettings>
|
||||
var gitHubApiService = new 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
|
||||
var menu = new InteractiveMenu(
|
||||
gitService,
|
||||
gitHubApiService,
|
||||
modDiscoveryService,
|
||||
modsDirectory);
|
||||
modsDirectory,
|
||||
updateCheckTask);
|
||||
|
||||
await menu.RunAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
|
||||
var gitHubApiService = new 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
|
||||
var mods = await ProgressReporter.WithStatusAsync(
|
||||
"Scanning mods directory...",
|
||||
@@ -76,6 +80,17 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
|
||||
|
||||
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);
|
||||
return failCount > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@ public class GitHubApiService : IGitHubApiService, IDisposable
|
||||
private IReadOnlyList<RemoteRepo>? _cachedRepos;
|
||||
private DateTimeOffset _cacheTime = DateTimeOffset.MinValue;
|
||||
|
||||
public HttpClient HttpClient => _httpClient;
|
||||
public int? RemainingRateLimit { get; private set; }
|
||||
public DateTimeOffset? RateLimitReset { get; private set; }
|
||||
|
||||
public GitHubApiService(HttpClient? httpClient = null)
|
||||
{
|
||||
_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");
|
||||
|
||||
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
|
||||
|
||||
@@ -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
@@ -10,19 +10,23 @@ public class InteractiveMenu
|
||||
private readonly IGitHubApiService _gitHubApiService;
|
||||
private readonly IModDiscoveryService _modDiscoveryService;
|
||||
private readonly string _modsDirectory;
|
||||
private readonly Task<UpdateCheckResult?> _updateCheckTask;
|
||||
|
||||
private IReadOnlyList<InstalledMod> _mods = [];
|
||||
private bool _updateNotificationShown;
|
||||
|
||||
public InteractiveMenu(
|
||||
IGitService gitService,
|
||||
IGitHubApiService gitHubApiService,
|
||||
IModDiscoveryService modDiscoveryService,
|
||||
string modsDirectory)
|
||||
string modsDirectory,
|
||||
Task<UpdateCheckResult?> updateCheckTask)
|
||||
{
|
||||
_gitService = gitService;
|
||||
_gitHubApiService = gitHubApiService;
|
||||
_modDiscoveryService = modDiscoveryService;
|
||||
_modsDirectory = modsDirectory;
|
||||
_updateCheckTask = updateCheckTask;
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken ct = default)
|
||||
@@ -35,6 +39,21 @@ public class InteractiveMenu
|
||||
AnsiConsole.Clear();
|
||||
ModTableRenderer.RenderModTable(_mods, _gitHubApiService.RemainingRateLimit,
|
||||
_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();
|
||||
|
||||
var choice = AnsiConsole.Prompt(
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@
|
||||
"Spectre.Console": "0.53.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user