Files
better-genshin-impact/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs
2025-11-16 00:55:51 +08:00

2486 lines
82 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Core.Script.Group;
using BetterGenshinImpact.Core.Script.Project;
using BetterGenshinImpact.Core.Script.Utils;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
using BetterGenshinImpact.GameTask.LogParse;
using BetterGenshinImpact.GameTask.TaskProgress;
using BetterGenshinImpact.Helpers.Ui;
using BetterGenshinImpact.Model;
using BetterGenshinImpact.Service.Interface;
using BetterGenshinImpact.View.Controls.Webview;
using BetterGenshinImpact.View.Pages.View;
using BetterGenshinImpact.View.Windows;
using BetterGenshinImpact.View.Windows.Editable;
using BetterGenshinImpact.ViewModel.Pages.View;
using BetterGenshinImpact.ViewModel.Windows.Editable;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using Wpf.Ui;
using Wpf.Ui.Controls;
using Wpf.Ui.Violeta.Controls;
using Button = Wpf.Ui.Controls.Button;
using MessageBoxButton = System.Windows.MessageBoxButton;
using MessageBoxResult = Wpf.Ui.Controls.MessageBoxResult;
using StackPanel = Wpf.Ui.Controls.StackPanel;
using TextBlock = Wpf.Ui.Controls.TextBlock;
using TextBox = Wpf.Ui.Controls.TextBox;
namespace BetterGenshinImpact.ViewModel.Pages;
public partial class ScriptControlViewModel : ViewModel
{
private readonly ISnackbarService _snackbarService;
private readonly ILogger<ScriptControlViewModel> _logger = App.GetLogger<ScriptControlViewModel>();
private readonly IScriptService _scriptService;
/// <summary>
/// 配置组配置
/// </summary>
[ObservableProperty]
private ObservableCollection<ScriptGroup> _scriptGroups = [];
/// <summary>
/// 当前选中的配置组
/// </summary>
[ObservableProperty]
private ScriptGroup? _selectedScriptGroup = null;
public readonly string ScriptGroupPath = Global.Absolute(@"User\ScriptGroup");
public readonly string LogPath = Global.Absolute(@"log");
public override void OnNavigatedTo()
{
ReadScriptGroup();
}
public ScriptControlViewModel(ISnackbarService snackbarService, IScriptService scriptService)
{
_snackbarService = snackbarService;
_scriptService = scriptService;
ScriptGroups.CollectionChanged += ScriptGroupsCollectionChanged;
}
[RelayCommand]
private void OnAddScriptGroup()
{
// 创建一个TextBox并设置自动聚焦
var textBox = new System.Windows.Controls.TextBox()
{
VerticalAlignment = VerticalAlignment.Top
};
textBox.Loaded += (sender, e) =>
{
textBox.Focus();
textBox.SelectAll();
};
var str = PromptDialog.Prompt("请输入配置组名称", "新增配置组", textBox);
if (!string.IsNullOrEmpty(str))
{
// 检查是否已存在
if (ScriptGroups.Any(x => x.Name == str))
{
_snackbarService.Show(
"配置组已存在",
$"配置组 {str} 已经存在,请勿重复添加",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
}
else
{
ScriptGroups.Add(new ScriptGroup { Name = str });
}
}
}
[RelayCommand]
private void ClearTasks()
{
// 确认?
var result = ThemedMessageBox.Question("是否清空所有任务?", "清空任务", MessageBoxButton.YesNo, System.Windows.MessageBoxResult.No);
if (result != System.Windows.MessageBoxResult.Yes)
{
return;
}
if (SelectedScriptGroup == null)
{
return;
}
SelectedScriptGroup.Projects.Clear();
WriteScriptGroup(SelectedScriptGroup);
}
[RelayCommand]
private async Task OpenLogParse()
{
if (SelectedScriptGroup == null)
{
return;
}
GameInfo? gameInfo = null;
var config = LogParse.LoadConfig();
OtherConfig.Miyoushe mcfg = TaskContext.Instance().Config.OtherConfig.MiyousheConfig;
if (mcfg.LogSyncCookie && !string.IsNullOrEmpty(mcfg.Cookie))
{
config.Cookie = mcfg.Cookie;
}
if (!string.IsNullOrEmpty(config.Cookie))
{
config.CookieDictionary.TryGetValue(config.Cookie, out gameInfo);
}
LogParseConfig.ScriptGroupLogParseConfig? sgpc;
if (!config.ScriptGroupLogDictionary.TryGetValue(SelectedScriptGroup.Name, out sgpc))
{
sgpc = new LogParseConfig.ScriptGroupLogParseConfig();
}
// 创建 StackPanel
var stackPanel = new StackPanel
{
Orientation = Orientation.Vertical,
Margin = new Thickness(10)
};
// 创建 ComboBox
var rangeComboBox = new ComboBox
{
Width = 200,
Margin = new Thickness(0, 0, 0, 10),
VerticalAlignment = VerticalAlignment.Center
};
var rangeComboBoxItems = new List<object>
{
new { Text = "当前配置组", Value = "CurrentConfig" },
new { Text = "所有", Value = "All" }
};
rangeComboBox.DisplayMemberPath = "Text"; // 显示的文本
rangeComboBox.SelectedValuePath = "Value"; // 绑定的值
rangeComboBox.ItemsSource = rangeComboBoxItems;
rangeComboBox.SelectedIndex = 0; // 默认选中第一个项
stackPanel.Children.Add(rangeComboBox);
var dayRangeComboBox = new ComboBox
{
Width = 200,
Margin = new Thickness(0, 0, 0, 10),
VerticalAlignment = VerticalAlignment.Center
};
// 定义范围选项数据
var dayRangeComboBoxItems = new List<object>
{
new { Text = "1天" , Value = "1" },
new { Text = "3天", Value = "3" },
new { Text = "7天", Value = "7" },
new { Text = "15天", Value = "15" },
new { Text = "31天", Value = "31" },
new { Text = "61天", Value = "61" },
new { Text = "92天", Value = "92" },
new { Text = "所有", Value = "All" }
};
dayRangeComboBox.ItemsSource = dayRangeComboBoxItems;
dayRangeComboBox.DisplayMemberPath = "Text"; // 显示的文本
dayRangeComboBox.SelectedValuePath = "Value"; // 绑定的值
dayRangeComboBox.SelectedIndex = 0;
stackPanel.Children.Add(dayRangeComboBox);
CheckBox mergerStatsSwitch = new CheckBox
{
Content = "合并相邻同名配置组",
VerticalAlignment = VerticalAlignment.Center
};
stackPanel.Children.Add(mergerStatsSwitch);
// 开关控件ToggleButton 或 CheckBox
CheckBox faultStatsSwitch = new CheckBox
{
Content = "异常情况统计",
VerticalAlignment = VerticalAlignment.Center
};
stackPanel.Children.Add(faultStatsSwitch);
// 开关控件ToggleButton 或 CheckBox
CheckBox hoeingStatsSwitch = new CheckBox
{
Content = "统计锄地摩拉怪物数",
VerticalAlignment = VerticalAlignment.Center
};
CheckBox GenerateFarmingPlanData = new CheckBox
{
Content = "生成锄地规划数据",
VerticalAlignment = VerticalAlignment.Center
};
stackPanel.Children.Add(GenerateFarmingPlanData);
//firstRow.Children.Add(toggleSwitch);
// 将第一行添加到 StackPanel
stackPanel.Children.Add(hoeingStatsSwitch);
// 第二行:文本框和“?”按钮
StackPanel secondRow = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 0, 0, 10)
};
// 文本框
TextBox cookieTextBox = new TextBox
{
Width = 200,
Margin = new Thickness(0, 0, 10, 0)
};
secondRow.Children.Add(cookieTextBox);
// “?”按钮
Button questionButton = new Button
{
Content = "?",
Width = 30,
Height = 30
};
secondRow.Children.Add(questionButton);
StackPanel threeRow = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 0, 0, 10)
};
// 创建一个 TextBlock
TextBlock hoeingDelayBlock = new TextBlock
{
Text = "锄地延时(秒)",
VerticalAlignment = VerticalAlignment.Center,
FontSize = 16,
Margin = new Thickness(0, 0, 10, 0)
};
TextBox hoeingDelayTextBox = new TextBox
{
Width = 100,
FontSize = 16,
VerticalContentAlignment = VerticalAlignment.Center
};
threeRow.Children.Add(hoeingDelayBlock);
threeRow.Children.Add(hoeingDelayTextBox);
// 将第二行添加到 StackPanel
stackPanel.Children.Add(secondRow);
stackPanel.Children.Add(threeRow);
//PrimaryButtonText
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "日志分析",
Content = stackPanel,
CloseButtonText = "取消",
PrimaryButtonText = "确定",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
uiMessageBox.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(uiMessageBox);
void OnQuestionButtonOnClick(object sender, RoutedEventArgs args)
{
WebpageWindow cookieWin = new()
{
Title = "日志分析",
Width = 800,
Height = 600,
Owner = uiMessageBox,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
cookieWin.NavigateToHtml(TravelsDiaryDetailManager.generHtmlMessage());
cookieWin.Show();
}
questionButton.Click += OnQuestionButtonOnClick;
//对象赋值
rangeComboBox.SelectedValue = sgpc.RangeValue;
dayRangeComboBox.SelectedValue = sgpc.DayRangeValue;
cookieTextBox.Text = config.Cookie;
hoeingStatsSwitch.IsChecked = sgpc.HoeingStatsSwitch;
GenerateFarmingPlanData.IsChecked = sgpc.GenerateFarmingPlanData;
faultStatsSwitch.IsChecked = sgpc.FaultStatsSwitch;
mergerStatsSwitch.IsChecked = sgpc.MergerStatsSwitch;
hoeingDelayTextBox.Text = sgpc.HoeingDelay;
MessageBoxResult result = await uiMessageBox.ShowDialogAsync();
if (result == MessageBoxResult.Primary)
{
string rangeValue = ((dynamic)rangeComboBox.SelectedItem).Value;
string dayRangeValue = ((dynamic)dayRangeComboBox.SelectedItem).Value;
string cookieValue = cookieTextBox.Text;
//保存配置文件
sgpc.DayRangeValue = dayRangeValue;
sgpc.RangeValue = rangeValue;
sgpc.HoeingStatsSwitch = hoeingStatsSwitch.IsChecked ?? false;
sgpc.GenerateFarmingPlanData = GenerateFarmingPlanData.IsChecked ?? false;
sgpc.FaultStatsSwitch = faultStatsSwitch.IsChecked ?? false;
sgpc.MergerStatsSwitch = mergerStatsSwitch.IsChecked ?? false;
sgpc.HoeingDelay = hoeingDelayTextBox.Text;
config.Cookie = cookieValue;
config.ScriptGroupLogDictionary[SelectedScriptGroup.Name] = sgpc;
if (mcfg.LogSyncCookie && !string.IsNullOrEmpty(cookieValue))
{
mcfg.Cookie = cookieValue;
}
LogParse.WriteConfigFile(config);
WebpageWindow win = new()
{
Title = "日志分析",
Width = 800,
Height = 600,
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
void OnHtmlGenerationStatusChanged(string status)
{
Application.Current.Dispatcher.Invoke(() =>
{
Toast.Information(status, time: 5000);
});
}
LogParse.HtmlGenerationStatusChanged += OnHtmlGenerationStatusChanged;
Toast.Information("正在准备数据...");
List<(string FileName, string Date)> fs = LogParse.GetLogFiles(LogPath);
if (dayRangeValue != "All")
{
int n = int.Parse(dayRangeValue);
if (n < fs.Count)
{
fs = fs.GetRange(fs.Count - n, n);
}
}
//最终确定是否打开锄地开关
bool hoeingStats = false;
if ((hoeingStatsSwitch.IsChecked ?? false) && string.IsNullOrEmpty(cookieValue))
{
Toast.Warning("未填写cookie此次将不启用锄地统计");
}
//真正存储的gameinfo
GameInfo? realGameInfo = gameInfo;
//统计锄地开关打开并且不为cookie不为空
if ((hoeingStatsSwitch.IsChecked ?? false) && !string.IsNullOrEmpty(cookieValue))
{
try
{
Toast.Information("正在从米游社获取旅行札记数据,请耐心等待!");
gameInfo = await TravelsDiaryDetailManager.UpdateTravelsDiaryDetailManager(cookieValue);
Toast.Success($"米游社数据获取成功,开始进行解析,请耐心等待!");
}
catch (Exception)
{
if (realGameInfo != null)
{
Toast.Warning("访问米游社接口异常,此次将锄地统计将不更新最新数据!");
}
else
{
Toast.Warning("访问米游社接口异常,此次将不启用锄地统计!");
}
}
}
if (gameInfo != null)
{
realGameInfo = gameInfo;
config.CookieDictionary[cookieValue] = realGameInfo;
LogParse.WriteConfigFile(config);
}
if ((hoeingStatsSwitch.IsChecked ?? false) && realGameInfo != null)
{
hoeingStats = true;
}
var configGroupEntities = LogParse.ParseFile(fs);
if (rangeValue == "CurrentConfig")
{
//Toast.Success(_selectedScriptGroup.Name);
configGroupEntities = configGroupEntities.Where(item => SelectedScriptGroup.Name == item.Name).ToList();
}
if (configGroupEntities.Count == 0)
{
Toast.Warning("未解析出日志记录!");
LogParse.HtmlGenerationStatusChanged -= OnHtmlGenerationStatusChanged;
}
else
{
configGroupEntities.Reverse();
try
{
// 生成HTML并加载
win.NavigateToHtml(LogParse.GenerHtmlByConfigGroupEntity(configGroupEntities,
hoeingStats ? realGameInfo : null, sgpc));
win.ShowDialog();
// 取消订阅事件
LogParse.HtmlGenerationStatusChanged -= OnHtmlGenerationStatusChanged;
}
catch (Exception ex)
{
LogParse.HtmlGenerationStatusChanged -= OnHtmlGenerationStatusChanged;
Toast.Error($"生成日志分析时出错: {ex.Message}");
}
}
}
}
static string[] GetJsonFiles(string folderPath)
{
// 检查文件夹是否存在
if (!Directory.Exists(folderPath))
{
return new string[0];
}
// 获取所有 .json 文件
return Directory.GetFiles(folderPath, "*.json", SearchOption.TopDirectoryOnly);
}
[RelayCommand]
public void OnOpenLocalScriptRepo()
{
TaskContext.Instance().Config.ScriptConfig.ScriptRepoHintDotVisible = false;
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
}
[RelayCommand]
private void UpdateTasks()
{
List<ScriptGroupProject> projects = new();
List<ScriptGroupProject> oldProjects = new();
oldProjects.AddRange(SelectedScriptGroup?.Projects ?? []);
var oldcount = oldProjects.Count;
List<string> folderNames = new();
foreach (var project in oldProjects)
{
if (project.Type == "Pathing")
{
if (!folderNames.Contains(project.FolderName))
{
folderNames.Add(project.FolderName);
//根据文件夹更新
var dirPath = $@"{MapPathingViewModel.PathJsonPath}\{project.FolderName}";
foreach (var jsonFile in GetJsonFiles(dirPath))
{
var fileInfo = new FileInfo(jsonFile);
var oldProject = oldProjects.FirstOrDefault(item => item.Name == fileInfo.Name);
if (oldProject == null)
{
projects.Add(ScriptGroupProject.BuildPathingProject(fileInfo.Name, project.FolderName));
}
else
{
projects.Add(oldProject);
}
}
}
}
else
{
projects.Add(project);
}
}
SelectedScriptGroup?.Projects.Clear();
foreach (var scriptGroupProject in projects)
{
SelectedScriptGroup?.AddProject(scriptGroupProject);
}
Toast.Success($"增加了{projects.Count - oldcount}个地图追踪任务");
if (SelectedScriptGroup != null) WriteScriptGroup(SelectedScriptGroup);
}
[RelayCommand]
private void ReverseTaskOrder()
{
List<ScriptGroupProject> projects = new();
projects.AddRange(SelectedScriptGroup?.Projects.Reverse() ?? []);
SelectedScriptGroup?.Projects.Clear();
projects.ForEach(item => SelectedScriptGroup?.Projects.Add(item));
if (SelectedScriptGroup != null) WriteScriptGroup(SelectedScriptGroup);
}
[RelayCommand]
private void ExportMergerJsons()
{
int count = 0;
var pathDir = Path.Combine(LogPath, "exportMergerJson", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), "AutoPathing");
foreach (var scriptGroupProject in SelectedScriptGroup?.Projects ?? [])
{
if (scriptGroupProject.Type == "Pathing")
{
var mergerJson = JsonMerger.getMergePathingJson(Path.Combine(MapPathingViewModel.PathJsonPath,
scriptGroupProject.FolderName, scriptGroupProject.Name));
string fullPath = Path.Combine(pathDir, scriptGroupProject.FolderName, scriptGroupProject.Name);
string dir = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(fullPath, mergerJson);
count++;
}
}
if (count > 0)
{
Process.Start("explorer.exe", pathDir);
}
}
[RelayCommand]
public void AddScriptGroupNextFlag(ScriptGroup? item)
{
foreach (var scriptGroup in ScriptGroups)
{
scriptGroup.NextFlag = false;
}
if (item != null)
{
item.NextFlag = true;
TaskContext.Instance().Config.NextScriptGroupName = item.Name;
}
}
[RelayCommand]
public void OnCopyScriptGroup(ScriptGroup? item)
{
if (item == null)
{
return;
}
var textBox = new System.Windows.Controls.TextBox()
{
VerticalAlignment = VerticalAlignment.Top
};
textBox.Loaded += (sender, e) =>
{
textBox.Focus();
textBox.SelectAll();
};
var str = PromptDialog.Prompt("请输入配置组名称", "复制配置组", textBox, item.Name);
if (!string.IsNullOrEmpty(str))
{
// 检查是否已存在
if (ScriptGroups.Any(x => x.Name == str))
{
_snackbarService.Show(
"配置组已存在",
$"配置组 {str} 已经存在,复制失败",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
}
else
{
var newScriptGroup = JsonSerializer.Deserialize<ScriptGroup>(JsonSerializer.Serialize(item));
if (newScriptGroup != null)
{
newScriptGroup.Name = str;
ScriptGroups.Add(newScriptGroup);
}
//WriteScriptGroup(newScriptGroup);
}
}
}
[RelayCommand]
public void OnRenameScriptGroup(ScriptGroup? item)
{
if (item == null)
{
return;
}
var textBox = new System.Windows.Controls.TextBox()
{
VerticalAlignment = VerticalAlignment.Top
};
textBox.Loaded += (sender, e) =>
{
textBox.Focus();
textBox.SelectAll();
};
var str = PromptDialog.Prompt("请输入配置组名称", "重命名配置组", textBox, item.Name);
if (!string.IsNullOrEmpty(str))
{
if (item.Name == str)
{
return;
}
// 检查是否已存在
if (ScriptGroups.Any(x => x.Name == str))
{
_snackbarService.Show(
"配置组已存在",
$"配置组 {str} 已经存在,重命名失败",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
}
else
{
File.Move(Path.Combine(ScriptGroupPath, $"{item.Name}.json"), Path.Combine(ScriptGroupPath, $"{str}.json"));
item.Name = str;
if (item.NextFlag)
{
TaskContext.Instance().Config.NextScriptGroupName = item.Name;
}
WriteScriptGroup(item);
}
}
}
[RelayCommand]
public void OnDeleteScriptGroup(ScriptGroup? item)
{
if (item == null)
{
return;
}
try
{
ScriptGroups.Remove(item);
File.Delete(Path.Combine(ScriptGroupPath, $"{item.Name}.json"));
_snackbarService.Show(
"配置组删除成功",
$"配置组 {item.Name} 已经被删除",
ControlAppearance.Success,
null,
TimeSpan.FromSeconds(2)
);
}
catch (Exception e)
{
_logger.LogDebug(e, "删除配置组配置时失败");
_snackbarService.Show(
"删除配置组配置失败",
$"配置组 {item.Name} 删除失败!",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
[RelayCommand]
private void OnAddJsScript()
{
var list = LoadAllJsScriptProjects();
var stackPanel = CreateJsScriptSelectionPanel(list, typeof(CheckBox));
var result = PromptDialog.Prompt("请选择需要添加的JS脚本", "请选择需要添加的JS脚本", stackPanel, new Size(500, 600));
if (!string.IsNullOrEmpty(result))
{
AddSelectedJsScripts((StackPanel)stackPanel.Content);
}
}
internal static ScrollViewer CreateJsScriptSelectionPanel(List<ScriptProject> list, Type selectType)
{
var stackPanel = new StackPanel();
var filterTextBox = new TextBox
{
Margin = new Thickness(0, 0, 0, 10),
PlaceholderText = "输入搜索条件...",
};
filterTextBox.TextChanged += delegate { ApplyJsScriptFilter(stackPanel, list, filterTextBox.Text, selectType); };
stackPanel.Children.Add(filterTextBox);
AddJsScriptsToPanel(stackPanel, list, filterTextBox.Text, selectType);
var scrollViewer = new ScrollViewer
{
Content = stackPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
//Height = 435 // 固定高度
};
return scrollViewer;
}
private static void ApplyJsScriptFilter(StackPanel parentPanel, List<ScriptProject> scripts, string filter, Type selectType)
{
if (parentPanel.Children.Count > 0)
{
List<UIElement> removeElements = new List<UIElement>();
foreach (UIElement parentPanelChild in parentPanel.Children)
{
if (parentPanelChild is FrameworkElement frameworkElement && frameworkElement.Name.StartsWith("dynamic_"))
{
removeElements.Add(frameworkElement);
}
}
removeElements.ForEach(parentPanel.Children.Remove);
}
AddJsScriptsToPanel(parentPanel, scripts, filter, selectType);
}
private static void AddJsScriptsToPanel(StackPanel parentPanel, List<ScriptProject> scripts, string filter, Type selectType)
{
foreach (var script in scripts)
{
var displayText = script.FolderName + " - " + script.Manifest.Name;
if (!string.IsNullOrEmpty(filter) &&
!displayText.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
!script.FolderName.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
!script.Manifest.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (selectType == typeof(CheckBox))
{
var checkBox = new CheckBox
{
Content = displayText,
Tag = script.FolderName,
Margin = new Thickness(0, 2, 0, 2),
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_")
};
parentPanel.Children.Add(checkBox);
}
else if (selectType == typeof(RadioButton))
{
var radioButton = new RadioButton
{
Content = displayText,
Tag = script.FolderName,
Margin = new Thickness(0, 2, 0, 2),
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_"),
GroupName = "JsScriptsRadioButtonGroup"
};
parentPanel.Children.Add(radioButton);
}
else
{
throw new ArgumentOutOfRangeException();
}
}
}
private void AddSelectedJsScripts(StackPanel stackPanel)
{
foreach (var child in stackPanel.Children)
{
if (child is CheckBox { IsChecked: true } checkBox && checkBox.Tag is string folderName)
{
SelectedScriptGroup?.AddProject(new ScriptGroupProject(new ScriptProject(folderName)));
}
}
}
[RelayCommand]
private void OnAddKmScript()
{
var list = LoadAllKmScripts();
var combobox = new ComboBox
{
VerticalAlignment = VerticalAlignment.Top
};
foreach (var fileInfo in list)
{
combobox.Items.Add(fileInfo.Name);
}
var str = PromptDialog.Prompt("请选择需要添加的键鼠脚本", "请选择需要添加的键鼠脚本", combobox);
if (!string.IsNullOrEmpty(str))
{
SelectedScriptGroup?.AddProject(ScriptGroupProject.BuildKeyMouseProject(str));
}
}
[RelayCommand]
private void OnAddShell()
{
var str = PromptDialog.Prompt("执行 shell 操作存在极大风险!请勿输入你看不懂的指令!以免引发安全隐患并损坏系统!\n执行 shell 的时候,游戏可能会失去焦点", "请输入需要执行的shell");
if (!string.IsNullOrEmpty(str))
{
SelectedScriptGroup?.AddProject(ScriptGroupProject.BuildShellProject(str));
}
}
[RelayCommand]
private async Task OnAddPathing()
{
try
{
// 在后台线程中加载数据
var root = await Task.Run(() => FileTreeNodeHelper.LoadDirectory<PathingTask>(MapPathingViewModel.PathJsonPath));
// 异步创建选择面板
var stackPanel = await CreatePathingScriptSelectionPanelAsync(root.Children);
// 显示选择对话框
var result = PromptDialog.Prompt("请选择需要添加的地图追踪任务", "请选择需要添加的地图追踪任务", stackPanel, new Size(600, 720));
if (!string.IsNullOrEmpty(result))
{
AddSelectedPathingScripts((StackPanel)stackPanel.Content);
}
}
catch (Exception ex)
{
Toast.Error($"加载地图追踪任务失败: {ex.Message}");
_logger.LogError(ex, "加载地图追踪任务时发生错误");
}
}
// 添加防抖计时器字段
private DispatcherTimer? _debounceTimer;
private const int DebounceDelayMs = 300;
// 存储路径与UI元素的映射
private readonly Dictionary<string, FrameworkElement> _nodeUIElements = [];
/// <summary>
/// 异步创建地图追踪任务选择面板
/// </summary>
private async Task<ScrollViewer> CreatePathingScriptSelectionPanelAsync(IEnumerable<FileTreeNode<PathingTask>> list)
{
var stackPanel = new StackPanel();
CheckBox excludeCheckBox = new()
{
Content = "排除已选择过的目录",
VerticalAlignment = VerticalAlignment.Center,
};
CheckBox deepCheckBox = new()
{
Content = "深度搜索",
VerticalAlignment = VerticalAlignment.Center,
};
TextBox filterTextBox = new()
{
Margin = new Thickness(0, 0, 0, 10),
PlaceholderText = "输入筛选条件...",
};
// 初始化防抖计时器
_debounceTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(DebounceDelayMs)
};
excludeCheckBox.Click += delegate
{
_ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked);
};
deepCheckBox.Click += delegate
{
_ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked);
};
filterTextBox.TextChanged += delegate
{
_debounceTimer.Stop();
// 设置计时器回调
_debounceTimer.Tick -= OnDebounceTimerTick;
_debounceTimer.Tick += OnDebounceTimerTick;
_debounceTimer.Start();
void OnDebounceTimerTick(object? sender, EventArgs e)
{
_debounceTimer.Stop();
_debounceTimer.Tick -= OnDebounceTimerTick;
_ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked);
}
};
stackPanel.Children.Add(excludeCheckBox);
stackPanel.Children.Add(deepCheckBox);
stackPanel.Children.Add(filterTextBox);
// 异步构建UI树
await BuildCompleteUITreeAsync(stackPanel, list, 0);
filterTextBox.Focus();
var scrollViewer = new ScrollViewer
{
Content = stackPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
};
return scrollViewer;
}
/// <summary>
/// 异步构建完整的UI树
/// </summary>
/// <param name="parentPanel">构建内容的父容器</param>
/// <param name="nodes">要构建的节点集合</param>
/// <param name="depth">构建的深度</param>
private async Task BuildCompleteUITreeAsync(StackPanel parentPanel, IEnumerable<FileTreeNode<PathingTask>>? nodes, int depth)
{
if (nodes == null)
return;
var nodeList = nodes.ToList();
for (int i = 0; i < nodeList.Count; i += 1)
{
var batch = nodeList.Skip(i).Take(1);
// 在UI线程中创建UI元素并添加到面板
await Application.Current.Dispatcher.InvokeAsync(async () =>
{
foreach (var node in batch)
{
var element = CreateUIElementForNode(node, depth);
parentPanel.Children.Add(element);
// 如果是目录且有子节点,递归构建子节点
if (node.IsDirectory && node.Children?.Any() == true && element is Expander expander)
{
if (expander.Content is StackPanel childPanel)
{
await BuildCompleteUITreeAsync(childPanel, node.Children, depth + 1);
}
}
}
}, DispatcherPriority.Background);
// 让出控制权避免长时间阻塞UI线程
await Task.Delay(1);
}
}
/// <summary>
/// 为单个节点创建UI元素
/// </summary>
/// <param name="node">文件树节点</param>
/// <param name="depth">节点深度</param>
/// <returns>创建的UI元素</returns>
private FrameworkElement CreateUIElementForNode(FileTreeNode<PathingTask> node, int depth)
{
var checkBox = new CheckBox
{
Content = node.FileName,
Tag = node.FilePath,
Margin = new Thickness(depth * 30, 0, 0, 0),
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_")
};
// 存储路径与UI元素的映射
if (!string.IsNullOrEmpty(node.FilePath))
_nodeUIElements[node.FilePath] = checkBox;
if (node.IsDirectory)
{
// 如果父节点没有任何子内容,则不可勾选
if (node.Children == null || node.Children.Count == 0)
checkBox.IsEnabled = false;
var childPanel = new StackPanel();
checkBox.IsThreeState = true;
var expander = new Expander
{
Header = checkBox,
Content = childPanel,
IsExpanded = false,
Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_"),
Visibility = Visibility.Visible
};
// 存储路径与UI元素的映射
if (!string.IsNullOrEmpty(node.FilePath))
{
_nodeUIElements[node.FilePath + "_expander"] = expander;
}
// 修改事件处理:用户点击时只在全选和全不选之间切换
checkBox.Click += (s, e) => HandleDirectoryCheckBoxClick(checkBox, childPanel);
return expander;
}
else
{
// 为文件复选框添加状态改变事件,用于更新父级状态
checkBox.Checked += (s, e) => UpdateParentCheckBoxState(checkBox);
checkBox.Unchecked += (s, e) => UpdateParentCheckBoxState(checkBox);
return checkBox;
}
}
/// <summary>
/// 异步应用筛选到已存在的节点
/// <param name="nodes">要筛选的节点集合</param>
/// <param name="filter">用户输入的筛选关键词</param>
/// <param name="excludeSelectedFolder">排除选择的目录</param>
/// <param name="isDeepSearch">深度搜索功能</param>
/// </summary>
private async Task ApplyFilterToExistingNodesAsync(IEnumerable<FileTreeNode<PathingTask>> nodes, string filter, bool? excludeSelectedFolder = false, bool? isDeepSearch = false)
{
var filteredResult = await Task.Run(() =>
{
IEnumerable<FileTreeNode<PathingTask>> filteredNodes = nodes;
// 如果启用排除已选择过的目录,先过滤掉这些目录
if (excludeSelectedFolder ?? false)
{
List<string> skipFolderNames = SelectedScriptGroup?.Projects.ToList().Select(item => item.FolderName).Distinct().ToList() ?? [];
string jsonString = JsonSerializer.Serialize(nodes);
var copiedNodes = JsonSerializer.Deserialize<ObservableCollection<FileTreeNode<PathingTask>>>(jsonString);
if (copiedNodes != null)
{
copiedNodes = FileTreeNodeHelper.FilterTree(copiedNodes, skipFolderNames);
copiedNodes = FileTreeNodeHelper.FilterEmptyNodes(copiedNodes);
filteredNodes = copiedNodes;
}
}
return filteredNodes;
});
// 在UI线程中更新可见性
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// 重置所有节点的可见性
foreach (var element in _nodeUIElements.Values)
element.Visibility = Visibility.Collapsed;
UpdateNodesVisibility(filteredResult, filter, isDeepSearch);
}, DispatcherPriority.Background);
}
/// <summary>
/// 更新节点的可见性和展开状态
/// </summary>
/// <param name="nodes">要处理的文件树节点集合</param>
/// <param name="filter">用户输入的筛选关键词</param>
/// <param name="isDeepSearch">是否启用深度搜索</param>
/// <param name="depth">当前节点在树中的深度级别</param>
/// <param name="parentMatched">当前节点的父级是否已经匹配筛选条件</param>
/// <param name="returnMatchStatus">是否返回匹配状态(用于子节点处理)</param>
/// <returns>returnMatchStatus则返回是否包含匹配的节点</returns>
private bool UpdateNodesVisibility(IEnumerable<FileTreeNode<PathingTask>> nodes, string filter, bool? isDeepSearch, int depth = 0, bool parentMatched = false, bool returnMatchStatus = false)
{
bool containsMatch = false;
foreach (var node in nodes)
{
if (string.IsNullOrEmpty(node.FilePath))
continue;
bool nodeMatches = !string.IsNullOrEmpty(filter) && IsNodeMatched(node, filter);
bool shouldShow = ShouldShowNode(node, filter, isDeepSearch, depth, parentMatched);
// 更新节点可见性
if (_nodeUIElements.TryGetValue(node.FilePath, out var element))
{
element.Visibility = shouldShow ? Visibility.Visible : Visibility.Collapsed;
if (shouldShow && nodeMatches && returnMatchStatus)
containsMatch = true;
}
// 如果是目录节点,递归处理子节点并更新展开状态
if (node.IsDirectory && _nodeUIElements.TryGetValue(node.FilePath + "_expander", out var expanderElement) && expanderElement is Expander expander)
{
if (shouldShow)
{
// 递归处理子节点传入returnMatchStatus = true来获取子节点匹配状态
bool childContainsMatch = UpdateNodesVisibility(node.Children, filter, isDeepSearch, depth + 1, nodeMatches || parentMatched, true);
// 如果子节点包含匹配且需要返回匹配状态,当前层级也标记为包含匹配
if (childContainsMatch && returnMatchStatus)
containsMatch = true;
expander.IsExpanded = ShouldExpandNode(filter, nodeMatches, parentMatched, childContainsMatch, depth, isDeepSearch, GetParentFolderName(node));
expander.Visibility = Visibility.Visible;
}
else
{
expander.Visibility = Visibility.Collapsed;
}
}
}
return containsMatch;
}
/// <summary>
/// 该节点是否应该显示
/// </summary>
/// <param name="node">要检查的节点</param>
/// <param name="filter">筛选条件</param>
/// <param name="isDeepSearch">是否启用深度搜索</param>
/// <param name="currentDepth">当前深度</param>
/// <param name="parentMatched">父节点是否已匹配</param>
/// <returns>是否应该显示该节点</returns>
private static bool ShouldShowNode(FileTreeNode<PathingTask> node, string filter, bool? isDeepSearch = false, int currentDepth = 0, bool parentMatched = false)
{
// 如果没有筛选条件,显示所有节点
if (string.IsNullOrEmpty(filter))
return true;
// 如果该节点任意层级父节点已匹配,则忽略深度限制显示其全部子内容
if (parentMatched)
return true;
bool currentNodeMatches = IsNodeMatched(node, filter);
// 如果该节点匹配,显示该节点
if (currentNodeMatches)
return true;
// 不超过允许深度的前提下,递归目录节点,逐一判断其所有子节点是否应该显示
if (currentDepth >= GetMaxDepth(isDeepSearch, GetParentFolderName(node)))
return false;
if (node.IsDirectory && node.Children?.Any() == true)
{
foreach (var child in node.Children)
{
// 递归时,传递当前节点的匹配状态,任意当前深度的节点应该显示,则当前节点也应该显示
if (ShouldShowNode(child, filter, isDeepSearch, currentDepth + 1, currentNodeMatches))
return true;
}
}
return false;
}
/// <summary>
/// 该节点是否匹配
/// </summary>
/// <param name="node">要检查的节点</param>
/// <param name="filter">筛选条件</param>
/// <returns>是否匹配</returns>
private static bool IsNodeMatched(FileTreeNode<PathingTask> node, string filter)
{
// 该节点名称是否匹配
if (node.FileName?.Contains(filter, StringComparison.OrdinalIgnoreCase) == true)
return true;
// 往前追溯,该节点路径中是否至少有一段匹配
if (!string.IsNullOrEmpty(node.FilePath))
{
var relativePath = Path.GetRelativePath(MapPathingViewModel.PathJsonPath, node.FilePath);
var pathSegments = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
// 处理路径段匹配,对于文件名需要去除扩展名
foreach (var segment in pathSegments)
{
// 如果这是最后一个段且不是目录,则去除扩展名后匹配
var segmentToMatch = segment;
if (segment == pathSegments.Last() && !node.IsDirectory)
{
segmentToMatch = Path.GetFileNameWithoutExtension(segment);
}
if (segmentToMatch.Contains(filter, StringComparison.OrdinalIgnoreCase))
return true;
}
}
return false;
}
/// <summary>
/// 该节点是否应该自动展开
/// </summary>
/// <param name="filter">筛选条件</param>
/// <param name="currentNodeMatches">当前节点是否匹配</param>
/// <param name="parentMatched">父节点是否已匹配</param>
/// <param name="childContainsMatch">子树是否包含匹配</param>
/// <param name="depth">当前深度</param>
/// <param name="isDeepSearch">是否启用深度搜索</param>
/// <param name="parentFolderName">父文件夹名称</param>
/// <returns>是否应该展开</returns>
private static bool ShouldExpandNode(string filter, bool currentNodeMatches, bool parentMatched, bool childContainsMatch, int depth, bool? isDeepSearch, string? parentFolderName)
{
// 如果没有筛选条件(输入框为空),所有节点都不展开
if (string.IsNullOrEmpty(filter))
return false;
// 如果该节点的父节点已匹配,子目录不再展开,便于浏览
if (parentMatched)
return false;
// 该节点的深度大于等于深度限制时,不再展开
if (depth >= GetMaxDepth(isDeepSearch, parentFolderName))
return false;
// 该节点名称直接匹配,自动展开
if (!string.IsNullOrEmpty(filter) && currentNodeMatches)
return true;
// 该节点的子树中存在至少一个匹配的节点,自动展开该节点以显示深层匹配节点
if (childContainsMatch)
return true;
return false;
}
/// <summary>
/// 获取该节点的父文件夹名称
/// </summary>
/// <param name="node">节点</param>
/// <returns>父文件夹名称</returns>
private static string? GetParentFolderName(FileTreeNode<PathingTask> node)
{
// 如果节点没有文件路径,返回 null
if (string.IsNullOrEmpty(node.FilePath))
return null;
// 获取相对于 PathJsonPath 的路径
var relativePath = Path.GetRelativePath(MapPathingViewModel.PathJsonPath, node.FilePath);
var pathSegments = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
// 返回第一级目录名称
return pathSegments.Length > 0 ? pathSegments[0] : null;
}
/// <summary>
/// 获取允许的最大深度
/// </summary>
/// <param name="isDeepSearch">是否启用深度搜索</param>
/// <param name="parentFolderName">父文件夹文件名内容</param>
/// <param name="currentNodeMatched">当前节点是否匹配</param>
/// <param name="parentMatched">父节点是否已匹配</param>
/// <returns>允许的深度</returns>
private static int GetMaxDepth(bool? isDeepSearch, string? parentFolderName, bool currentNodeMatched = false, bool parentMatched = false)
{
// 如果开启深度搜索,允许全部子内容
if (isDeepSearch == true)
return int.MaxValue;
// 如果当前节点匹配或父节点已匹配,允许全部子内容
if (currentNodeMatched || parentMatched)
return int.MaxValue;
int defaultDepth = 1;
// 特殊目录的深度扩展
if (parentFolderName == "地方特产")
return defaultDepth + 1;
return defaultDepth;
}
/// <summary>
/// 处理目录复选框点击事件
/// </summary>
/// <param name="checkBox">被点击的目录复选框</param>
/// <param name="childPanel">子面板</param>
private void HandleDirectoryCheckBoxClick(CheckBox checkBox, StackPanel childPanel)
{
var childCheckBoxes = GetAllChildCheckBoxes(childPanel);
// 判断目标状态:如果所有子项都已选中,则全不选;否则全选
bool allChildrenChecked = childCheckBoxes.Count > 0 && childCheckBoxes.All(cb => cb.IsChecked == true);
bool targetState = !allChildrenChecked;
checkBox.IsChecked = targetState;
SetChildCheckBoxesState(childPanel, targetState);
UpdateParentCheckBoxState(checkBox);
}
/// <summary>
/// 递归获取面板中所有的子复选框
/// </summary>
/// <param name="panel">获取的面板</param>
/// <returns>所有子复选框列表</returns>
private List<CheckBox> GetAllChildCheckBoxes(StackPanel panel)
{
var checkBoxes = new List<CheckBox>();
foreach (var child in panel.Children)
{
if (child is CheckBox checkBox)
{
checkBoxes.Add(checkBox);
}
else if (child is Expander expander)
{
if (expander.Header is CheckBox headerCheckBox)
{
checkBoxes.Add(headerCheckBox);
}
if (expander.Content is StackPanel nestedPanel)
{
checkBoxes.AddRange(GetAllChildCheckBoxes(nestedPanel));
}
}
}
return checkBoxes;
}
/// <summary>
/// 更新父级复选框的三态状态
/// </summary>
/// <param name="changedCheckBox">状态发生改变的复选框</param>
private void UpdateParentCheckBoxState(CheckBox changedCheckBox)
{
// 查找父级复选框
var parentCheckBox = FindParentCheckBox(changedCheckBox);
if (parentCheckBox == null)
return;
// 获取同级所有复选框
var siblingCheckBoxes = GetSiblingCheckBoxes(changedCheckBox);
// 计算状态
int checkedCount = siblingCheckBoxes.Count(cb => cb.IsChecked == true);
int uncheckedCount = siblingCheckBoxes.Count(cb => cb.IsChecked == false);
int indeterminateCount = siblingCheckBoxes.Count(cb => cb.IsChecked == null);
// 设置父级复选框状态
if (checkedCount == siblingCheckBoxes.Count)
parentCheckBox.IsChecked = true;
else if (uncheckedCount == siblingCheckBoxes.Count)
parentCheckBox.IsChecked = false;
else
parentCheckBox.IsChecked = null;
// 递归更新上级父级
UpdateParentCheckBoxState(parentCheckBox);
}
/// <summary>
/// 查找指定复选框的父级复选框
/// </summary>
/// <param name="checkBox">当前复选框</param>
/// <returns>父级复选框如果没有则返回null</returns>
private CheckBox? FindParentCheckBox(CheckBox checkBox)
{
var filePath = checkBox.Tag as string;
if (string.IsNullOrEmpty(filePath))
return null;
// 获取父目录路径
var parentPath = Path.GetDirectoryName(filePath);
if (string.IsNullOrEmpty(parentPath))
return null;
// 查找父级复选框
if (_nodeUIElements.TryGetValue(parentPath, out var parentElement) && parentElement is CheckBox parentCheckBox)
{
return parentCheckBox;
}
return null;
}
/// <summary>
/// 获取同级的所有复选框
/// </summary>
/// <param name="checkBox">当前复选框</param>
/// <returns>同级复选框列表</returns>
private static List<CheckBox> GetSiblingCheckBoxes(CheckBox checkBox)
{
// 先尝试获取逻辑父级
var parent = LogicalTreeHelper.GetParent(checkBox) as FrameworkElement;
// 如果逻辑父级不存在,尝试可视化父级
parent ??= VisualTreeHelper.GetParent(checkBox) as FrameworkElement;
// 如果当前元素是Expander的Header获取Expander的父级
if (parent is Expander expander)
{
parent = LogicalTreeHelper.GetParent(expander) as FrameworkElement ??
VisualTreeHelper.GetParent(expander) as FrameworkElement;
}
// 遍历同级元素
var siblings = new List<CheckBox>();
if (parent is StackPanel stackPanel)
{
foreach (var child in stackPanel.Children)
{
if (child is CheckBox siblingCheckBox)
siblings.Add(siblingCheckBox);
else if (child is Expander childExpander && childExpander.Header is CheckBox expanderCheckBox)
siblings.Add(expanderCheckBox);
}
}
return siblings;
}
/// <summary>
/// 递归设置子复选框状态
/// </summary>
/// <param name="childStackPanel">子面板</param>
/// <param name="state">目标状态</param>
private static void SetChildCheckBoxesState(StackPanel childStackPanel, bool state)
{
foreach (var child in childStackPanel.Children)
{
if (child is CheckBox checkBox)
{
checkBox.IsChecked = state;
}
else if (child is Expander expander && expander.Content is StackPanel nestedStackPanel)
{
if (expander.Header is CheckBox headerCheckBox)
{
headerCheckBox.IsChecked = state;
}
SetChildCheckBoxesState(nestedStackPanel, state);
}
}
}
private void AddSelectedPathingScripts(StackPanel stackPanel)
{
foreach (var child in stackPanel.Children)
{
if (child is CheckBox { IsChecked: true } checkBox && checkBox.Tag is string filePath)
{
var fileInfo = new FileInfo(filePath);
if (!fileInfo.Attributes.HasFlag(FileAttributes.Directory))
{
var relativePath = Path.GetRelativePath(MapPathingViewModel.PathJsonPath, fileInfo.Directory!.FullName);
SelectedScriptGroup?.AddProject(ScriptGroupProject.BuildPathingProject(fileInfo.Name, relativePath));
}
}
else if (child is Expander { Content: StackPanel nestedStackPanel })
{
AddSelectedPathingScripts(nestedStackPanel);
}
}
}
// private Dictionary<string, List<FileInfo>> LoadAllPathingScripts()
// {
// var folder = Global.Absolute(@"User\AutoPathing");
// var directories = Directory.GetDirectories(folder);
// var result = new Dictionary<string, List<FileInfo>>();
//
// foreach (var directory in directories)
// {
// var dirInfo = new DirectoryInfo(directory);
// var files = dirInfo.GetFiles("*.*", SearchOption.TopDirectoryOnly).ToList();
// result.Add(dirInfo.Name, files);
// }
//
// return result;
// }
internal static List<ScriptProject> LoadAllJsScriptProjects()
{
var path = Global.ScriptPath();
Directory.CreateDirectory(path);
// 获取所有脚本项目
var projects = Directory.GetDirectories(path)
.Select(x =>
{
try
{
return new ScriptProject(Path.GetFileName(x));
}
catch (Exception e)
{
Toast.Warning($"加载单个脚本失败:{e.Message}");
return null;
}
})
.Where(x => x != null)
.ToList();
return projects;
}
private List<FileInfo> LoadAllKmScripts()
{
var folder = Global.Absolute(@"User\KeyMouseScript");
Directory.CreateDirectory(folder);
// 获取所有脚本项目
var files = Directory.GetFiles(folder, "*.*",
SearchOption.AllDirectories);
return files.Select(file => new FileInfo(file)).ToList();
}
[RelayCommand]
public void OnEditScriptCommon(ScriptGroupProject? item)
{
if (item == null)
{
return;
}
ShowEditWindow(item);
// foreach (var group in ScriptGroups)
// {
// WriteScriptGroup(group);
// }
}
[RelayCommand]
private void AddNextFlag(ScriptGroupProject? item)
{
if (item == null || SelectedScriptGroup == null)
{
return;
}
List<ValueTuple<string, int, string, string>> nextScheduledTask = TaskContext.Instance().Config.NextScheduledTask;
var nst = nextScheduledTask.Find(item2 => item2.Item1 == SelectedScriptGroup?.Name);
if (nst != default)
{
nextScheduledTask.Remove(nst);
}
nextScheduledTask.Add((SelectedScriptGroup?.Name ?? "", item.Index, item.FolderName, item.Name));
foreach (var item1 in SelectedScriptGroup?.Projects ?? [])
{
item1.NextFlag = false;
}
item.NextFlag = true;
}
public static void ShowEditWindow(ScriptGroupProject project)
{
var viewModel = new ScriptGroupProjectEditorViewModel(project);
var editor = new ScriptGroupProjectEditor(project)
{
DataContext = viewModel
};
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "修改通用设置",
Content = editor,
CloseButtonText = "关闭",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
uiMessageBox.ShowDialogAsync();
}
[RelayCommand]
public void OnEditJsScriptSettings(ScriptGroupProject? item)
{
if (item == null)
{
return;
}
if (item.Project == null)
{
item.BuildScriptProjectRelation();
}
if (item.Project == null)
{
return;
}
if (item.Type == "Javascript")
{
if (item.JsScriptSettingsObject == null)
{
item.JsScriptSettingsObject = new ExpandoObject();
}
var ui = item.Project.LoadSettingUi(item.JsScriptSettingsObject);
if (ui == null)
{
Toast.Warning("此脚本未提供自定义配置");
return;
}
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "修改JS脚本自定义设置 ",
Content = ui,
CloseButtonText = "关闭",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
uiMessageBox.ShowDialogAsync();
// 由于 JsScriptSettingsObject 的存在,这里只能手动再次保存配置
foreach (var group in ScriptGroups)
{
WriteScriptGroup(group);
}
}
else
{
Toast.Warning("只有JS脚本才有自定义配置");
}
}
[RelayCommand]
public async void OnDeleteScriptByFolder(ScriptGroupProject? item)
{
if (item == null)
{
return;
}
if (SelectedScriptGroup != null)
{
var toBeDeletedProjects = SelectedScriptGroup.Projects
.Where(item2 => item2.FolderName == item.FolderName)
.ToList();
foreach (var project in toBeDeletedProjects)
{
SelectedScriptGroup.Projects.Remove(project);
}
_snackbarService.Show(
"脚本配置移除成功",
$"已移除 {item.FolderName} 下的所有关联配置",
ControlAppearance.Success,
null,
TimeSpan.FromSeconds(2)
);
}
}
[RelayCommand]
public void OnDeleteScript(ScriptGroupProject? item)
{
if (item == null)
{
return;
}
SelectedScriptGroup?.Projects.Remove(item);
_snackbarService.Show(
"脚本配置移除成功",
$"{item.Name} 的关联配置已经移除",
ControlAppearance.Success,
null,
TimeSpan.FromSeconds(2)
);
}
[RelayCommand]
public void OnOpenScriptFolder(ScriptGroupProject? item)
{
if (item == null)
{
return;
}
try
{
string? path = null;
switch (item.Type)
{
case "Javascript":
path = Path.Combine(Global.ScriptPath(), item.FolderName);
break;
case "KeyMouse":
path = Global.Absolute(@"User\KeyMouseScript");
break;
case "Pathing":
path = Path.Combine(MapPathingViewModel.PathJsonPath, item.FolderName);
break;
}
if (path != null && Directory.Exists(path))
{
Process.Start("explorer.exe", path);
}
else
{
_snackbarService.Show("打开失败", "目录不存在", ControlAppearance.Caution, null, TimeSpan.FromSeconds(2));
}
}
catch (Exception e)
{
_logger.LogDebug(e, "打开脚本目录失败");
_snackbarService.Show("打开失败", e.Message, ControlAppearance.Danger, null, TimeSpan.FromSeconds(3));
}
}
private void ScriptGroupsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (ScriptGroup newItem in e.NewItems)
{
newItem.Projects.CollectionChanged += ScriptProjectsCollectionChanged;
foreach (var project in newItem.Projects)
{
project.PropertyChanged += ScriptProjectsPChanged;
}
}
}
if (e.OldItems != null)
{
foreach (ScriptGroup oldItem in e.OldItems)
{
foreach (var project in oldItem.Projects)
{
project.PropertyChanged -= ScriptProjectsPChanged;
}
oldItem.Projects.CollectionChanged -= ScriptProjectsCollectionChanged;
}
}
// 补充排序字段
var i = 1;
foreach (var group in ScriptGroups)
{
group.Index = i++;
}
// 保存配置组配置
foreach (var group in ScriptGroups)
{
WriteScriptGroup(group);
}
}
private void ScriptProjectsPChanged(object? sender, PropertyChangedEventArgs e)
{
foreach (var group in ScriptGroups)
{
WriteScriptGroup(group);
}
}
private void ScriptProjectsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// 补充排序字段
if (SelectedScriptGroup is { Projects.Count: > 0 })
{
var i = 1;
foreach (var project in SelectedScriptGroup.Projects)
{
project.Index = i++;
}
}
// 保存配置组配置
if (SelectedScriptGroup != null)
{
WriteScriptGroup(SelectedScriptGroup);
}
}
private void WriteScriptGroup(ScriptGroup scriptGroup)
{
try
{
if (!Directory.Exists(ScriptGroupPath))
{
Directory.CreateDirectory(ScriptGroupPath);
}
var file = Path.Combine(ScriptGroupPath, $"{scriptGroup.Name}.json");
File.WriteAllText(file, scriptGroup.ToJson());
}
catch (Exception e)
{
_logger.LogDebug(e, "保存配置组配置时失败");
_snackbarService.Show(
"保存配置组配置失败",
$"{scriptGroup.Name} 保存失败!",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
private static void SetTaskContextNextFlag(ScriptGroup group)
{
var nst = TaskContext.Instance().Config.NextScheduledTask.Find(item => item.Item1 == group.Name);
foreach (var item in group.Projects)
{
item.NextFlag = false;
if (nst != default)
{
if (nst.Item2 == item.Index && nst.Item3 == item.FolderName && nst.Item4 == item.Name)
{
item.NextFlag = true;
}
}
}
}
private void ReadScriptGroup()
{
try
{
if (!Directory.Exists(ScriptGroupPath))
{
Directory.CreateDirectory(ScriptGroupPath);
}
ScriptGroups.Clear();
var files = Directory.GetFiles(ScriptGroupPath, "*.json");
List<ScriptGroup> groups = [];
foreach (var file in files)
{
try
{
var json = File.ReadAllText(file);
var group = ScriptGroup.FromJson(json);
SetTaskContextNextFlag(group);
if (group.Name == TaskContext.Instance().Config.NextScriptGroupName)
{
group.NextFlag = true;
}
groups.Add(group);
}
catch (Exception e)
{
_logger.LogDebug(e, "读取单个配置组配置时失败");
_snackbarService.Show(
"读取配置组配置失败",
"读取配置组配置失败:" + e.Message,
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
// 按index排序
groups.Sort((a, b) => a.Index.CompareTo(b.Index));
foreach (var group in groups)
{
ScriptGroups.Add(group);
}
}
catch (Exception e)
{
_logger.LogDebug(e, "读取配置组配置时失败");
_snackbarService.Show(
"读取配置组配置失败",
"读取配置组配置失败!",
ControlAppearance.Danger,
null,
TimeSpan.FromSeconds(3)
);
}
}
[RelayCommand]
public void OnGoToScriptGroupUrl()
{
Process.Start(new ProcessStartInfo("https://bettergi.com/feats/autos/dispatcher.html") { UseShellExecute = true });
}
[RelayCommand]
public void OnImportScriptGroup(string scriptGroupExample)
{
ScriptGroup group = new();
if ("AutoCrystalflyExampleGroup" == scriptGroupExample)
{
group.Name = "晶蝶示例组";
group.AddProject(new ScriptGroupProject(new ScriptProject("AutoCrystalfly")));
}
if (ScriptGroups.Any(x => x.Name == group.Name))
{
_snackbarService.Show(
"配置组已存在",
$"配置组 {group.Name} 已经存在,请勿重复添加",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
return;
}
ScriptGroups.Add(group);
}
[RelayCommand]
public async Task OnStartScriptGroupAsync()
{
if (SelectedScriptGroup == null)
{
_snackbarService.Show(
"未选择配置组",
"请先选择一个配置组",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(2)
);
return;
}
RunnerContext.Instance.Reset();
TaskProgress taskProgress = new()
{
ScriptGroupNames = [SelectedScriptGroup.Name]
};
RunnerContext.Instance.taskProgress = taskProgress;
taskProgress.CurrentScriptGroupName = SelectedScriptGroup.Name;
TaskProgressManager.SaveTaskProgress(taskProgress);
await _scriptService.RunMulti(GetNextProjects(SelectedScriptGroup), SelectedScriptGroup.Name, taskProgress);
}
[RelayCommand]
public void OnOpenScriptGroupSettings()
{
if (SelectedScriptGroup == null)
{
return;
}
// var uiMessageBox = new Wpf.Ui.Controls.MessageBox
// {
// Content = new ScriptGroupConfigView(SelectedScriptGroup.Config),
// Title = "配置组设置"
// };
//
// await uiMessageBox.ShowDialogAsync();
var dialogWindow = new FluentWindow
{
Title = "配置组设置",
Content = new ScriptGroupConfigView(new ScriptGroupConfigViewModel(TaskContext.Instance().Config, SelectedScriptGroup.Config)),
Width = 800,
Height = 600,
MinWidth = 800,
MaxWidth = 800,
MinHeight = 600,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
ExtendsContentIntoTitleBar = true,
WindowBackdropType = WindowBackdropType.Auto,
};
dialogWindow.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(dialogWindow);
// var dialogWindow = new WpfUiWindow(new ScriptGroupConfigView(SelectedScriptGroup.Config))
// {
// Title = "配置组设置"
// };
// 显示对话框
var result = dialogWindow.ShowDialog();
// if (result == true)
// {
// // 用户点击了确定或关闭
// }
WriteScriptGroup(SelectedScriptGroup);
}
public static List<ScriptGroup> GetNextScriptGroups(List<ScriptGroup> groups)
{
if (groups.Where(g => g.NextFlag).Count() > 0)
{
List<ScriptGroup> ng = new();
bool start = false;
foreach (var group in groups)
{
if (group.NextFlag)
{
start = true;
group.NextFlag = false;
TaskContext.Instance().Config.NextScriptGroupName = String.Empty;
}
if (start)
{
ng.Add(group);
}
}
return ng;
}
return groups;
}
public static List<ScriptGroupProject> GetNextProjects(ScriptGroup group)
{
SetTaskContextNextFlag(group);
List<ScriptGroupProject> ls = new List<ScriptGroupProject>();
if (group.Projects.Where(g => g.NextFlag ?? false).Count() > 0)
{
bool start = false;
foreach (var item in group.Projects)
{
if (item.NextFlag ?? false)
{
start = true;
}
if (!start)
{
item.SkipFlag = true;
}
ls.Add(item);
}
if (!start)
{
ls.AddRange(group.Projects);
}
//拿出来后清空,和置状态
if (start)
{
List<ValueTuple<string, int, string, string>> nextScheduledTask = TaskContext.Instance().Config.NextScheduledTask;
foreach (var item in nextScheduledTask)
{
if (item.Item1 == group.Name)
{
nextScheduledTask.Remove(item);
break;
}
}
foreach (var item in group.Projects)
{
item.NextFlag = false;
}
}
return ls;
}
return group.Projects.Select(g => g).ToList();
}
[RelayCommand]
public async Task OnContinueMultiScriptGroupAsync()
{
// 创建一个 StackPanel 来包含全选按钮和所有配置组的 CheckBox
var stackPanel = new StackPanel();
// 添加分割线
var separator = new Separator
{
Margin = new Thickness(0, 4, 0, 4)
};
stackPanel.Children.Add(separator);
List<TaskProgress> taskProgresses = TaskProgressManager.LoadAllTaskProgress();
var checkBox = new ComboBox(); ;
stackPanel.Children.Add(checkBox);
ObservableCollection<KeyValuePair<string, string>> kvs = new ObservableCollection<KeyValuePair<string, string>>();
foreach (var taskProgress in taskProgresses)
{
var name = taskProgress.Name + "_" + taskProgress.CurrentScriptGroupName + "_";
if (taskProgress.Loop)
{
name += "循环(" + taskProgress.LoopCount + ")_";
}
if (taskProgress.CurrentScriptGroupProjectInfo != null)
{
name = name + taskProgress.CurrentScriptGroupProjectInfo.Index + "_" + taskProgress.CurrentScriptGroupProjectInfo.Name;
}
kvs.Add(new KeyValuePair<string, string>(taskProgress.Name, name));
}
checkBox.SelectedValuePath = "Key";
checkBox.DisplayMemberPath = "Value";
checkBox.ItemsSource = kvs;
checkBox.SelectedIndex = 0;
//SelectedValuePath="Key"
// DisplayMemberPath="Value"
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "选择需要继续执行的进度记录",
Content = new ScrollViewer
{
Content = stackPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Height = 300 // 设置固定高度
,
Width = 600
},
CloseButtonText = "关闭",
PrimaryButtonText = "确认执行",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
var result = await uiMessageBox.ShowDialogAsync();
if (result == MessageBoxResult.Primary)
{
/*var selectedGroups = checkBoxes
.Where(kv => kv.Value.IsChecked == true)
.Select(kv => kv.Key)
.ToList();*/
Object val = checkBox.SelectedValue;
if (val == null)
{
return;
}
await OnContinueTaskProgressAsync(Convert.ToString(val), taskProgresses);
}
}
public async Task OnContinueTaskProgressAsync(string name, List<TaskProgress>? taskProgresses = null)
{
if (taskProgresses == null)
{
taskProgresses = TaskProgressManager.LoadAllTaskProgress();
}
TaskProgress? taskProgress = null;
if (name == "latest")
{
if (taskProgresses.Count > 0)
{
taskProgress = taskProgresses[0];
}
}
else
{
taskProgress = taskProgresses.FirstOrDefault(t => t.Name == name);
}
if (taskProgress != null)
{
//await StartGroups(selectedGroups);
//taskProgress.Next
var sg = ScriptGroups.ToList().Where(sg => taskProgress.ScriptGroupNames.Contains(sg.Name)).ToList();
TaskProgressManager.GenerNextProjectInfo(taskProgress, sg);
if (taskProgress.Next == null)
{
_logger.LogWarning("无法定位到下一个要执行的项目next为空" + taskProgress.Name + ")");
}
else
{
await StartGroups(sg, taskProgress);
}
}
else
{
_logger.LogWarning("无法定位到下一个要执行的项目:taskProgress为空");
}
}
public async Task OnStartMultiScriptTaskProgressAsync(params string[] names)
{
if (ScriptGroups.Count == 0)
{
ReadScriptGroup();
}
string taskProgressName;
if (names == null || names.Length == 0)
{
taskProgressName = "latest";
}
else
{
taskProgressName = names[0];
}
await OnContinueTaskProgressAsync(taskProgressName);
}
[RelayCommand]
public async Task OnStartMultiScriptGroupAsync()
{
// 创建一个 StackPanel 来包含全选按钮和所有配置组的 CheckBox
var stackPanel = new StackPanel();
var checkBoxes = new Dictionary<ScriptGroup, CheckBox>();
var loopCheckBox = new CheckBox
{
Content = "循环",
};
// 创建全选按钮
var selectAllCheckBox = new CheckBox
{
Content = "全选",
IsThreeState = true
};
selectAllCheckBox.Checked += (s, e) =>
{
foreach (var checkBox in checkBoxes.Values)
{
checkBox.IsChecked = true;
}
};
selectAllCheckBox.Unchecked += (s, e) =>
{
foreach (var checkBox in checkBoxes.Values)
{
checkBox.IsChecked = false;
}
};
selectAllCheckBox.Indeterminate += (s, e) =>
{
if (checkBoxes.Values.All(cb => cb.IsChecked == true))
{
selectAllCheckBox.IsChecked = false;
}
else if (checkBoxes.Values.All(cb => cb.IsChecked == false))
{
selectAllCheckBox.IsChecked = true;
}
};
stackPanel.Children.Add(loopCheckBox);
stackPanel.Children.Add(selectAllCheckBox);
// 添加分割线
var separator = new Separator
{
Margin = new Thickness(0, 4, 0, 4)
};
stackPanel.Children.Add(separator);
// 创建每个配置组的 CheckBox
foreach (var scriptGroup in ScriptGroups)
{
if (scriptGroup.Config.PathingConfig.HideOnRepeat)
{
continue;
}
var checkBox = new CheckBox
{
Content = scriptGroup.Name,
Tag = scriptGroup
};
checkBoxes[scriptGroup] = checkBox;
stackPanel.Children.Add(checkBox);
checkBox.Checked += (s, e) => UpdateSelectAllCheckBoxState();
checkBox.Unchecked += (s, e) => UpdateSelectAllCheckBoxState();
}
void UpdateSelectAllCheckBoxState()
{
int checkedCount = checkBoxes.Values.Count(cb => cb.IsChecked == true);
if (checkedCount == 0)
{
selectAllCheckBox.IsChecked = false;
}
else if (checkedCount == checkBoxes.Count)
{
selectAllCheckBox.IsChecked = true;
}
else
{
selectAllCheckBox.IsChecked = null;
}
}
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "选择需要执行的配置组",
Content = new ScrollViewer
{
Content = stackPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Height = 300 // 设置固定高度
},
CloseButtonText = "关闭",
PrimaryButtonText = "确认执行",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
var result = await uiMessageBox.ShowDialogAsync();
if (result == MessageBoxResult.Primary)
{
var selectedGroups = checkBoxes
.Where(kv => kv.Value.IsChecked == true)
.Select(kv => kv.Key)
.ToList();
if (selectedGroups.Count == 0)
{
_snackbarService.Show(
"未选择配置组",
"请至少选择一个配置组进行执行",
ControlAppearance.Caution,
null,
TimeSpan.FromSeconds(3)
);
return;
}
await StartGroups(selectedGroups, null, loopCheckBox.IsChecked ?? false);
}
}
private void SelectAllCheckBox_Indeterminate(object sender, RoutedEventArgs e)
{
throw new NotImplementedException();
}
public async Task OnStartMultiScriptGroupWithNamesAsync(params string[] names)
{
if (ScriptGroups.Count == 0)
{
ReadScriptGroup();
}
List<ScriptGroup> scriptGroups = new List<ScriptGroup>();
foreach (var name in names)
{
try
{
var group = ScriptGroups.First(x => x.Name == name);
scriptGroups.Add(group);
}
catch (InvalidOperationException)
{
_logger.LogWarning("传入的配置组名称不存在:{Name}", name);
}
}
if (scriptGroups.Count > 0)
{
await StartGroups(scriptGroups);
}
else
{
_logger.LogWarning("需要执行的配置组为空");
}
}
public async Task StartGroups(List<ScriptGroup> scriptGroups, TaskProgress? taskProgress = null, bool loop = false)
{
_logger.LogInformation("开始连续执行选中配置组:{Names}", string.Join(",", scriptGroups.Select(x => x.Name)));
try
{
RunnerContext.Instance.IsContinuousRunGroup = true;
if (taskProgress == null)
{
taskProgress = new()
{
ScriptGroupNames = scriptGroups.Select(x => x.Name).ToList()
,
Loop = loop
};
}
RunnerContext.Instance.taskProgress = taskProgress;
var sg = GetNextScriptGroups(scriptGroups);
foreach (var scriptGroup in sg)
{
if (taskProgress.Next != null)
{
if (scriptGroup.Name != taskProgress.Next.GroupName)
{
continue;
}
}
taskProgress.CurrentScriptGroupName = scriptGroup.Name;
TaskProgressManager.SaveTaskProgress(taskProgress);
await _scriptService.RunMulti(GetNextProjects(scriptGroup), scriptGroup.Name, taskProgress);
await Task.Delay(2000);
}
taskProgress.LoopCount++;
if (taskProgress is { Loop: true })
{
taskProgress.LastScriptGroupName = null;
taskProgress.LastSuccessScriptGroupProjectInfo = null;
taskProgress.Next = null;
await StartGroups(scriptGroups, taskProgress);
}
else
{
//只有最后一次成功才算
if (taskProgress.ConsecutiveFailureCount == 0)
{
taskProgress.EndTime = DateTime.Now;
TaskProgressManager.SaveTaskProgress(taskProgress);
}
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
}
finally
{
RunnerContext.Instance.Reset();
}
}
}