using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Semver;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(UpdateDialog))]
[ManagedService]
[Singleton]
public partial class UpdateViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IHttpClientFactory httpClientFactory;
private readonly IUpdateHelper updateHelper;
private bool isLoaded;
[ObservableProperty]
private bool isUpdateAvailable;
[ObservableProperty]
private UpdateInfo? updateInfo;
[ObservableProperty]
private string? releaseNotes;
[ObservableProperty]
private string? updateText;
[ObservableProperty]
private int progressValue;
[ObservableProperty]
private bool showProgressBar;
[ObservableProperty]
private string? currentVersionText;
[ObservableProperty]
private string? newVersionText;
[GeneratedRegex(
@"(##\s*(v[0-9]+\.[0-9]+\.[0-9]+)((?:\n|.)+?))(?=(##\s*v[0-9]+\.[0-9]+\.[0-9]+)|\z)"
)]
private static partial Regex RegexChangelog();
public UpdateViewModel(
ISettingsManager settingsManager,
IHttpClientFactory httpClientFactory,
IUpdateHelper updateHelper
)
{
this.settingsManager = settingsManager;
this.httpClientFactory = httpClientFactory;
this.updateHelper = updateHelper;
EventManager.Instance.UpdateAvailable += (_, info) =>
{
IsUpdateAvailable = true;
UpdateInfo = info;
};
updateHelper.StartCheckingForUpdates().SafeFireAndForget();
}
///
/// Formats changelog markdown including up to the current version
///
internal static string? FormatChangelog(string markdown, SemVersion currentVersion)
{
var pattern = RegexChangelog();
var results = pattern
.Matches(markdown)
.Select(
m =>
new
{
Block = m.Groups[1].Value.Trim(),
Version = m.Groups[2].Value.Trim(),
Content = m.Groups[3].Value.Trim()
}
)
.ToList();
// Join all blocks until and excluding the current version
// If we're on a pre-release, include the current release
var currentVersionBlock = results.FindIndex(
x => x.Version == $"v{currentVersion.WithoutPrereleaseOrMetadata()}"
);
if (currentVersionBlock == -1)
{
return null;
}
var blocks = results
.Take(currentVersionBlock + (currentVersion.IsPrerelease ? 1 : 0))
.Select(x => x.Block)
.ToList();
return string.Join(Environment.NewLine + Environment.NewLine, blocks);
}
public async Task Preload()
{
if (UpdateInfo is null)
return;
ReleaseNotes = await GetReleaseNotes(UpdateInfo.Changelog.ToString());
}
internal async Task GetReleaseNotes(string changelogUrl)
{
using var client = httpClientFactory.CreateClient();
var response = await client.GetAsync(changelogUrl);
if (response.IsSuccessStatusCode)
{
var changelog = await response.Content.ReadAsStringAsync();
// Formatting for new changelog format
// https://keepachangelog.com/en/1.1.0/
if (changelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
{
return FormatChangelog(changelog, Compat.AppVersion)
?? "## Unable to format release notes";
}
return changelog;
}
else
{
return "## Unable to load release notes";
}
}
partial void OnUpdateInfoChanged(UpdateInfo? value)
{
CurrentVersionText = $"v{Compat.AppVersion}";
NewVersionText = $"v{value?.Version}";
}
public override async Task OnLoadedAsync()
{
if (!isLoaded)
{
await Preload();
}
}
///
public override void OnUnloaded()
{
base.OnUnloaded();
isLoaded = false;
}
[RelayCommand]
private async Task InstallUpdate()
{
if (UpdateInfo == null)
{
return;
}
ShowProgressBar = true;
UpdateText = string.Format(
Resources.TextTemplate_UpdatingPackage,
Resources.Label_StabilityMatrix
);
await updateHelper.DownloadUpdate(
UpdateInfo,
new Progress(report =>
{
ProgressValue = Convert.ToInt32(report.Percentage);
})
);
// On unix, we need to set the executable bit
if (Compat.IsUnix)
{
File.SetUnixFileMode(
UpdateHelper.ExecutablePath, // 0755
UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
| UnixFileMode.GroupRead
| UnixFileMode.GroupExecute
| UnixFileMode.OtherRead
| UnixFileMode.OtherExecute
);
}
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";
await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds...";
await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 1 second...";
await Task.Delay(1000);
Process.Start(UpdateHelper.ExecutablePath);
App.Shutdown();
}
}