using System.Net.Http.Json; using System.Reflection; using System.Text.RegularExpressions; using System.Text.Json.Serialization; namespace LMEMappeCatastaliApp.Services; public sealed class AppUpdateService { private const string ReleasesUrl = "https://gitea.lmcloud.it/api/v1/repos/livio/LMEMappeCatastaliProApp/releases?limit=50"; private static readonly Regex ProTagPattern = new( @"^v?(?\d+\.\d+\.\d+)-pro(?:[-.].*)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(20) }; public static string CurrentVersion => typeof(AppUpdateService).Assembly .GetCustomAttribute()? .InformationalVersion.Split('+')[0] ?? AppInfo.Current.VersionString; public async Task CheckAsync(CancellationToken cancellationToken = default) { var releases = await _httpClient.GetFromJsonAsync(ReleasesUrl, cancellationToken) ?? throw new InvalidDataException("Risposta aggiornamenti non valida."); GiteaRelease? selectedRelease = null; GiteaAsset? selectedApk = null; Version? selectedVersion = null; foreach (var release in releases.Where(item => !item.Draft)) { var match = ProTagPattern.Match(release.TagName ?? string.Empty); var apk = release.Assets?.FirstOrDefault(item => IsProApk(item.Name)); if (!match.Success || apk is null || !Version.TryParse(match.Groups["version"].Value, out var version)) { continue; } if (selectedVersion is null || version > selectedVersion) { selectedRelease = release; selectedApk = apk; selectedVersion = version; } } if (selectedRelease is null || selectedApk is null || selectedVersion is null) { throw new InvalidDataException("Nessuna release compatibile con LME Mappe Catastali Pro trovata."); } _ = Version.TryParse(CurrentVersion, out var current); return new( current is not null && selectedVersion > current, selectedVersion.ToString(3), selectedRelease.HtmlUrl ?? string.Empty, selectedApk.BrowserDownloadUrl ?? selectedRelease.HtmlUrl ?? string.Empty, selectedRelease.Body ?? string.Empty); } private static bool IsProApk(string? name) => name?.StartsWith("LME-Mappe-Catastali-Pro-", StringComparison.OrdinalIgnoreCase) == true && name.EndsWith(".apk", StringComparison.OrdinalIgnoreCase); private sealed class GiteaRelease { [JsonPropertyName("tag_name")] public string? TagName { get; set; } [JsonPropertyName("html_url")] public string? HtmlUrl { get; set; } [JsonPropertyName("assets")] public GiteaAsset[]? Assets { get; set; } [JsonPropertyName("body")] public string? Body { get; set; } [JsonPropertyName("draft")] public bool Draft { get; set; } } private sealed class GiteaAsset { [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("browser_download_url")] public string? BrowserDownloadUrl { get; set; } } } public sealed record AppUpdateResult(bool IsAvailable, string Version, string ReleaseUrl, string DownloadUrl, string ReleaseNotes);