feat: 新增更新模块

This commit is contained in:
denglihong2007
2025-08-30 14:52:14 +08:00
parent 2ca60e8e16
commit d79e129724
8 changed files with 323 additions and 7 deletions

View File

@@ -8,5 +8,6 @@ namespace CRSim.Core.Abstractions
Task<List<TrainStop>?> GetTimeTableAsync(string number);
Task<List<TrainStop>?> GetTrainNumbersAsync(string name);
List<PluginManifest>? GetOnlinePlugins(string uri);
Task<UpdateInfo?> GetUpdateAsync(string uri);
}
}

View File

@@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
namespace CRSim.Core.Models
{
[JsonSerializable(typeof(Json))]
[JsonSerializable(typeof(UpdateInfo))]
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class JsonContext : JsonSerializerContext
{

View File

@@ -0,0 +1,170 @@
using System.Text.Json.Serialization;
namespace CRSim.Core.Models
{
public class UpdateInfo
{
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("assets_url")]
public string? AssetsUrl { get; set; }
[JsonPropertyName("upload_url")]
public string? UploadUrl { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
[JsonPropertyName("id")]
public long? Id { get; set; }
[JsonPropertyName("author")]
public AuthorEntity? Author { get; set; }
[JsonPropertyName("node_id")]
public string? NodeId { get; set; }
[JsonPropertyName("tag_name")]
public string? TagName { get; set; }
[JsonPropertyName("target_commitish")]
public string? TargetCommitish { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("draft")]
public bool? Draft { get; set; }
[JsonPropertyName("immutable")]
public bool? Immutable { get; set; }
[JsonPropertyName("prerelease")]
public bool? Prerelease { get; set; }
[JsonPropertyName("created_at")]
public string? CreatedAt { get; set; }
[JsonPropertyName("updated_at")]
public string? UpdatedAt { get; set; }
[JsonPropertyName("published_at")]
public string? PublishedAt { get; set; }
[JsonPropertyName("assets")]
public List<AssetsEntity?>? Assets { get; set; }
[JsonPropertyName("tarball_url")]
public string? TarballUrl { get; set; }
[JsonPropertyName("zipball_url")]
public string? ZipballUrl { get; set; }
[JsonPropertyName("body")]
public string? Body { get; set; }
[JsonPropertyName("mentions_count")]
public long? MentionsCount { get; set; }
}
public class AuthorEntity
{
[JsonPropertyName("login")]
public string? Login { get; set; }
[JsonPropertyName("id")]
public long? Id { get; set; }
[JsonPropertyName("node_id")]
public string? NodeId { get; set; }
[JsonPropertyName("avatar_url")]
public string? AvatarUrl { get; set; }
[JsonPropertyName("gravatar_id")]
public string? GravatarId { get; set; }
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
[JsonPropertyName("followers_url")]
public string? FollowersUrl { get; set; }
[JsonPropertyName("following_url")]
public string? FollowingUrl { get; set; }
[JsonPropertyName("gists_url")]
public string? GistsUrl { get; set; }
[JsonPropertyName("starred_url")]
public string? StarredUrl { get; set; }
[JsonPropertyName("subscriptions_url")]
public string? SubscriptionsUrl { get; set; }
[JsonPropertyName("organizations_url")]
public string? OrganizationsUrl { get; set; }
[JsonPropertyName("repos_url")]
public string? ReposUrl { get; set; }
[JsonPropertyName("events_url")]
public string? EventsUrl { get; set; }
[JsonPropertyName("received_events_url")]
public string? ReceivedEventsUrl { get; set; }
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("user_view_type")]
public string? UserViewType { get; set; }
[JsonPropertyName("site_admin")]
public bool? SiteAdmin { get; set; }
}
public class AssetsEntity
{
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("id")]
public long? Id { get; set; }
[JsonPropertyName("node_id")]
public string? NodeId { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("label")]
public string? Label { get; set; }
[JsonPropertyName("uploader")]
public UploaderEntity? Uploader { get; set; }
[JsonPropertyName("content_type")]
public string? ContentType { get; set; }
[JsonPropertyName("state")]
public string? State { get; set; }
[JsonPropertyName("size")]
public long? Size { get; set; }
[JsonPropertyName("digest")]
public string? Digest { get; set; }
[JsonPropertyName("download_count")]
public long? DownloadCount { get; set; }
[JsonPropertyName("created_at")]
public string? CreatedAt { get; set; }
[JsonPropertyName("updated_at")]
public string? UpdatedAt { get; set; }
[JsonPropertyName("browser_download_url")]
public string? BrowserDownloadUrl { get; set; }
}
public class UploaderEntity
{
[JsonPropertyName("login")]
public string? Login { get; set; }
[JsonPropertyName("id")]
public long? Id { get; set; }
[JsonPropertyName("node_id")]
public string? NodeId { get; set; }
[JsonPropertyName("avatar_url")]
public string? AvatarUrl { get; set; }
[JsonPropertyName("gravatar_id")]
public string? GravatarId { get; set; }
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
[JsonPropertyName("followers_url")]
public string? FollowersUrl { get; set; }
[JsonPropertyName("following_url")]
public string? FollowingUrl { get; set; }
[JsonPropertyName("gists_url")]
public string? GistsUrl { get; set; }
[JsonPropertyName("starred_url")]
public string? StarredUrl { get; set; }
[JsonPropertyName("subscriptions_url")]
public string? SubscriptionsUrl { get; set; }
[JsonPropertyName("organizations_url")]
public string? OrganizationsUrl { get; set; }
[JsonPropertyName("repos_url")]
public string? ReposUrl { get; set; }
[JsonPropertyName("events_url")]
public string? EventsUrl { get; set; }
[JsonPropertyName("received_events_url")]
public string? ReceivedEventsUrl { get; set; }
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("user_view_type")]
public string? UserViewType { get; set; }
[JsonPropertyName("site_admin")]
public bool? SiteAdmin { get; set; }
}
}

