// // Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team) // Copyright (C) 2021-2025 MaaAssistantArknights Contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License v3.0 only as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY // #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using JetBrains.Annotations; using MaaWpfGui.Constants; using MaaWpfGui.Extensions; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.Models; using MaaWpfGui.Properties; using MaaWpfGui.Services; using MaaWpfGui.Utilities; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.UI; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Stylet; namespace MaaWpfGui.ViewModels.UserControl.Settings; /// /// 软件更新设置 /// public class VersionUpdateSettingsUserControlModel : PropertyChangedBase { static VersionUpdateSettingsUserControlModel() { Instance = new(); } public static VersionUpdateSettingsUserControlModel Instance { get; } public enum UpdateVersionType { /// /// 测试版 /// Nightly, /// /// 开发版 /// Beta, /// /// 稳定版 /// Stable, } /// /// Gets the core version. /// public static string CoreVersion { get; } = Marshal.PtrToStringAnsi(MaaService.AsstGetVersion()) ?? "0.0.1"; public static string CoreVersionDisplay => string.Join("\u200B", CoreVersion.ToCharArray()); private static readonly string _uiVersion = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion.Split('+')[0] ?? "0.0.1"; /// /// Gets the UI version. /// public static string UiVersion { get; } = _uiVersion == "0.0.1" ? "DEBUG VERSION" : _uiVersion; public static string UiVersionDisplay => string.Join("\u200B", UiVersion.ToCharArray()); public static DateTime BuildDateTime { get; } = Assembly.GetExecutingAssembly().GetCustomAttribute()?.BuildDateTime ?? DateTime.MinValue; public static string BuildDateTimeCurrentCultureString => BuildDateTime.ToLocalTimeString(); private static (DateTime DateTime, string VersionName) _resourceInfo = GetResourceVersionByClientType(SettingsViewModel.GameSettings.ClientType); public (DateTime DateTime, string VersionName) ResourceInfo { get => _resourceInfo; set => SetAndNotify(ref _resourceInfo, value); } private static string _resourceVersion = _resourceInfo.VersionName; private string _newResourceFoundInfo = string.Empty; public string NewResourceFoundInfo { get => _newResourceFoundInfo; set { SetAndNotify(ref _newResourceFoundInfo, value); Instances.SettingsViewModel.UpdateWindowTitle(); } } private string _newVersionFoundInfo = string.Empty; public string NewVersionFoundInfo { get => _newVersionFoundInfo; set { SetAndNotify(ref _newVersionFoundInfo, value); Instances.SettingsViewModel.UpdateWindowTitle(); } } /// /// Gets or sets the resource version. /// public string ResourceVersion { get => _resourceVersion; set => SetAndNotify(ref _resourceVersion, value); } private static DateTime _resourceDateTime = _resourceInfo.DateTime; public DateTime ResourceDateTime { get => _resourceDateTime; set => SetAndNotify(ref _resourceDateTime, value); } private string _resourceDateTimeCurrentCultureString = _resourceDateTime.ToLocalTimeString(); public string ResourceDateTimeCurrentCultureString { get => _resourceDateTimeCurrentCultureString; set => SetAndNotify(ref _resourceDateTimeCurrentCultureString, value); } public void ResourceInfoUpdate() { ResourceInfo = GetResourceVersionByClientType(SettingsViewModel.GameSettings.ClientType); ResourceVersion = ResourceInfo.VersionName; ResourceDateTime = ResourceInfo.DateTime; ResourceDateTimeCurrentCultureString = ResourceDateTime.ToLocalTimeString(); Instances.SettingsViewModel.UpdateWindowTitle(); } public static (DateTime DateTime, string VersionName) GetResourceVersionByClientType(string clientType) { bool isDefaultClient = new HashSet { string.Empty, "Official", "Bilibili" }.Contains(clientType); string defaultJsonPath = Path.Combine(PathsHelper.ResourceDir, "version.json"); var jsonPath = isDefaultClient ? defaultJsonPath : Path.Combine(PathsHelper.ResourceDir, $"global/{clientType}/resource/version.json"); string versionName; if (!File.Exists(defaultJsonPath) || (!isDefaultClient && !File.Exists(jsonPath))) { return (DateTime.MinValue, string.Empty); } var versionJson = LoadJson(jsonPath); var currentTime = (ulong)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var poolTime = (ulong?)versionJson?["gacha"]?["time"]; // 卡池的开始时间 var activityTime = (ulong?)versionJson?["activity"]?["time"]; // 活动的开始时间 var lastUpdated = isDefaultClient ? (string?)versionJson?["last_updated"] : (string?)LoadJson(defaultJsonPath)?["last_updated"]; var dateTime = lastUpdated == null ? DateTime.MinValue : DateTime.ParseExact(lastUpdated, "yyyy-MM-dd HH:mm:ss.fff", null); if (currentTime < poolTime && currentTime < activityTime) { versionName = string.Empty; } else if (currentTime >= poolTime && currentTime < activityTime) { versionName = versionJson?["gacha"]?["pool"]?.ToString() ?? string.Empty; } else if (currentTime < poolTime && currentTime >= activityTime) { versionName = versionJson?["activity"]?["name"]?.ToString() ?? string.Empty; } else if (poolTime > activityTime) { versionName = versionJson?["gacha"]?["pool"]?.ToString() ?? string.Empty; } else { versionName = versionJson?["activity"]?["name"]?.ToString() ?? string.Empty; } return (dateTime, versionName); static JObject? LoadJson(string path) { return JsonConvert.DeserializeObject(File.ReadAllText(path)); } } private UpdateVersionType _versionType = (UpdateVersionType)Enum.Parse( typeof(UpdateVersionType), ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionType, nameof(UpdateVersionType.Stable))); /// /// Gets or sets the type of version to update. /// public UpdateVersionType VersionType { get => _versionType; set { SetAndNotify(ref _versionType, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionType, value.ToString()); } } /// /// Gets the list of the version type. /// public List> AllVersionTypeList { get; } = [ new() { Display = LocalizationHelper.GetString("UpdateCheckNightly"), Value = UpdateVersionType.Nightly }, new() { Display = LocalizationHelper.GetString("UpdateCheckBeta"), Value = UpdateVersionType.Beta }, new() { Display = LocalizationHelper.GetString("UpdateCheckStable"), Value = UpdateVersionType.Stable }, ]; public List> VersionTypeList { get => AllVersionTypeList.Where(v => AllowNightlyUpdates || v.Value != UpdateVersionType.Nightly).ToList(); } public bool AllowNightlyUpdates { get; set; } = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.AllowNightlyUpdates, bool.FalseString)); private bool _hasAcknowledgedNightlyWarning = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.HasAcknowledgedNightlyWarning, bool.FalseString)); public bool HasAcknowledgedNightlyWarning { get => _hasAcknowledgedNightlyWarning; set { SetAndNotify(ref _hasAcknowledgedNightlyWarning, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.HasAcknowledgedNightlyWarning, value.ToString()); } } public List> UpdateSourceList { get; } = [ new() { Display = LocalizationHelper.GetString("GlobalSource"), Value = "Github" }, new() { Display = LocalizationHelper.GetString("MirrorChyan"), Value = "MirrorChyan" }, ]; private string _updateSource = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.UpdateSource, "Github"); /// /// Gets or sets the type of version to update. /// public string UpdateSource { get => _updateSource; set { SetAndNotify(ref _updateSource, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.UpdateSource, value); } } private bool _forceGithubGlobalSource = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.ForceGithubGlobalSource, bool.FalseString)); public bool ForceGithubGlobalSource { get => _forceGithubGlobalSource; set { SetAndNotify(ref _forceGithubGlobalSource, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.ForceGithubGlobalSource, value.ToString()); } } private string _mirrorChyanCdk = SimpleEncryptionHelper.Decrypt(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MirrorChyanCdk, string.Empty)); public string MirrorChyanCdk { get => _mirrorChyanCdk; set { if (string.IsNullOrEmpty(value)) { MirrorChyanCdkExpiredTime = 0; } if (!SetAndNotify(ref _mirrorChyanCdk, value)) { return; } if (value.Length == 24) { Task.Run(async () => { await Instances.VersionUpdateViewModel.VersionUpdateAndAskToRestartAsync(); await ResourceUpdater.ResourceUpdateAndReloadAsync(); }); } value = SimpleEncryptionHelper.Encrypt(value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.MirrorChyanCdk, value); } } // 时间戳 private long _mirrorChyanCdkExpiredTime = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MirrorChyanCdkExpiredTime, 0L); public long MirrorChyanCdkExpiredTime { get => _mirrorChyanCdkExpiredTime; set { if (!SetAndNotify(ref _mirrorChyanCdkExpiredTime, value)) { return; } ConfigurationHelper.SetGlobalValue(ConfigurationKeys.MirrorChyanCdkExpiredTime, value.ToString()); RefreshMirrorChyanCdkRemaining(); } } private bool _mirrorChyanCdkFetchFailed = false; public bool MirrorChyanCdkFetchFailed { get => _mirrorChyanCdkFetchFailed; set => SetAndNotify(ref _mirrorChyanCdkFetchFailed, value); } public DateTime MirrorChyanCdkExpiredDateTime => DateTimeOffset.FromUnixTimeSeconds(MirrorChyanCdkExpiredTime).DateTime; public DateTime MirrorChyanCdkExpiredLocalTime => MirrorChyanCdkExpiredDateTime.ToLocalTime(); /// /// Gets 剩余时间 /// public TimeSpan MirrorChyanCdkRemaining => MirrorChyanCdkExpiredDateTime - DateTime.Now; /// /// Gets a value indicating whether 是否已过期 /// public bool IsMirrorChyanCdkExpired => MirrorChyanCdkRemaining.TotalSeconds <= 0; /// /// Gets 显示用的剩余时间提示 /// public string MirrorChyanCdkRemainingText => IsMirrorChyanCdkExpired ? LocalizationHelper.GetString("MirrorChyanCdkExpired") : string.Format(LocalizationHelper.GetString("MirrorChyanCdkRemainingDays"), MirrorChyanCdkRemaining.TotalDays.ToString("F1")); /// /// Gets uI 显示用颜色 /// public string MirrorChyanCdkRemainingBrush { get { if (IsMirrorChyanCdkExpired) { return UiLogColor.Error; } if (MirrorChyanCdkRemaining.TotalDays <= 7) { return UiLogColor.Warning; } return UiLogColor.Success; } } public void RefreshMirrorChyanCdkRemaining() { OnPropertyChanged(nameof(MirrorChyanCdkExpiredDateTime)); OnPropertyChanged(nameof(MirrorChyanCdkRemaining)); OnPropertyChanged(nameof(IsMirrorChyanCdkExpired)); OnPropertyChanged(nameof(MirrorChyanCdkRemainingText)); OnPropertyChanged(nameof(MirrorChyanCdkRemainingBrush)); } private bool _startupUpdateCheck = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.StartupUpdateCheck, bool.TrueString)); // UI 绑定的方法 [UsedImplicitly] public void MirrorChyanCdkCopy() { System.Windows.Forms.Clipboard.Clear(); System.Windows.Forms.Clipboard.SetDataObject(MirrorChyanCdk); } /// /// Gets or sets a value indicating whether to check update. /// public bool StartupUpdateCheck { get => _startupUpdateCheck; set { SetAndNotify(ref _startupUpdateCheck, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.StartupUpdateCheck, value.ToString()); } } private bool _updateAutoCheck = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.UpdateAutoCheck, bool.FalseString)); /// /// Gets or sets a value indicating whether to check update. /// public bool UpdateAutoCheck { get => _updateAutoCheck; set { SetAndNotify(ref _updateAutoCheck, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.UpdateAutoCheck, value.ToString()); } } private string _proxy = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.UpdateProxy, string.Empty); /// /// Gets or sets the proxy settings. /// public string Proxy { get => _proxy; set { SetAndNotify(ref _proxy, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.UpdateProxy, value); } } public List ProxyTypeList { get; } = [ new() { Display = "HTTP Proxy", Value = "http" }, new() { Display = "SOCKS5 Proxy", Value = "socks5" }, ]; private string _proxyType = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.ProxyType, "http"); public string ProxyType { get => _proxyType; set { SetAndNotify(ref _proxyType, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.ProxyType, value); } } private bool _isCheckingForUpdates; /// /// Gets or sets a value indicating whether the update is being checked. /// public bool IsCheckingForUpdates { get => _isCheckingForUpdates; set { SetAndNotify(ref _isCheckingForUpdates, value); } } private bool _autoDownloadUpdatePackage = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.AutoDownloadUpdatePackage, bool.TrueString)); /// /// Gets or sets a value indicating whether to auto download update package. /// public bool AutoDownloadUpdatePackage { get => _autoDownloadUpdatePackage; set { SetAndNotify(ref _autoDownloadUpdatePackage, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.AutoDownloadUpdatePackage, value.ToString()); } } private bool _autoInstallUpdatePackage = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.AutoInstallUpdatePackage, bool.FalseString)); /// /// Gets or sets a value indicating whether to auto install update package. /// public bool AutoInstallUpdatePackage { get => _autoInstallUpdatePackage; set { SetAndNotify(ref _autoInstallUpdatePackage, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.AutoInstallUpdatePackage, value.ToString()); } } /// /// Updates manually. /// /// A representing the asynchronous operation. [UsedImplicitly] public async Task ManualUpdate() { if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan" && string.IsNullOrEmpty(SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk)) { ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanSelectedButNoCdk")); return; } var ret = await Instances.VersionUpdateViewModel.CheckAndDownloadVersionUpdate(); var toastMessage = ret switch { VersionUpdateViewModel.CheckUpdateRetT.NoNeedToUpdate => string.Empty, VersionUpdateViewModel.CheckUpdateRetT.NoNeedToUpdateDebugVersion => LocalizationHelper.GetString("NoNeedToUpdateDebugVersion"), VersionUpdateViewModel.CheckUpdateRetT.AlreadyLatest => LocalizationHelper.GetString("AlreadyLatest"), VersionUpdateViewModel.CheckUpdateRetT.UnknownError => LocalizationHelper.GetString("NewVersionDetectFailedTitle"), VersionUpdateViewModel.CheckUpdateRetT.NetworkError => LocalizationHelper.GetString("CheckNetworking"), VersionUpdateViewModel.CheckUpdateRetT.FailedToGetInfo => LocalizationHelper.GetString("GetReleaseNoteFailed"), VersionUpdateViewModel.CheckUpdateRetT.OK => string.Empty, VersionUpdateViewModel.CheckUpdateRetT.NewVersionIsBeingBuilt => LocalizationHelper.GetString("NewVersionIsBeingBuilt"), VersionUpdateViewModel.CheckUpdateRetT.OnlyGameResourceUpdated => LocalizationHelper.GetString("GameResourceUpdated"), VersionUpdateViewModel.CheckUpdateRetT.NoMirrorChyanCdk => LocalizationHelper.GetString("MirrorChyanSoftwareUpdateTip"), _ => string.Empty, }; if (toastMessage != string.Empty) { ToastNotification.ShowDirect(toastMessage); } if (ret == VersionUpdateViewModel.CheckUpdateRetT.OK) { _ = Instances.VersionUpdateViewModel.AskToRestart(); } } [UsedImplicitly] public async Task ManualUpdateResource() { if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan" && string.IsNullOrEmpty(SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk)) { ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanSelectedButNoCdk")); return; } IsCheckingForUpdates = true; var (ret, uri, releaseNote) = await ResourceUpdater.CheckFromMirrorChyanAsync(); var toastMessage = ret switch { VersionUpdateViewModel.CheckUpdateRetT.AlreadyLatest => LocalizationHelper.GetString("AlreadyLatest"), VersionUpdateViewModel.CheckUpdateRetT.UnknownError => LocalizationHelper.GetString("NewVersionDetectFailedTitle"), VersionUpdateViewModel.CheckUpdateRetT.NetworkError => LocalizationHelper.GetString("CheckNetworking"), _ => string.Empty, }; if (toastMessage != string.Empty) { ToastNotification.ShowDirect(toastMessage); } if (ret == VersionUpdateViewModel.CheckUpdateRetT.AlreadyLatest) { IsCheckingForUpdates = false; return; } bool success = UpdateSource switch { "Github" => await ResourceUpdater.UpdateFromGithubAsync(), "MirrorChyan" => (ret == VersionUpdateViewModel.CheckUpdateRetT.OK) && await ResourceUpdater.DownloadFromMirrorChyanAsync(uri, releaseNote), _ => await ResourceUpdater.UpdateFromGithubAsync(), }; if (success) { ResourceUpdater.ResourceReload(); } else { AchievementTrackerHelper.Instance.AddProgress(AchievementIds.CdnTorture); } IsCheckingForUpdates = false; } // UI 绑定的方法 [UsedImplicitly] public void ShowChangelog() { Instances.WindowManager.ShowWindow(Instances.VersionUpdateViewModel); } }