Abstract update checker

This commit is contained in:
Yoshi Askharoun
2025-08-20 08:30:27 -07:00
parent 3769b79b7a
commit e5cbbcb45f
5 changed files with 104 additions and 42 deletions
@@ -41,7 +41,8 @@
<Rectangle Height="5" Width="109" Margin="0,-16,0,0" Grid.ColumnSpan="2"
VerticalAlignment="Top" HorizontalAlignment="Left"
Fill="{StaticResource ZuneHorizontalGradientAlt}"/>
<TextBlock Text="{Binding Title}" d:Text="WINDOWS LIVE ID" FontSize="28" Margin="0,-10,0,12"
<TextBlock Text="{Binding Title}" d:Text="WINDOWS LIVE ID" FontSize="28"
Grid.ColumnSpan="2" Margin="0,-10,0,12"
Foreground="{StaticResource ZuneMediumTextBrush}"
Visibility="{Binding Title, Converter={StaticResource NotNullToVis}}"/>
+1
View File
@@ -38,6 +38,7 @@
</TextBlock>
<Button Content="CHECK FOR UPDATES" Click="UpdateCheckButton_Click"
Margin="0,8,0,12" Padding="8,0" HorizontalAlignment="Left"
IsEnabled="{Binding EnableCheckForUpdates}"
Style="{StaticResource ActionButtonStyle}"/>
<TextBlock Style="{StaticResource ZuneBodyTextBlockStyle}">
+19 -41
View File
@@ -1,16 +1,13 @@
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Octokit;
using Syroot.Windows.IO;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Navigation;
using ZuneModCore;
using ZuneModdingHelper.Messages;
using ZuneModdingHelper.Services;
namespace ZuneModdingHelper.Pages
{
@@ -21,15 +18,18 @@ namespace ZuneModdingHelper.Pages
{
const string UPDATES_DIALOG_TITLE = "UPDATES";
private readonly IGitHubClient? _gitHub = Ioc.Default.GetService<IGitHubClient>();
private ReleaseAsset _latestAsset;
private readonly IUpdateService? _updateService = Ioc.Default.GetService<IUpdateService>();
private UpdateAvailableInfo _updateInfo;
private string _downloadedUpdatePath;
public AboutPage()
{
DataContext = this;
InitializeComponent();
}
public bool EnableCheckForUpdates => _updateService is not null;
private void Link_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
App.OpenInBrowser(e.Uri.AbsoluteUri);
@@ -38,7 +38,7 @@ namespace ZuneModdingHelper.Pages
private async void UpdateCheckButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
if (_gitHub is null)
if (_updateService is null)
return;
WeakReferenceMessenger.Default.Send(new ShowDialogMessage(new ProgressDialogViewModel
@@ -51,16 +51,8 @@ namespace ZuneModdingHelper.Pages
try
{
// Get releases list from GitHub
// Why not use the `releases/latest` endpoint? Good question: https://github.com/octokit/octokit.net/issues/2916
var releases = await _gitHub.Repository.Release.GetAll("ZuneDev", "ZuneModdingHelper");
var latest = releases
.Select(r => new { Release = r, Version = ReleaseVersion.Parse(r.TagName) })
.OrderByDescending(t => t.Version)
.ThenBy(t => t.Release.Prerelease)
.First();
if (ReleaseVersion.TryParse(latest.Release.TagName, out var latestVer) || App.Version >= latestVer)
_updateInfo = await _updateService.FetchAvailableUpdate();
if (_updateInfo is null)
{
WeakReferenceMessenger.Default.Send<CloseDialogMessage>();
WeakReferenceMessenger.Default.Send(new ShowDialogMessage(new DialogViewModel
@@ -73,13 +65,12 @@ namespace ZuneModdingHelper.Pages
}
// Newer version available, prompt user to download
_latestAsset = latest.Release.Assets[0];
WeakReferenceMessenger.Default.Send<CloseDialogMessage>();
WeakReferenceMessenger.Default.Send(new ShowDialogMessage(new DialogViewModel
{
Title = UPDATES_DIALOG_TITLE,
Description = $"Release {latest.Release.Name} is available. Would you like to download it now?",
Description = $"Release {_updateInfo.Name} is available. Would you like to download it now?",
AffirmativeText = "YES",
NegativeText = "NO",
ShowAffirmativeButton = true,
@@ -107,30 +98,14 @@ namespace ZuneModdingHelper.Pages
IsIndeterminate = true,
ShowAffirmativeButton = false,
ShowNegativeButton = false,
Maximum = 100,
Maximum = 1.0,
};
WeakReferenceMessenger.Default.Send(new ShowDialogMessage(prog));
// Download new version to AppData
string downloadedFile = Path.Combine(Path.GetTempPath(), _latestAsset.Name);
if (File.Exists(downloadedFile)) File.Delete(downloadedFile);
using (var client = new System.Net.WebClient())
{
client.DownloadProgressChanged += (object sender, System.Net.DownloadProgressChangedEventArgs e) =>
{
// Update UI with download progress
prog.Progress = e.ProgressPercentage;
};
prog.Progress = 0;
prog.IsIndeterminate = false;
await client.DownloadFileTaskAsync(new Uri(_latestAsset.BrowserDownloadUrl), downloadedFile);
}
// Ask user to save file
Microsoft.Win32.SaveFileDialog saveFileDialog = new()
{
FileName = _latestAsset.Name,
FileName = _updateInfo.Name,
InitialDirectory = new KnownFolder(KnownFolderType.Downloads).Path
};
bool dialogResult = saveFileDialog.ShowDialog() ?? false;
@@ -139,12 +114,17 @@ namespace ZuneModdingHelper.Pages
if (dialogResult)
{
_downloadedUpdatePath = saveFileDialog.FileName;
File.Copy(downloadedFile, _downloadedUpdatePath, true);
// Download new version to requested folder with progress updates
Progress<double> progress = new(p => prog.Progress = p);
prog.Progress = 0;
prog.IsIndeterminate = false;
await _updateService.DownloadUpdate(_updateInfo, _downloadedUpdatePath, progress);
WeakReferenceMessenger.Default.Send(new ShowDialogMessage(new DialogViewModel
{
Title = UPDATES_DIALOG_TITLE,
Description = "Update downloaded. Would you like to install the new version?",
Description = "Update downloaded. Would you like to launch the new version?",
AffirmativeText = "YES",
NegativeText = "NO",
ShowAffirmativeButton = true,
@@ -152,8 +132,6 @@ namespace ZuneModdingHelper.Pages
OnAction = new AsyncRelayCommand<bool>(OnLaunchUpdateDialogResult)
}));
}
File.Delete(downloadedFile);
}
private async Task OnLaunchUpdateDialogResult(bool accepted)
@@ -0,0 +1,67 @@
using Octokit;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ZuneModCore;
namespace ZuneModdingHelper.Services;
public class GitHubReleasesUpdateService(IGitHubClient gitHub) : IUpdateService
{
public async Task<UpdateAvailableInfo> FetchAvailableUpdate(CancellationToken token = default)
{
try
{
// Get releases list from GitHub
// Why not use the `releases/latest` endpoint? Good question: https://github.com/octokit/octokit.net/issues/2916
var releases = await gitHub.Repository.Release.GetAll("ZuneDev", "ZuneModdingHelper");
token.ThrowIfCancellationRequested();
var latest = releases
.Select(r => new GitHubUpdateAvailableInfo(r))
.OrderByDescending(t => t.Version)
.ThenBy(t => t.Release.Prerelease)
.First();
if (App.Version < latest.Version)
return latest;
}
catch { }
return null;
}
public async Task DownloadUpdate(UpdateAvailableInfo u, string downloadedFile, IProgress<double> progress, CancellationToken token = default)
{
if (u is not GitHubUpdateAvailableInfo update)
throw new ArgumentException("update");
if (File.Exists(downloadedFile))
File.Delete(downloadedFile);
using var client = new System.Net.WebClient();
client.DownloadProgressChanged += (sender, e) =>
{
// Update caller with download progress
progress.Report(e.ProgressPercentage / 100d);
};
// TODO: Select appropriate asset
var asset = update.Release.Assets[0];
await client.DownloadFileTaskAsync(new Uri(asset.BrowserDownloadUrl), downloadedFile);
}
private record GitHubUpdateAvailableInfo(string Name, ReleaseVersion Version, DateTimeOffset? ReleasedAt, Release Release)
: UpdateAvailableInfo(Name, Version, ReleasedAt)
{
public GitHubUpdateAvailableInfo(Release release)
: this(release.Name, ReleaseVersion.Parse(release.TagName), release.PublishedAt, release)
{
}
};
}
@@ -0,0 +1,15 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ZuneModCore;
namespace ZuneModdingHelper.Services;
public interface IUpdateService
{
Task<UpdateAvailableInfo> FetchAvailableUpdate(CancellationToken token = default);
Task DownloadUpdate(UpdateAvailableInfo update, string destinationDirectory, IProgress<double> progress, CancellationToken token = default);
}
public record UpdateAvailableInfo(string Name, ReleaseVersion Version, DateTimeOffset? ReleasedAt);