View File

@@ -151,5 +151,20 @@ namespace CRSim.Core.Services
}
return null;
}
public async Task<UpdateInfo?> GetUpdateAsync(string uri)
{
try
{
var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.ParseAdd("CRSim");
var response = await client.GetStringAsync(uri);
return JsonSerializer.Deserialize(response, JsonContext.Default.UpdateInfo);
}
catch
{
}
return null;
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace CRSim.Core.Utils
{
public class FileHashHelper
{
/// <summary>
/// 计算文件 SHA256 值
/// </summary>
public static string ComputeSHA256(string filePath)
{
using var sha256 = SHA256.Create();
using var stream = File.OpenRead(filePath);
var hashBytes = sha256.ComputeHash(stream);
return Convert.ToHexStringLower(hashBytes);
}
/// <summary>
/// 验证文件 SHA256
/// </summary>
public static bool VerifySHA256(string filePath, string expectedHash)
{
var fileHash = ComputeSHA256(filePath);
return string.Equals(fileHash, expectedHash, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -1,4 +1,7 @@
using System.Diagnostics.Metrics;
using Downloader;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.IO.Packaging;
using System.Reflection;
namespace CRSim.ViewModels
@@ -12,13 +15,18 @@ namespace CRSim.ViewModels
public partial string AppVersion { get; set; } = "";
public ObservableCollection<InfoItem> Apis { get; } =
[
new InfoItem { Title = "官方插件源", Detail = "http://47.122.74.193:25565" },
new InfoItem { Title = "镜像站插件源", Detail = "https://crsim.com.cn/api" },
new InfoItem { Title = "官方源", Detail = "http://47.122.74.193:25565" },
new InfoItem { Title = "镜像站源", Detail = "https://crsim.com.cn/api" },
];
private Settings _settings;
private readonly ISettingsService _settingsService;
private readonly IDatabaseService _databaseService;
private readonly INetworkService _networkService;
private readonly IDialogService _dialogService;
[ObservableProperty]
public partial int UpdateProgress { get; set; }
#region
[ObservableProperty]
public partial string DepartureCheckInAdvanceDuration { get; set; }
@@ -53,14 +61,16 @@ namespace CRSim.ViewModels
[ObservableProperty]
public partial bool ReopenUnclosedScreensOnLoad { get; set; }
#endregion
public SettingsPageViewModel(ISettingsService settingsService, IDatabaseService databaseService, IDialogService dialogService)
public SettingsPageViewModel(ISettingsService settingsService, IDatabaseService databaseService, IDialogService dialogService, INetworkService networkService)
{
_settingsService = settingsService;
_databaseService = databaseService;
_dialogService = dialogService;
_networkService = networkService;
var version = Assembly.GetExecutingAssembly().GetName().Version;
AppVersion = $"Version {version}";
LoadSettings();
}
private void LoadSettings()
@@ -95,7 +105,7 @@ namespace CRSim.ViewModels
_settings.ApiUri = ApiUri.Detail;
_settingsService.SaveSettings();
}
private void UpdateSettings(string input, bool allowNegative, Action<int> updateAction)
private static void UpdateSettings(string input, bool allowNegative, Action<int> updateAction)
{
if (int.TryParse(input, out int value) && (allowNegative || value >= 0))
{
@@ -111,6 +121,89 @@ namespace CRSim.ViewModels
await _databaseService.SaveData();
}
[RelayCommand]
public async Task CheckUpdate()
{
var update = ApiUri.Title == "官方源" ?
await _networkService.GetUpdateAsync("https://api.github.com/repos/denglihong2007/CRSim/releases/latest") :
await _networkService.GetUpdateAsync("https://crsim.com.cn/api/version");
if (update is null)
{
await _dialogService.ShowMessageAsync("错误", "检查更新失败。");
}
else if (update.Name == AppVersion.Split(' ')[1])
{
await _dialogService.ShowMessageAsync("信息", "当前已经是最新版本。");
}
else
{
await _dialogService.ShowTextAsync("发现新版本 " + update.Name, update.Body + "\n系统即将下载安装新版本。");
DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
var downloader = new DownloadService();
UpdateProgress = 0;
try
{
int lastProgress = 0;
var lastUpdate = DateTime.MinValue;
downloader.DownloadProgressChanged += (s, e) =>
{
var now = DateTime.Now;
if ((e.ProgressPercentage - lastProgress >= 1) || (now - lastUpdate).TotalMilliseconds >= 100)
{
lastProgress = (int)e.ProgressPercentage;
lastUpdate = now;
dispatcherQueue.TryEnqueue(() =>
{
UpdateProgress = (int)e.ProgressPercentage;
});
}
};
var path = Path.Combine(AppPaths.TempPath, "update.zip");
await downloader.DownloadFileTaskAsync(update.Assets[0].BrowserDownloadUrl, path);
UpdateProgress = 0;
if (!FileHashHelper.VerifySHA256(path, update.Assets[0].Digest.Split(':')[1]))
{
await _dialogService.ShowMessageAsync("错误", "下载的文件校验失败,请重试。");
return;
}
var programDirectory = AppDomain.CurrentDomain.BaseDirectory;
var appExePath = Process.GetCurrentProcess().MainModule.FileName;
var appName = Path.GetFileName(appExePath);
string batchScript = $@"
@echo off
taskkill /F /IM ""{appName}"" >nul 2>&1
timeout /t 2 >nul
cd /d ""{programDirectory}""
powershell -Command ""Expand-Archive -Path '{path}' -DestinationPath '{programDirectory}' -Force""
del /f /q ""{path}""
start """" ""{appExePath}""
exit /b 0
";
string batchFilePath = Path.Combine(Path.GetTempPath(), "CRSimUpdate.bat");
File.WriteAllText(batchFilePath, batchScript);
var processInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{batchFilePath}\"",
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
CreateNoWindow = true
};
Process.Start(processInfo);
Application.Current.Exit();
}
catch (Exception e)
{
await _dialogService.ShowTextAsync("错误", "更新失败。\n" + e);
UpdateProgress = 0;
return;
}
}
}
[RelayCommand]
public async Task ExportData()
{
string? path = _dialogService.SaveFile(".json", "data");

View File

@@ -42,10 +42,14 @@
<controls:SettingsCard Header="密钥" Description="用于解锁特定功能" HeaderIcon="{winui:FontIcon Glyph=&#xE8D7;}">
<TextBox Width="168" Text="{x:Bind ViewModel.UserKey, Mode=TwoWay}"/>
</controls:SettingsCard>
<controls:SettingsCard Header="插件市场API" Description="插件市场资源源,重启应用生效" HeaderIcon="{winui:FontIcon Glyph=&#xE943;}">
<controls:SettingsCard Header="API 地址" Description="适用于插件市场与软件更新" HeaderIcon="{winui:FontIcon Glyph=&#xE943;}">
<ComboBox ItemsSource="{x:Bind ViewModel.Apis}" SelectedItem="{x:Bind ViewModel.ApiUri,Mode=TwoWay}"
DisplayMemberPath="Title" Width="168"/>
</controls:SettingsCard>
<controls:SettingsCard Header="应用更新" Description="检查软件更新" HeaderIcon="{winui:FontIcon Glyph=&#xECC5;}">
<Button Style="{StaticResource AccentButtonStyle}" Content="检查更新" Command="{x:Bind ViewModel.CheckUpdateCommand}" />
</controls:SettingsCard>
<ProgressBar Visibility="{x:Bind ViewModel.UpdateProgress, Mode=OneWay, Converter={StaticResource IntToVisibilityConverter}, ConverterParameter=false}" Value="{x:Bind ViewModel.UpdateProgress, Mode=OneWay}"/>
</StackPanel>
<TextBlock Text="引导屏模拟" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Margin="0,0,0,12"/>
<StackPanel Spacing="4" Margin="0,0,0,24">

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>2.3.109.0</Version>
<Version>3.0.0.0</Version>
<Authors>电排骨</Authors>
<RepositoryUrl>https://github.com/denglihong2007/CRSim</RepositoryUrl>
</PropertyGroup>