From b78d6441014ed5b0c68934d2e6e8c92f8182fefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=89=E9=B8=AD=E8=9B=8B?= Date: Tue, 29 Jul 2025 23:39:31 +0800 Subject: [PATCH] Add GearTriggerPage and related ViewModel; implement trigger functionality --- .../Model/Gear/Triggers/HotkeyGearTrigger.cs | 83 ++++ .../Model/Gear/Triggers/TimedGearTrigger.cs | 90 ++++ .../Converters/EnumToVisibilityConverter.cs | 30 ++ .../Converters/NullToVisibilityConverter.cs | 30 ++ .../View/Pages/GearTriggerPage.xaml | 404 ++++++++++++++++++ .../View/Pages/GearTriggerPage.xaml.cs | 19 + .../Pages/Component/GearTriggerViewModel.cs | 176 ++++++++ .../Pages/GearTriggerPageViewModel.cs | 310 ++++++++++++++ 8 files changed, 1142 insertions(+) create mode 100644 BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs create mode 100644 BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs create mode 100644 BetterGenshinImpact/View/Converters/EnumToVisibilityConverter.cs create mode 100644 BetterGenshinImpact/View/Converters/NullToVisibilityConverter.cs create mode 100644 BetterGenshinImpact/View/Pages/GearTriggerPage.xaml create mode 100644 BetterGenshinImpact/View/Pages/GearTriggerPage.xaml.cs create mode 100644 BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs create mode 100644 BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs diff --git a/BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs b/BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs new file mode 100644 index 00000000..d614baca --- /dev/null +++ b/BetterGenshinImpact/Model/Gear/Triggers/HotkeyGearTrigger.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System; +using BetterGenshinImpact.Model.Gear.Tasks; +using BetterGenshinImpact.Model; + +namespace BetterGenshinImpact.Model.Gear.Triggers; + +/// +/// 热键触发器 +/// +public class HotkeyGearTrigger : GearBaseTrigger +{ + /// + /// 热键配置 + /// + public HotKey? Hotkey { get; set; } + + /// + /// 是否启用 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 冷却时间(毫秒),防止重复触发 + /// + public int CooldownMs { get; set; } = 1000; + + private DateTime _lastExecutionTime = DateTime.MinValue; + private bool _isExecuting = false; + + public override async Task Run() + { + // 热键触发器通常不直接调用Run方法 + // 而是通过热键事件触发ExecuteTasks方法 + await ExecuteTasks(); + } + + /// + /// 热键触发时执行任务 + /// + public async Task OnHotkeyPressed() + { + if (!IsEnabled || _isExecuting) + return; + + // 检查冷却时间 + var now = DateTime.Now; + if ((now - _lastExecutionTime).TotalMilliseconds < CooldownMs) + return; + + _lastExecutionTime = now; + _isExecuting = true; + + try + { + await ExecuteTasks(); + } + finally + { + _isExecuting = false; + } + } + + private async Task ExecuteTasks() + { + List list = GearTaskRefenceList.Select(gearTask => gearTask.ToGearTask()).ToList(); + foreach (var gearTask in list) + { + await gearTask.Run(CancellationToken.None); + } + } + + /// + /// 获取热键显示文本 + /// + public string GetHotkeyText() + { + return Hotkey?.ToString() ?? "未设置"; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs b/BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs new file mode 100644 index 00000000..4da3776b --- /dev/null +++ b/BetterGenshinImpact/Model/Gear/Triggers/TimedGearTrigger.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using BetterGenshinImpact.Model.Gear.Tasks; + +namespace BetterGenshinImpact.Model.Gear.Triggers; + +/// +/// 定时触发器 +/// +public class TimedGearTrigger : GearBaseTrigger +{ + /// + /// 触发间隔(毫秒) + /// + public int IntervalMs { get; set; } = 5000; + + /// + /// 是否重复执行 + /// + public bool IsRepeating { get; set; } = true; + + /// + /// 延迟启动时间(毫秒) + /// + public int DelayMs { get; set; } = 0; + + /// + /// 最大执行次数(0表示无限制) + /// + public int MaxExecutions { get; set; } = 0; + + private int _executionCount = 0; + private CancellationTokenSource? _cancellationTokenSource; + + public override async Task Run() + { + _cancellationTokenSource = new CancellationTokenSource(); + _executionCount = 0; + + // 延迟启动 + if (DelayMs > 0) + { + await Task.Delay(DelayMs, _cancellationTokenSource.Token); + } + + do + { + if (_cancellationTokenSource.Token.IsCancellationRequested) + break; + + // 执行任务 + await ExecuteTasks(); + _executionCount++; + + // 检查是否达到最大执行次数 + if (MaxExecutions > 0 && _executionCount >= MaxExecutions) + break; + + // 如果不重复执行,则退出 + if (!IsRepeating) + break; + + // 等待下次执行 + await Task.Delay(IntervalMs, _cancellationTokenSource.Token); + } + while (IsRepeating && !_cancellationTokenSource.Token.IsCancellationRequested); + } + + /// + /// 停止触发器 + /// + public void Stop() + { + _cancellationTokenSource?.Cancel(); + } + + private async Task ExecuteTasks() + { + List list = GearTaskRefenceList.Select(gearTask => gearTask.ToGearTask()).ToList(); + foreach (var gearTask in list) + { + if (_cancellationTokenSource?.Token.IsCancellationRequested == true) + break; + await gearTask.Run(_cancellationTokenSource?.Token ?? CancellationToken.None); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/View/Converters/EnumToVisibilityConverter.cs b/BetterGenshinImpact/View/Converters/EnumToVisibilityConverter.cs new file mode 100644 index 00000000..5ba4e90c --- /dev/null +++ b/BetterGenshinImpact/View/Converters/EnumToVisibilityConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace BetterGenshinImpact.View.Converters; + +[ValueConversion(typeof(Enum), typeof(Visibility))] +public class EnumToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value == null || parameter == null) + { + return Visibility.Collapsed; + } + + var enumValue = value.ToString(); + var targetValue = parameter.ToString(); + + return string.Equals(enumValue, targetValue, StringComparison.OrdinalIgnoreCase) + ? Visibility.Visible + : Visibility.Collapsed; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/View/Converters/NullToVisibilityConverter.cs b/BetterGenshinImpact/View/Converters/NullToVisibilityConverter.cs new file mode 100644 index 00000000..c4b4f275 --- /dev/null +++ b/BetterGenshinImpact/View/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace BetterGenshinImpact.View.Converters; + +[ValueConversion(typeof(object), typeof(Visibility))] +public class NullToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var isInvert = parameter?.ToString()?.Equals("Invert", StringComparison.OrdinalIgnoreCase) == true; + var isNull = value == null; + + if (isInvert) + { + return isNull ? Visibility.Visible : Visibility.Collapsed; + } + else + { + return isNull ? Visibility.Collapsed : Visibility.Visible; + } + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml b/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml new file mode 100644 index 00000000..2837f7dd --- /dev/null +++ b/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml.cs b/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml.cs new file mode 100644 index 00000000..49104063 --- /dev/null +++ b/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml.cs @@ -0,0 +1,19 @@ +using System.Windows.Controls; +using BetterGenshinImpact.ViewModel.Pages; + +namespace BetterGenshinImpact.View.Pages; + +/// +/// GearTriggerPage.xaml 的交互逻辑 +/// +public partial class GearTriggerPage : UserControl +{ + public GearTriggerPageViewModel ViewModel { get; } + + public GearTriggerPage(GearTriggerPageViewModel viewModel) + { + ViewModel = viewModel; + DataContext = this; + InitializeComponent(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs new file mode 100644 index 00000000..aede491c --- /dev/null +++ b/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using BetterGenshinImpact.Model.Gear.Triggers; +using BetterGenshinImpact.Model.Gear.Tasks; +using BetterGenshinImpact.Model; + +namespace BetterGenshinImpact.ViewModel.Pages.Component; + +/// +/// 触发器ViewModel +/// +public partial class GearTriggerViewModel : ObservableObject +{ + [ObservableProperty] + private string _name = string.Empty; + + [ObservableProperty] + private string _description = string.Empty; + + [ObservableProperty] + private bool _isEnabled = true; + + [ObservableProperty] + private bool _isSelected = false; + + [ObservableProperty] + private DateTime _createdTime = DateTime.Now; + + [ObservableProperty] + private DateTime _modifiedTime = DateTime.Now; + + /// + /// 触发器类型 + /// + [ObservableProperty] + private TriggerType _triggerType = TriggerType.Sequential; + + /// + /// 任务引用列表 + /// + [ObservableProperty] + private ObservableCollection _taskReferences = new(); + + // 顺序触发器属性(无额外属性) + + // 定时触发器属性 + [ObservableProperty] + private int _intervalMs = 5000; + + [ObservableProperty] + private bool _isRepeating = true; + + [ObservableProperty] + private int _delayMs = 0; + + [ObservableProperty] + private int _maxExecutions = 0; + + // 热键触发器属性 + [ObservableProperty] + private HotKey? _hotkey; + + [ObservableProperty] + private int _cooldownMs = 1000; + + public GearTriggerViewModel() + { + } + + public GearTriggerViewModel(string name, TriggerType triggerType) + { + Name = name; + TriggerType = triggerType; + Description = GetDefaultDescription(triggerType); + } + + /// + /// 获取触发器类型的默认描述 + /// + private string GetDefaultDescription(TriggerType triggerType) + { + return triggerType switch + { + TriggerType.Sequential => "按顺序执行任务", + TriggerType.Timed => "定时执行任务", + TriggerType.Hotkey => "热键触发执行任务", + _ => "未知触发器类型" + }; + } + + /// + /// 获取触发器类型显示名称 + /// + public string TriggerTypeDisplayName => TriggerType switch + { + TriggerType.Sequential => "顺序触发", + TriggerType.Timed => "定时触发", + TriggerType.Hotkey => "热键触发", + _ => "未知类型" + }; + + /// + /// 获取热键显示文本 + /// + public string HotkeyDisplayText => Hotkey?.ToString() ?? "未设置"; + + /// + /// 获取定时器配置显示文本 + /// + public string TimerDisplayText + { + get + { + if (TriggerType != TriggerType.Timed) return ""; + + var text = $"间隔: {IntervalMs}ms"; + if (DelayMs > 0) text += $", 延迟: {DelayMs}ms"; + if (MaxExecutions > 0) text += $", 最大次数: {MaxExecutions}"; + if (!IsRepeating) text += ", 单次执行"; + + return text; + } + } + + /// + /// 转换为对应的触发器实例 + /// + public GearBaseTrigger ToTrigger() + { + GearBaseTrigger trigger = TriggerType switch + { + TriggerType.Sequential => new SequentialGearTrigger(), + TriggerType.Timed => new TimedGearTrigger + { + IntervalMs = IntervalMs, + IsRepeating = IsRepeating, + DelayMs = DelayMs, + MaxExecutions = MaxExecutions + }, + TriggerType.Hotkey => new HotkeyGearTrigger + { + Hotkey = Hotkey, + CooldownMs = CooldownMs, + IsEnabled = IsEnabled + }, + _ => new SequentialGearTrigger() + }; + + trigger.Name = Name; + trigger.GearTaskRefenceList = new ObservableCollection(TaskReferences); + + return trigger; + } +} + +/// +/// 触发器类型枚举 +/// +public enum TriggerType +{ + /// + /// 顺序触发 + /// + Sequential, + + /// + /// 定时触发 + /// + Timed, + + /// + /// 热键触发 + /// + Hotkey +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs new file mode 100644 index 00000000..234f06e9 --- /dev/null +++ b/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using BetterGenshinImpact.ViewModel.Pages.Component; +using BetterGenshinImpact.Model.Gear.Tasks; +using BetterGenshinImpact.Model; + +namespace BetterGenshinImpact.ViewModel.Pages; + +/// +/// 任务触发页面ViewModel +/// +public partial class GearTriggerPageViewModel : ViewModel +{ + private readonly ILogger _logger; + + /// + /// 顺序触发器列表 + /// + [ObservableProperty] + private ObservableCollection _sequentialTriggers = new(); + + /// + /// 定时触发器列表 + /// + [ObservableProperty] + private ObservableCollection _timedTriggers = new(); + + /// + /// 热键触发器列表 + /// + [ObservableProperty] + private ObservableCollection _hotkeyTriggers = new(); + + /// + /// 当前选中的触发器 + /// + [ObservableProperty] + private GearTriggerViewModel? _selectedTrigger; + + /// + /// 当前选中的触发器类型Tab + /// + [ObservableProperty] + private int _selectedTriggerTypeIndex = 0; + + /// + /// 触发器配置中的任务定义列表 + /// + [ObservableProperty] + private ObservableCollection _availableTaskDefinitions = new(); + + /// + /// 当前触发器的任务引用列表 + /// + [ObservableProperty] + private ObservableCollection _currentTaskReferences = new(); + + /// + /// 当前选中的任务引用 + /// + [ObservableProperty] + private GearTaskRefence? _selectedTaskReference; + + public GearTriggerPageViewModel(ILogger logger) + { + _logger = logger; + InitializeData(); + } + + private void InitializeData() + { + // 初始化示例数据 + var sequentialTrigger = new GearTriggerViewModel("示例顺序触发器", TriggerType.Sequential); + SequentialTriggers.Add(sequentialTrigger); + + var timedTrigger = new GearTriggerViewModel("示例定时触发器", TriggerType.Timed) + { + IntervalMs = 10000, + IsRepeating = true + }; + TimedTriggers.Add(timedTrigger); + + var hotkeyTrigger = new GearTriggerViewModel("示例热键触发器", TriggerType.Hotkey) + { + Hotkey = new HotKey(System.Windows.Input.Key.F1, System.Windows.Input.ModifierKeys.Control) + }; + HotkeyTriggers.Add(hotkeyTrigger); + + // 初始化可用的任务定义(这里应该从实际的任务定义服务获取) + LoadAvailableTaskDefinitions(); + } + + private void LoadAvailableTaskDefinitions() + { + // 这里应该从实际的任务定义服务获取 + // 暂时创建示例数据 + var sampleTask = new GearTaskDefinitionViewModel("示例任务组", "这是一个示例任务组"); + AvailableTaskDefinitions.Add(sampleTask); + } + + partial void OnSelectedTriggerChanged(GearTriggerViewModel? value) + { + UpdateCurrentTaskReferences(); + } + + private void UpdateCurrentTaskReferences() + { + CurrentTaskReferences.Clear(); + if (SelectedTrigger != null) + { + foreach (var taskRef in SelectedTrigger.TaskReferences) + { + CurrentTaskReferences.Add(taskRef); + } + } + } + + /// + /// 添加顺序触发器 + /// + [RelayCommand] + private void AddSequentialTrigger() + { + var trigger = new GearTriggerViewModel($"顺序触发器 {SequentialTriggers.Count + 1}", TriggerType.Sequential); + SequentialTriggers.Add(trigger); + SelectedTrigger = trigger; + SelectedTriggerTypeIndex = 0; + } + + /// + /// 添加定时触发器 + /// + [RelayCommand] + private void AddTimedTrigger() + { + var trigger = new GearTriggerViewModel($"定时触发器 {TimedTriggers.Count + 1}", TriggerType.Timed); + TimedTriggers.Add(trigger); + SelectedTrigger = trigger; + SelectedTriggerTypeIndex = 1; + } + + /// + /// 添加热键触发器 + /// + [RelayCommand] + private void AddHotkeyTrigger() + { + var trigger = new GearTriggerViewModel($"热键触发器 {HotkeyTriggers.Count + 1}", TriggerType.Hotkey); + HotkeyTriggers.Add(trigger); + SelectedTrigger = trigger; + SelectedTriggerTypeIndex = 2; + } + + /// + /// 删除触发器 + /// + [RelayCommand] + private void DeleteTrigger(GearTriggerViewModel? trigger) + { + if (trigger == null) return; + + var result = MessageBox.Show($"确定要删除触发器 '{trigger.Name}' 吗?", "确认删除", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result == MessageBoxResult.Yes) + { + switch (trigger.TriggerType) + { + case TriggerType.Sequential: + SequentialTriggers.Remove(trigger); + break; + case TriggerType.Timed: + TimedTriggers.Remove(trigger); + break; + case TriggerType.Hotkey: + HotkeyTriggers.Remove(trigger); + break; + } + + if (SelectedTrigger == trigger) + { + SelectedTrigger = null; + } + } + } + + /// + /// 编辑触发器 + /// + [RelayCommand] + private void EditTrigger(GearTriggerViewModel? trigger) + { + if (trigger == null) return; + SelectedTrigger = trigger; + + // 切换到对应的Tab + SelectedTriggerTypeIndex = trigger.TriggerType switch + { + TriggerType.Sequential => 0, + TriggerType.Timed => 1, + TriggerType.Hotkey => 2, + _ => 0 + }; + } + + /// + /// 添加任务定义到当前触发器 + /// + [RelayCommand] + private void AddTaskDefinition(GearTaskDefinitionViewModel? taskDefinition) + { + if (taskDefinition == null || SelectedTrigger == null) return; + + // 创建任务引用 + var taskRef = new GearTaskRefence + { + Name = taskDefinition.Name, + Enabled = true, + GearTaskFilePath = $"tasks/{taskDefinition.Name}.json" // 这里应该是实际的文件路径 + }; + + SelectedTrigger.TaskReferences.Add(taskRef); + CurrentTaskReferences.Add(taskRef); + } + + /// + /// 删除任务引用 + /// + [RelayCommand] + private void RemoveTaskReference(GearTaskRefence? taskRef) + { + if (taskRef == null || SelectedTrigger == null) return; + + SelectedTrigger.TaskReferences.Remove(taskRef); + CurrentTaskReferences.Remove(taskRef); + } + + /// + /// 上移任务引用 + /// + [RelayCommand] + private void MoveTaskReferenceUp(GearTaskRefence? taskRef) + { + if (taskRef == null || SelectedTrigger == null) return; + + var index = CurrentTaskReferences.IndexOf(taskRef); + if (index > 0) + { + CurrentTaskReferences.Move(index, index - 1); + SelectedTrigger.TaskReferences.Move(index, index - 1); + } + } + + /// + /// 下移任务引用 + /// + [RelayCommand] + private void MoveTaskReferenceDown(GearTaskRefence? taskRef) + { + if (taskRef == null || SelectedTrigger == null) return; + + var index = CurrentTaskReferences.IndexOf(taskRef); + if (index < CurrentTaskReferences.Count - 1) + { + CurrentTaskReferences.Move(index, index + 1); + SelectedTrigger.TaskReferences.Move(index, index + 1); + } + } + + /// + /// 保存配置 + /// + [RelayCommand] + private void SaveConfiguration() + { + try + { + // 这里应该实现保存到文件的逻辑 + _logger.LogInformation("触发器配置已保存"); + MessageBox.Show("配置保存成功!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + _logger.LogError(ex, "保存触发器配置时发生错误"); + MessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + /// + /// 加载配置 + /// + [RelayCommand] + private void LoadConfiguration() + { + try + { + // 这里应该实现从文件加载的逻辑 + _logger.LogInformation("触发器配置已加载"); + MessageBox.Show("配置加载成功!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + _logger.LogError(ex, "加载触发器配置时发生错误"); + MessageBox.Show($"加载失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } +} \ No newline at end of file