mirror of
https://github.com/denglihong2007/CRSim
synced 2026-08-08 02:25:35 +08:00
feat: 新增更新模块
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
170
CRSim.Core/Models/UpdateInfo.cs
Normal file
170
CRSim.Core/Models/UpdateInfo.cs
Normal 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; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
CRSim.Core/Utils/FileHashHelper.cs
Normal file
32
CRSim.Core/Utils/FileHashHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -42,10 +42,14 @@
|
||||
<controls:SettingsCard Header="密钥" Description="用于解锁特定功能" HeaderIcon="{winui:FontIcon Glyph=}">
|
||||
<TextBox Width="168" Text="{x:Bind ViewModel.UserKey, Mode=TwoWay}"/>
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard Header="插件市场API" Description="插件市场资源源,重启应用生效" HeaderIcon="{winui:FontIcon Glyph=}">
|
||||
<controls:SettingsCard Header="API 地址" Description="适用于插件市场与软件更新" HeaderIcon="{winui:FontIcon Glyph=}">
|
||||
<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=}">
|
||||
<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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user