// // MaaWpfGui - A part of the MaaCoreArknights project // Copyright (C) 2021 MistEO and 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.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Windows; using HandyControl.Controls; using HandyControl.Data; using MaaWpfGui.Constants; using MaaWpfGui.Extensions; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.Models; using MaaWpfGui.Services.HotKeys; using MaaWpfGui.States; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.UserControl.Settings; using Newtonsoft.Json; using Serilog; using Stylet; using ComboBox = System.Windows.Controls.ComboBox; using Timer = System.Timers.Timer; namespace MaaWpfGui.ViewModels.UI { /// /// The view model of settings. /// public class SettingsViewModel : Screen { private readonly RunningState _runningState; private static readonly ILogger _logger = Log.ForContext(); /// /// Gets the visibility of task setting views. /// public TaskSettingVisibilityInfo TaskSettingVisibilities { get; } = TaskSettingVisibilityInfo.Current; #region 设置界面Model /// /// Gets 游戏设置model /// public static GameSettingsUserControlModel GameSettings { get; } = GameSettingsUserControlModel.Instance; /// /// Gets 连接设置model /// public static ConnectSettingsUserControlModel ConnectSettings { get; } = ConnectSettingsUserControlModel.Instance; /// /// Gets 启动设置Model /// public static StartSettingsUserControlModel StartSettings { get; } = StartSettingsUserControlModel.Instance; /// /// Gets 界面设置model /// public static GuiSettingsUserControlModel GuiSettings { get; } = GuiSettingsUserControlModel.Instance; /// /// Gets 背景设置model /// public static BackgroundSettingsUserControlModel BackgroundSettings { get; } = BackgroundSettingsUserControlModel.Instance; /// /// Gets 定时设置model /// public static TimerSettingsUserControlModel TimerSettings { get; } = TimerSettingsUserControlModel.Instance; /// /// Gets 远程控制model /// public static RemoteControlUserControlModel RemoteControlSettings { get; } = RemoteControlUserControlModel.Instance; /// /// Gets 软件更新model /// public static VersionUpdateSettingsUserControlModel VersionUpdateSettings { get; } = VersionUpdateSettingsUserControlModel.Instance; /// /// Gets 外部通知model /// public static ExternalNotificationSettingsUserControlModel ExternalNotificationSettings { get; } = ExternalNotificationSettingsUserControlModel.Instance; /// /// Gets 性能设置model /// public static PerformanceUserControlModel PerformanceSettings { get; } = PerformanceUserControlModel.Instance; /// /// Gets 问题反馈model /// public static IssueReportUserControlModel IssueReportSettings { get; } = IssueReportUserControlModel.Instance; #endregion 设置界面Model /// /// Initializes a new instance of the class. /// public SettingsViewModel() { DisplayName = LocalizationHelper.GetString("Settings"); Init(); HangoverEnd(); _runningState = RunningState.Instance; } #region Init private List _listTitle = [ LocalizationHelper.GetString("SwitchConfiguration"), LocalizationHelper.GetString("ScheduleSettings"), LocalizationHelper.GetString("PerformanceSettings"), LocalizationHelper.GetString("GameSettings"), LocalizationHelper.GetString("ConnectionSettings"), LocalizationHelper.GetString("StartupSettings"), LocalizationHelper.GetString("RemoteControlSettings"), LocalizationHelper.GetString("UiSettings"), LocalizationHelper.GetString("BackgroundSettings"), LocalizationHelper.GetString("ExternalNotificationSettings"), LocalizationHelper.GetString("HotKeySettings"), LocalizationHelper.GetString("UpdateSettings"), LocalizationHelper.GetString("IssueReport"), LocalizationHelper.GetString("AboutUs"), ]; /// /// Gets or sets the list title. /// public List ListTitle { get => _listTitle; set => SetAndNotify(ref _listTitle, value); } private bool _idle; /// /// Gets or sets a value indicating whether it is idle. /// public bool Idle { get => _idle; set => SetAndNotify(ref _idle, value); } private void Init() { TaskQueueViewModel.InfrastTask.InitInfrast(); TaskQueueViewModel.RoguelikeTask.InitRoguelike(); InitConfiguration(); InitUiSettings(); InitConnectConfig(); InitVersionUpdate(); } private void InitConfiguration() { var configurations = new ObservableCollection(); foreach (var conf in ConfigurationHelper.GetConfigurationList()) { configurations.Add(new CombinedData { Display = conf, Value = conf }); } ConfigurationList = configurations; } private void InitUiSettings() { var languageList = (from pair in LocalizationHelper.SupportedLanguages where pair.Key != PallasLangKey || Cheers select new CombinedData { Display = pair.Value, Value = pair.Key }) .ToList(); GuiSettings.LanguageList = languageList; GuiSettings.SwitchDarkMode(); } private void InitConnectConfig() { var addressListJson = ConfigurationHelper.GetValue(ConfigurationKeys.AddressHistory, string.Empty); if (!string.IsNullOrEmpty(addressListJson)) { ConnectSettings.ConnectAddressHistory = JsonConvert.DeserializeObject>(addressListJson) ?? []; } } private void InitVersionUpdate() { if (VersionUpdateSettings.VersionType == VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly && !VersionUpdateSettings.AllowNightlyUpdates) { VersionUpdateSettings.VersionType = VersionUpdateSettingsUserControlModel.UpdateVersionType.Beta; } } #endregion Init #region EasterEggs /// /// The Pallas language key. /// public const string PallasLangKey = "pallas"; private bool _cheers = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Cheers, bool.FalseString)); /// /// Gets or sets a value indicating whether to cheer. /// public bool Cheers { get => _cheers; set { if (_cheers == value) { return; } SetAndNotify(ref _cheers, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Cheers, value.ToString()); if (_cheers) { ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Localization, PallasLangKey); } } } private bool _hangover = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Hangover, bool.FalseString)); /// /// Gets or sets a value indicating whether to hangover. /// public bool Hangover { get => _hangover; set { SetAndNotify(ref _hangover, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Hangover, value.ToString()); } } private string _lastBuyWineTime = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.LastBuyWineTime, DateTime.UtcNow.ToYjDate().AddDays(-1).ToFormattedString()); public string LastBuyWineTime { get => _lastBuyWineTime; set { SetAndNotify(ref _lastBuyWineTime, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.LastBuyWineTime, value); } } public void HangoverEnd() { if (!Hangover) { return; } Hangover = false; MessageBoxHelper.Show( LocalizationHelper.GetString("Hangover"), LocalizationHelper.GetString("Burping"), iconKey: "HangoverGeometry", iconBrushKey: "PallasBrush"); Bootstrapper.ShutdownAndRestartWithoutArgs(); } public void Sober() { if (!Cheers || GuiSettings.Language != PallasLangKey) { return; } ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Localization, SoberLanguage); Hangover = true; Cheers = false; } private string _soberLanguage = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.SoberLanguage, LocalizationHelper.DefaultLanguage); public string SoberLanguage { get => _soberLanguage; set { SetAndNotify(ref _soberLanguage, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.SoberLanguage, value); } } /// /// Did you buy wine? /// /// The answer. public bool DidYouBuyWine() { var now = DateTime.UtcNow.ToYjDate(); if (now == DateTime.ParseExact(LastBuyWineTime.Replace('-', '/'), "yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)) { return false; } // if (now.IsAprilFoolsDay()) // { // return true; // } string[] wineList = ["酒", "liquor", "drink", "wine", "beer", "술", "🍷", "🍸", "🍺", "🍻", "🥃", "🍶"]; return wineList.Any(TaskQueueViewModel.MallTask.CreditFirstList.Contains); } #endregion EasterEggs #region HotKey /// /// Gets or sets the hotkey: ShowGui. /// public static MaaHotKey HotKeyShowGui { get => Instances.MaaHotKeyManager.GetOrNull(MaaHotKeyAction.ShowGui); set => SetHotKey(MaaHotKeyAction.ShowGui, value); } /// /// Gets or sets the hotkey: LinkStart. /// public static MaaHotKey HotKeyLinkStart { get => Instances.MaaHotKeyManager.GetOrNull(MaaHotKeyAction.LinkStart); set => SetHotKey(MaaHotKeyAction.LinkStart, value); } private static void SetHotKey(MaaHotKeyAction action, MaaHotKey value) { if (value != null) { Instances.MaaHotKeyManager.TryRegister(action, value); } else { Instances.MaaHotKeyManager.UnRegister(action); } } #endregion HotKey #region 配置 public ObservableCollection ConfigurationList { get; set; } = []; private string? _currentConfiguration = ConfigurationHelper.GetCurrentConfiguration(); public string? CurrentConfiguration { get => _currentConfiguration; set { SetAndNotify(ref _currentConfiguration, value); ConfigurationHelper.SwitchConfiguration(value); Bootstrapper.ShutdownAndRestartWithoutArgs(); } } private string _newConfigurationName = string.Empty; public string NewConfigurationName { get => _newConfigurationName; set => SetAndNotify(ref _newConfigurationName, value); } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void AddConfiguration() { if (string.IsNullOrEmpty(NewConfigurationName)) { NewConfigurationName = DateTime.Now.ToString("yy/MM/dd HH:mm:ss"); } if (ConfigurationHelper.AddConfiguration(NewConfigurationName, CurrentConfiguration)) { ConfigurationList.Add(new CombinedData { Display = NewConfigurationName, Value = NewConfigurationName }); var growlInfo = new GrowlInfo { IsCustom = true, Message = string.Format(LocalizationHelper.GetString("AddConfigSuccess"), NewConfigurationName), IconKey = "HangoverGeometry", IconBrushKey = "PallasBrush", }; Growl.Info(growlInfo); } else { var growlInfo = new GrowlInfo { IsCustom = true, Message = string.Format(LocalizationHelper.GetString("ConfigExists"), NewConfigurationName), IconKey = "HangoverGeometry", IconBrushKey = "PallasBrush", }; Growl.Info(growlInfo); } } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void DeleteConfiguration(CombinedData delete) { if (ConfigurationHelper.DeleteConfiguration(delete.Display)) { ConfigurationList.Remove(delete); } } #endregion 配置 #region SettingsGuide // 目前共4步,再多塞不下了,后续可以整个新功能展示( public const int GuideMaxStep = 4; private int _guideStepIndex = Convert.ToInt32(ConfigurationHelper.GetValue(ConfigurationKeys.GuideStepIndex, "0")); public int GuideStepIndex { get => _guideStepIndex; set { SetAndNotify(ref _guideStepIndex, value); ConfigurationHelper.SetValue(ConfigurationKeys.GuideStepIndex, value.ToString()); } } private string _guideTransitionMode = "Bottom2Top"; public string GuideTransitionMode { get => _guideTransitionMode; set => SetAndNotify(ref _guideTransitionMode, value); } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void NextGuide(StepBar stepBar) { GuideTransitionMode = "Bottom2Top"; stepBar.Next(); } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void PrevGuide(StepBar stepBar) { GuideTransitionMode = "Top2Bottom"; stepBar.Prev(); } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public void DoneGuide() { TaskSettingVisibilities.Guide = false; GuideStepIndex = GuideMaxStep; } #endregion SettingsGuide #region 设置页面列表和滚动视图联动绑定 private enum NotifyType { None, SelectedIndex, ScrollOffset, } private NotifyType _notifySource = NotifyType.None; private Timer? _resetNotifyTimer; private void ResetNotifySource() { if (_resetNotifyTimer != null) { _resetNotifyTimer.Stop(); _resetNotifyTimer.Close(); } _resetNotifyTimer = new Timer(20); _resetNotifyTimer.Elapsed += (_, _) => { _notifySource = NotifyType.None; }; _resetNotifyTimer.AutoReset = false; _resetNotifyTimer.Enabled = true; _resetNotifyTimer.Start(); } /// /// Gets or sets the height of scroll viewport. /// public double ScrollViewportHeight { get; set; } /// /// Gets or sets the extent height of scroll. /// public double ScrollExtentHeight { get; set; } /// /// Gets or sets the list of divider vertical offset. /// public List DividerVerticalOffsetList { get; set; } = new(); private int _selectedIndex; /// /// Gets or sets the index selected. /// public int SelectedIndex { get => _selectedIndex; set { switch (_notifySource) { case NotifyType.None: _notifySource = NotifyType.SelectedIndex; SetAndNotify(ref _selectedIndex, value); if (DividerVerticalOffsetList?.Count > 0 && value < DividerVerticalOffsetList.Count) { ScrollOffset = DividerVerticalOffsetList[value]; } ResetNotifySource(); break; case NotifyType.ScrollOffset: SetAndNotify(ref _selectedIndex, value); break; case NotifyType.SelectedIndex: break; default: throw new ArgumentOutOfRangeException(); } } } private double _scrollOffset; /// /// Gets or sets the scroll offset. /// public double ScrollOffset { get => _scrollOffset; set { switch (_notifySource) { case NotifyType.None: _notifySource = NotifyType.ScrollOffset; SetAndNotify(ref _scrollOffset, value); // 设置 ListBox SelectedIndex 为当前 ScrollOffset 索引 if (DividerVerticalOffsetList?.Count > 0) { // 滚动条滚动到底部,返回最后一个 Divider 索引 if (value + ScrollViewportHeight >= ScrollExtentHeight) { SelectedIndex = DividerVerticalOffsetList.Count - 1; ResetNotifySource(); break; } // 根据出当前 ScrollOffset 选出最后一个在可视范围的 Divider 索引 var dividerSelect = DividerVerticalOffsetList.Select((n, i) => ( dividerAppeared: value >= n, index: i)); var index = dividerSelect.LastOrDefault(n => n.dividerAppeared).index; SelectedIndex = index; } ResetNotifySource(); break; case NotifyType.SelectedIndex: SetAndNotify(ref _scrollOffset, value); break; case NotifyType.ScrollOffset: break; default: throw new ArgumentOutOfRangeException(); } } } #endregion 设置页面列表和滚动视图联动绑定 /// /// Requires the user to restart to apply settings. /// /// Whether to include the YostarEN resolution tip. public static void AskRestartToApplySettings(bool isYostarEN = false) { var resolutionTip = isYostarEN ? "\n" + LocalizationHelper.GetString("SwitchResolutionTip") : string.Empty; var result = MessageBoxHelper.Show( LocalizationHelper.GetString("PromptRestartForSettingsChange") + resolutionTip, LocalizationHelper.GetString("Tip"), MessageBoxButton.OKCancel, MessageBoxImage.Question); if (result == MessageBoxResult.OK) { Bootstrapper.ShutdownAndRestartWithoutArgs(); } } /// /// Make comboBox searchable /// /// Event sender /// Event args // UI 绑定的方法 // EventArgs 不能省略,否则会报错 // ReSharper disable once UnusedMember.Global // ReSharper disable once UnusedParameter.Global public static void MakeComboBoxSearchable(object sender, EventArgs e) { (sender as ComboBox)?.MakeComboBoxSearchable(); } // UI 绑定的方法 // ReSharper disable once UnusedMember.Global public async Task CheckAndDownloadAnnouncement() { await Instances.AnnouncementViewModel.CheckAndDownloadAnnouncement(); _ = Execute.OnUIThreadAsync(() => Instances.WindowManager.ShowWindow(Instances.AnnouncementViewModel)); } /// /// 标题栏显示模拟器名称和IP端口。 /// public void UpdateWindowTitle() { var rvm = (RootViewModel)this.Parent; string updateTip = string.Empty; var newVersionFoundInfo = VersionUpdateSettings.NewVersionFoundInfo; var coreVersion = VersionUpdateSettingsUserControlModel.CoreVersion; var startupUpdateCheck = VersionUpdateSettings.StartupUpdateCheck; var isDebug = Instances.VersionUpdateViewModel.IsDebugVersion(); if (newVersionFoundInfo != coreVersion && !isDebug && !string.IsNullOrEmpty(newVersionFoundInfo) && startupUpdateCheck) { updateTip = $"{newVersionFoundInfo} - "; } var newResourceFoundInfo = VersionUpdateSettings.NewResourceFoundInfo; if (!string.IsNullOrEmpty(newResourceFoundInfo)) { updateTip += $"{newResourceFoundInfo} - "; } string prefix = ConfigurationHelper.GetValue(ConfigurationKeys.WindowTitlePrefix, string.Empty); if (!string.IsNullOrEmpty(prefix)) { prefix += " - "; } List windowTitleSelectShowList = GuiSettings.WindowTitleSelectShowList .Cast>().Select(pair => pair.Key) .ToList(); string currentConfiguration = string.Empty; string connectConfigName = string.Empty; string connectAddress = string.Empty; string clientName = string.Empty; foreach (var select in windowTitleSelectShowList) { switch (select) { case "1": // 配置名 currentConfiguration = $" ({CurrentConfiguration})"; break; case "2": // 连接模式 foreach (var data in ConnectSettings.ConnectConfigList.Where(data => data.Value == ConnectSettings.ConnectConfig)) { connectConfigName = $" - {data.Display}"; } break; case "3": // 端口地址 connectAddress = $" ({ConnectSettings.ConnectAddress})"; break; case "4": // 客户端类型 clientName = $" - {ClientName}"; break; } } string resourceVersion = !string.IsNullOrEmpty(VersionUpdateSettings.ResourceVersion) ? $" - {LocalizationHelper.FormatResourceVersion(VersionUpdateSettings.ResourceVersion, VersionUpdateSettings.ResourceDateTime)}" : string.Empty; rvm.WindowTitle = $"{updateTip}{prefix}MAA{currentConfiguration} - {coreVersion}{resourceVersion}{connectConfigName}{connectAddress}{clientName}"; } /// /// Gets the client type. /// private string ClientName { get { foreach (var item in GameSettings.ClientTypeList.Where(item => item.Value == GameSettings.ClientType)) { return item.Display; } return "Unknown Client"; } } private readonly Dictionary _serverMapping = new() { { string.Empty, "CN" }, { "Official", "CN" }, { "Bilibili", "CN" }, { "YoStarEN", "US" }, { "YoStarJP", "JP" }, { "YoStarKR", "KR" }, { "txwy", "ZH_TW" }, }; /// /// Gets the server type. /// public string ServerType => _serverMapping[GameSettings.ClientType]; } }