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 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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 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
@@ -29,6 +29,7 @@
|
|||||||
"Spectre.Console": "0.53.1"
|
"Spectre.Console": "0.53.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"net10.0/linux-x64": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user