diff --git a/src/ToolboxApp/App.xaml b/src/ToolboxApp/App.xaml
index 32710c7..7cdd8a9 100644
--- a/src/ToolboxApp/App.xaml
+++ b/src/ToolboxApp/App.xaml
@@ -1,9 +1,32 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ToolboxApp/MainWindow.xaml b/src/ToolboxApp/MainWindow.xaml
index 1df5096..02475a5 100644
--- a/src/ToolboxApp/MainWindow.xaml
+++ b/src/ToolboxApp/MainWindow.xaml
@@ -1,12 +1,295 @@
-
-
+ Title="个人工具箱"
+ Height="720"
+ Width="1120"
+ MinHeight="620"
+ MinWidth="960"
+ Background="{StaticResource AppBackgroundBrush}"
+ Loaded="Window_OnLoaded"
+ Closing="Window_OnClosing">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ToolboxApp/MainWindow.xaml.cs b/src/ToolboxApp/MainWindow.xaml.cs
index 13b045b..da9f2f3 100644
--- a/src/ToolboxApp/MainWindow.xaml.cs
+++ b/src/ToolboxApp/MainWindow.xaml.cs
@@ -1,23 +1,202 @@
-using System.Text;
+using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
+using ToolboxApp.Models;
+using ToolboxApp.Services;
+using ToolboxApp.ViewModels;
+using MessageBox = System.Windows.MessageBox;
namespace ToolboxApp;
-///
-/// Interaction logic for MainWindow.xaml
-///
public partial class MainWindow : Window
{
+ private readonly MainWindowViewModel _viewModel;
+ private readonly HotkeyService _hotkeyService = new();
+ private TrayService? _trayService;
+ private bool _initialized;
+ private bool _exitRequested;
+
public MainWindow()
{
InitializeComponent();
+ _viewModel = new MainWindowViewModel();
+ _viewModel.HotkeysRefreshRequested += (_, _) => RegisterHotkeys();
+ DataContext = _viewModel;
}
-}
\ No newline at end of file
+
+ private async void Window_OnLoaded(object sender, RoutedEventArgs e)
+ {
+ if (_initialized)
+ {
+ return;
+ }
+
+ _initialized = true;
+ await _viewModel.InitializeAsync(this);
+
+ Width = _viewModel.Data.Settings.MainWindowWidth;
+ Height = _viewModel.Data.Settings.MainWindowHeight;
+
+ _trayService = new TrayService(
+ this,
+ () => _viewModel.OpenSettingsCommand.Execute(null),
+ enabled => _viewModel.SetGlobalHotkeysEnabled(enabled),
+ () => _viewModel.Data.Settings.GlobalHotkeysEnabled,
+ RequestExit);
+
+ RegisterHotkeys();
+
+ if (_viewModel.Data.Settings.StartHiddenToTray)
+ {
+ Hide();
+ _trayService.RefreshMenuText();
+ }
+
+ await _viewModel.RunAutoRunAsync();
+ }
+
+ private void Window_OnClosing(object? sender, CancelEventArgs e)
+ {
+ if (!_exitRequested && _viewModel.Data.Settings.HideOnClose)
+ {
+ e.Cancel = true;
+ Hide();
+ _trayService?.RefreshMenuText();
+ return;
+ }
+
+ _viewModel.Data.Settings.MainWindowWidth = Width;
+ _viewModel.Data.Settings.MainWindowHeight = Height;
+ _viewModel.SaveAsync().GetAwaiter().GetResult();
+ _hotkeyService.Dispose();
+ _trayService?.Dispose();
+ }
+
+ private void SearchTextBox_OnKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ SearchTextBox.Clear();
+ }
+ }
+
+ protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
+ {
+ base.OnPreviewKeyDown(e);
+
+ if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.F)
+ {
+ SearchTextBox.Focus();
+ SearchTextBox.SelectAll();
+ e.Handled = true;
+ }
+ }
+
+ private void ToolsListBox_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
+ {
+ if (_viewModel.LaunchSelectedCommand.CanExecute(null))
+ {
+ _viewModel.LaunchSelectedCommand.Execute(null);
+ }
+ }
+
+ private void ToolsListBox_OnKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter && _viewModel.LaunchSelectedCommand.CanExecute(null))
+ {
+ _viewModel.LaunchSelectedCommand.Execute(null);
+ e.Handled = true;
+ }
+ }
+
+ private void ToolsListBox_OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var item = FindParent((DependencyObject)e.OriginalSource);
+ if (item is not null)
+ {
+ item.IsSelected = true;
+ item.Focus();
+ }
+ }
+
+ private void ContextLaunch_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (_viewModel.LaunchSelectedCommand.CanExecute(null))
+ {
+ _viewModel.LaunchSelectedCommand.Execute(null);
+ }
+ }
+
+ private void ContextEdit_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (_viewModel.EditSelectedCommand.CanExecute(null))
+ {
+ _viewModel.EditSelectedCommand.Execute(null);
+ }
+ }
+
+ private void ContextDelete_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (_viewModel.DeleteSelectedCommand.CanExecute(null))
+ {
+ _viewModel.DeleteSelectedCommand.Execute(null);
+ }
+ }
+
+ private void RegisterHotkeys()
+ {
+ if (!_initialized)
+ {
+ return;
+ }
+
+ _hotkeyService.RegisterAll(
+ this,
+ _viewModel.Data.Tools,
+ _viewModel.Data.Settings.GlobalHotkeysEnabled,
+ _viewModel.LaunchToolByIdAsync,
+ message => _viewModel.AddLog(message.Level, message.Message));
+ _trayService?.RefreshMenuText();
+ }
+
+ private void RequestExit()
+ {
+ if (_viewModel.Data.Settings.ConfirmExit)
+ {
+ var confirmed = MessageBox.Show(
+ this,
+ "确定退出个人工具箱吗?退出后托盘和全局快捷键都会停止。",
+ "退出",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question) == MessageBoxResult.Yes;
+
+ if (!confirmed)
+ {
+ return;
+ }
+ }
+
+ _exitRequested = true;
+ Close();
+ System.Windows.Application.Current.Shutdown();
+ }
+
+ private static T? FindParent(DependencyObject child)
+ where T : DependencyObject
+ {
+ var current = child;
+ while (current is not null)
+ {
+ if (current is T result)
+ {
+ return result;
+ }
+
+ current = VisualTreeHelper.GetParent(current);
+ }
+
+ return null;
+ }
+}
diff --git a/src/ToolboxApp/Services/ConfigurationService.cs b/src/ToolboxApp/Services/ConfigurationService.cs
index 18a3f56..1dcb730 100644
--- a/src/ToolboxApp/Services/ConfigurationService.cs
+++ b/src/ToolboxApp/Services/ConfigurationService.cs
@@ -34,6 +34,7 @@ public sealed class ConfigurationService
public async Task LoadAsync(CancellationToken cancellationToken = default)
{
EnsureDirectories();
+ var toolsFileExists = File.Exists(ToolsPath);
var data = new ToolboxData
{
@@ -45,7 +46,11 @@ public sealed class ConfigurationService
data.Settings.DataVersion = CurrentDataVersion;
SystemToolService.EnsureDefaultCategories(data.Categories);
- SystemToolService.RestoreDefaultSystemTools(data.Categories, data.Tools);
+ if (!toolsFileExists || data.Tools.Count == 0)
+ {
+ SystemToolService.RestoreDefaultSystemTools(data.Categories, data.Tools);
+ }
+
MergeAutoRunEntries(data);
await SaveAsync(data, cancellationToken);
diff --git a/src/ToolboxApp/ViewModels/CombinationMemberViewModel.cs b/src/ToolboxApp/ViewModels/CombinationMemberViewModel.cs
new file mode 100644
index 0000000..df6f650
--- /dev/null
+++ b/src/ToolboxApp/ViewModels/CombinationMemberViewModel.cs
@@ -0,0 +1,55 @@
+using ToolboxApp.Models;
+
+namespace ToolboxApp.ViewModels;
+
+public sealed class CombinationMemberViewModel : ObservableObject
+{
+ private readonly Func _toolResolver;
+ private bool _enabled;
+ private int _intervalAfterMs;
+
+ public CombinationMemberViewModel(CombinationMember member, Func toolResolver)
+ {
+ Member = member;
+ _toolResolver = toolResolver;
+ _enabled = member.Enabled;
+ _intervalAfterMs = member.IntervalAfterMs;
+ }
+
+ public CombinationMember Member { get; }
+ public string ToolId => Member.ToolId;
+ public string ToolName => _toolResolver(Member.ToolId)?.Name ?? "引用不存在";
+ public string ToolTypeLabel => _toolResolver(Member.ToolId)?.Type switch
+ {
+ ToolType.System => "系统",
+ ToolType.Local => "本地",
+ ToolType.Url => "网址",
+ ToolType.Combination => "组合",
+ _ => "未知"
+ };
+
+ public bool Enabled
+ {
+ get => _enabled;
+ set
+ {
+ if (SetProperty(ref _enabled, value))
+ {
+ Member.Enabled = value;
+ }
+ }
+ }
+
+ public int IntervalAfterMs
+ {
+ get => _intervalAfterMs;
+ set
+ {
+ var normalized = Math.Max(0, value);
+ if (SetProperty(ref _intervalAfterMs, normalized))
+ {
+ Member.IntervalAfterMs = normalized;
+ }
+ }
+ }
+}
diff --git a/src/ToolboxApp/ViewModels/MainWindowViewModel.cs b/src/ToolboxApp/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..6285701
--- /dev/null
+++ b/src/ToolboxApp/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,552 @@
+using System.Collections.ObjectModel;
+using System.Windows;
+using ToolboxApp.Commands;
+using ToolboxApp.Models;
+using ToolboxApp.Services;
+using ToolboxApp.Views;
+using Clipboard = System.Windows.Clipboard;
+using MessageBox = System.Windows.MessageBox;
+
+namespace ToolboxApp.ViewModels;
+
+public sealed class MainWindowViewModel : ObservableObject
+{
+ private readonly ConfigurationService _configurationService;
+ private readonly ToolLaunchService _toolLaunchService;
+ private readonly AutoRunService _autoRunService;
+ private readonly PathValidationService _pathValidationService;
+ private readonly StartupService _startupService;
+ private ToolboxData _data = new();
+ private CategoryItem? _selectedCategory;
+ private ToolCardViewModel? _selectedTool;
+ private string _searchText = "";
+ private bool _isLogPanelExpanded = true;
+
+ public MainWindowViewModel()
+ {
+ _configurationService = new ConfigurationService();
+ _toolLaunchService = new ToolLaunchService();
+ _autoRunService = new AutoRunService(_toolLaunchService);
+ _pathValidationService = new PathValidationService();
+ _startupService = new StartupService();
+
+ AddLocalToolCommand = new RelayCommand(AddLocalTool);
+ AddUrlToolCommand = new RelayCommand(AddUrlTool);
+ AddCombinationCommand = new RelayCommand(AddCombination);
+ LaunchSelectedCommand = new AsyncRelayCommand(LaunchSelectedAsync, () => SelectedTool is not null);
+ EditSelectedCommand = new RelayCommand(EditSelectedTool, () => SelectedTool is not null);
+ DeleteSelectedCommand = new RelayCommand(DeleteSelectedTool, () => SelectedTool is not null);
+ OpenSettingsCommand = new RelayCommand(OpenSettings);
+ AddCategoryCommand = new RelayCommand(AddCategory);
+ RenameCategoryCommand = new RelayCommand(RenameCategory, () => SelectedCategory is not null);
+ DeleteCategoryCommand = new RelayCommand(DeleteCategory, () => SelectedCategory is not null && SelectedCategory.Id != SystemToolService.UncategorizedCategoryId);
+ ClearLogsCommand = new RelayCommand(() => Logs.Clear());
+ CopyLogsCommand = new RelayCommand(CopyLogs, () => Logs.Count > 0);
+ ToggleLogPanelCommand = new RelayCommand(() => IsLogPanelExpanded = !IsLogPanelExpanded);
+ }
+
+ public event EventHandler? HotkeysRefreshRequested;
+
+ public Window? Owner { get; set; }
+
+ public ObservableCollection Categories { get; } = [];
+ public ObservableCollection Tools { get; } = [];
+ public ObservableCollection Logs { get; } = [];
+
+ public RelayCommand AddLocalToolCommand { get; }
+ public RelayCommand AddUrlToolCommand { get; }
+ public RelayCommand AddCombinationCommand { get; }
+ public AsyncRelayCommand LaunchSelectedCommand { get; }
+ public RelayCommand EditSelectedCommand { get; }
+ public RelayCommand DeleteSelectedCommand { get; }
+ public RelayCommand OpenSettingsCommand { get; }
+ public RelayCommand AddCategoryCommand { get; }
+ public RelayCommand RenameCategoryCommand { get; }
+ public RelayCommand DeleteCategoryCommand { get; }
+ public RelayCommand ClearLogsCommand { get; }
+ public RelayCommand CopyLogsCommand { get; }
+ public RelayCommand ToggleLogPanelCommand { get; }
+
+ public ToolboxData Data => _data;
+ public ConfigurationService ConfigurationService => _configurationService;
+ public StartupService StartupService => _startupService;
+
+ public CategoryItem? SelectedCategory
+ {
+ get => _selectedCategory;
+ set
+ {
+ if (SetProperty(ref _selectedCategory, value))
+ {
+ RefreshTools();
+ RaiseSelectionCommandState();
+ }
+ }
+ }
+
+ public ToolCardViewModel? SelectedTool
+ {
+ get => _selectedTool;
+ set
+ {
+ if (SetProperty(ref _selectedTool, value))
+ {
+ RaiseSelectionCommandState();
+ }
+ }
+ }
+
+ public string SearchText
+ {
+ get => _searchText;
+ set
+ {
+ if (SetProperty(ref _searchText, value))
+ {
+ RefreshTools();
+ }
+ }
+ }
+
+ public bool IsLogPanelExpanded
+ {
+ get => _isLogPanelExpanded;
+ set
+ {
+ if (SetProperty(ref _isLogPanelExpanded, value))
+ {
+ _data.Settings.LogPanelExpanded = value;
+ _ = SaveAsync();
+ }
+ }
+ }
+
+ public async Task InitializeAsync(Window owner)
+ {
+ Owner = owner;
+ _data = await _configurationService.LoadAsync();
+ _isLogPanelExpanded = _data.Settings.LogPanelExpanded;
+ OnPropertyChanged(nameof(IsLogPanelExpanded));
+
+ var invalidCount = _pathValidationService.ValidateTools(_data.Tools);
+ RefreshCategories();
+ RefreshTools();
+
+ AddLog(LogLevel.Info, $"配置目录:{_configurationService.ConfigDirectory}");
+ AddLog(LogLevel.Success, "个人工具箱已启动。");
+ if (invalidCount > 0)
+ {
+ AddLog(LogLevel.Warning, $"路径检查完成:发现 {invalidCount} 个失效工具。");
+ }
+
+ await SaveAsync();
+ }
+
+ public async Task RunAutoRunAsync(CancellationToken cancellationToken = default)
+ {
+ _configurationService.MergeAutoRunEntries(_data);
+ await SaveAsync(cancellationToken);
+ await _autoRunService.ExecuteAsync(_data, AddLog, cancellationToken);
+ }
+
+ public async Task LaunchToolByIdAsync(string toolId)
+ {
+ var tool = _data.Tools.FirstOrDefault(item => item.Id == toolId && !item.IsDeleted);
+ if (tool is null)
+ {
+ AddLog(LogLevel.Warning, $"快捷键目标不存在:{toolId}");
+ return;
+ }
+
+ await _toolLaunchService.LaunchAsync(tool, _data.Tools, AddLog);
+ }
+
+ public async Task SaveAsync(CancellationToken cancellationToken = default)
+ {
+ _configurationService.MergeAutoRunEntries(_data);
+ await _configurationService.SaveAsync(_data, cancellationToken);
+ }
+
+ public void RefreshHotkeys()
+ {
+ HotkeysRefreshRequested?.Invoke(this, EventArgs.Empty);
+ RefreshTools();
+ }
+
+ public void SetGlobalHotkeysEnabled(bool enabled)
+ {
+ _data.Settings.GlobalHotkeysEnabled = enabled;
+ AddLog(LogLevel.Info, enabled ? "全局快捷键已开启。" : "全局快捷键已关闭。");
+ _ = SaveAsync();
+ RefreshHotkeys();
+ }
+
+ public void AddLog(LogLevel level, string message)
+ {
+ System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ {
+ Logs.Add(new LogMessage
+ {
+ Level = level,
+ Message = message
+ });
+
+ while (Logs.Count > 500)
+ {
+ Logs.RemoveAt(0);
+ }
+
+ CopyLogsCommand.RaiseCanExecuteChanged();
+ });
+ }
+
+ private void AddLog(LogMessage message)
+ {
+ AddLog(message.Level, message.Message);
+ }
+
+ private async Task LaunchSelectedAsync()
+ {
+ if (SelectedTool is null)
+ {
+ return;
+ }
+
+ await _toolLaunchService.LaunchAsync(SelectedTool.Tool, _data.Tools, AddLog);
+ RefreshTools();
+ }
+
+ private void AddLocalTool()
+ {
+ var tool = CreateBaseTool(ToolType.Local);
+ tool.Name = "新的本地工具";
+ tool.IconKey = "folder";
+
+ var edited = ToolEditorWindow.Edit(tool, Categories, Owner);
+ if (edited is null)
+ {
+ return;
+ }
+
+ edited.SortOrder = NextToolSortOrder();
+ _data.Tools.Add(edited);
+ _pathValidationService.ValidateTools(_data.Tools);
+ RefreshTools();
+ AddLog(LogLevel.Success, $"已添加本地工具:{edited.Name}");
+ _ = SaveAsync();
+ RefreshHotkeys();
+ }
+
+ private void AddUrlTool()
+ {
+ var tool = CreateBaseTool(ToolType.Url);
+ tool.Name = "新的网址";
+ tool.IconKey = "link";
+
+ var edited = ToolEditorWindow.Edit(tool, Categories, Owner);
+ if (edited is null)
+ {
+ return;
+ }
+
+ edited.SortOrder = NextToolSortOrder();
+ _data.Tools.Add(edited);
+ RefreshTools();
+ AddLog(LogLevel.Success, $"已添加网址工具:{edited.Name}");
+ _ = SaveAsync();
+ RefreshHotkeys();
+ }
+
+ private void AddCombination()
+ {
+ var tool = CreateBaseTool(ToolType.Combination);
+ tool.Name = "新的组合";
+ tool.IconKey = "combination";
+ tool.Combination = new CombinationConfig();
+
+ var edited = CombinationEditorWindow.Edit(tool, Categories, _data.Tools, _toolLaunchService, Owner);
+ if (edited is null)
+ {
+ return;
+ }
+
+ edited.SortOrder = NextToolSortOrder();
+ _data.Tools.Add(edited);
+ RefreshTools();
+ AddLog(LogLevel.Success, $"已添加组合:{edited.Name}");
+ _ = SaveAsync();
+ RefreshHotkeys();
+ }
+
+ private void EditSelectedTool()
+ {
+ if (SelectedTool is null)
+ {
+ return;
+ }
+
+ var original = SelectedTool.Tool;
+ var candidate = original.Clone();
+ ToolItem? edited = original.Type == ToolType.Combination
+ ? CombinationEditorWindow.Edit(candidate, Categories, _data.Tools, _toolLaunchService, Owner)
+ : ToolEditorWindow.Edit(candidate, Categories, Owner);
+
+ if (edited is null)
+ {
+ return;
+ }
+
+ CopyToolValues(edited, original);
+ _pathValidationService.ValidateTools(_data.Tools);
+ RefreshTools();
+ AddLog(LogLevel.Success, $"已保存工具:{original.Name}");
+ _ = SaveAsync();
+ RefreshHotkeys();
+ }
+
+ private void DeleteSelectedTool()
+ {
+ if (SelectedTool is null)
+ {
+ return;
+ }
+
+ var tool = SelectedTool.Tool;
+ var confirmed = MessageBox.Show(
+ Owner,
+ $"确定删除“{tool.Name}”吗?组合中对它的引用也会失效。",
+ "删除工具",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning) == MessageBoxResult.Yes;
+
+ if (!confirmed)
+ {
+ return;
+ }
+
+ tool.IsDeleted = true;
+ foreach (var combination in _data.Tools.Where(item => item.Type == ToolType.Combination && item.Combination is not null))
+ {
+ combination.Combination!.Members.RemoveAll(member => member.ToolId == tool.Id);
+ }
+
+ RefreshTools();
+ AddLog(LogLevel.Warning, $"已删除工具:{tool.Name}");
+ _ = SaveAsync();
+ RefreshHotkeys();
+ }
+
+ private void OpenSettings()
+ {
+ var window = new SettingsWindow(_data, _configurationService, _startupService, _pathValidationService, _toolLaunchService)
+ {
+ Owner = Owner
+ };
+
+ if (window.ShowDialog() == true)
+ {
+ _data = window.Data;
+ _pathValidationService.ValidateTools(_data.Tools);
+ RefreshCategories();
+ RefreshTools();
+ AddLog(LogLevel.Success, "设置已保存。");
+ _ = SaveAsync();
+ RefreshHotkeys();
+ }
+ }
+
+ private void AddCategory()
+ {
+ var name = PromptWindow.Prompt("新建分类", "分类名称", "", Owner);
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return;
+ }
+
+ _data.Categories.Add(new CategoryItem
+ {
+ Name = name.Trim(),
+ IconKey = "category",
+ SortOrder = _data.Categories.Count == 0 ? 0 : _data.Categories.Max(category => category.SortOrder) + 1
+ });
+
+ RefreshCategories();
+ AddLog(LogLevel.Success, $"已新建分类:{name.Trim()}");
+ _ = SaveAsync();
+ }
+
+ private void RenameCategory()
+ {
+ if (SelectedCategory is null)
+ {
+ return;
+ }
+
+ var name = PromptWindow.Prompt("重命名分类", "分类名称", SelectedCategory.Name, Owner);
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return;
+ }
+
+ SelectedCategory.Name = name.Trim();
+ RefreshCategories();
+ RefreshTools();
+ AddLog(LogLevel.Success, $"已重命名分类:{name.Trim()}");
+ _ = SaveAsync();
+ }
+
+ private void DeleteCategory()
+ {
+ if (SelectedCategory is null)
+ {
+ return;
+ }
+
+ if (SelectedCategory.Id == SystemToolService.UncategorizedCategoryId)
+ {
+ AddLog(LogLevel.Warning, "未分类为固定分类,不能删除。");
+ return;
+ }
+
+ var confirmed = MessageBox.Show(
+ Owner,
+ $"删除分类“{SelectedCategory.Name}”后,其中工具会移动到“未分类”。是否继续?",
+ "删除分类",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning) == MessageBoxResult.Yes;
+
+ if (!confirmed)
+ {
+ return;
+ }
+
+ foreach (var tool in _data.Tools.Where(tool => tool.CategoryId == SelectedCategory.Id))
+ {
+ tool.CategoryId = SystemToolService.UncategorizedCategoryId;
+ }
+
+ var deletedName = SelectedCategory.Name;
+ _data.Categories.Remove(SelectedCategory);
+ RefreshCategories();
+ SelectedCategory = _data.Categories.FirstOrDefault(category => category.Id == SystemToolService.UncategorizedCategoryId);
+ AddLog(LogLevel.Warning, $"已删除分类:{deletedName}");
+ _ = SaveAsync();
+ }
+
+ private void CopyLogs()
+ {
+ if (Logs.Count == 0)
+ {
+ return;
+ }
+
+ Clipboard.SetText(string.Join(Environment.NewLine, Logs.Select(log => log.DisplayText)));
+ AddLog(LogLevel.Success, "已复制底部信息。");
+ }
+
+ private ToolItem CreateBaseTool(ToolType type)
+ {
+ return new ToolItem
+ {
+ Id = Guid.NewGuid().ToString("N"),
+ Type = type,
+ CategoryId = SelectedCategory?.Id ?? Categories.FirstOrDefault()?.Id ?? SystemToolService.UncategorizedCategoryId,
+ SortOrder = NextToolSortOrder()
+ };
+ }
+
+ private int NextToolSortOrder()
+ {
+ return _data.Tools.Count == 0 ? 0 : _data.Tools.Max(tool => tool.SortOrder) + 1;
+ }
+
+ private void RefreshCategories()
+ {
+ Categories.Clear();
+ foreach (var category in _data.Categories.OrderBy(category => category.SortOrder).ThenBy(category => category.Name))
+ {
+ Categories.Add(category);
+ }
+
+ SelectedCategory ??= Categories.FirstOrDefault();
+ if (SelectedCategory is not null && Categories.All(category => category.Id != SelectedCategory.Id))
+ {
+ SelectedCategory = Categories.FirstOrDefault();
+ }
+
+ DeleteCategoryCommand.RaiseCanExecuteChanged();
+ RenameCategoryCommand.RaiseCanExecuteChanged();
+ }
+
+ private void RefreshTools()
+ {
+ var selectedId = SelectedTool?.Id;
+ Tools.Clear();
+
+ var query = _data.Tools.Where(tool => !tool.IsDeleted);
+ if (!string.IsNullOrWhiteSpace(SearchText))
+ {
+ var keyword = SearchText.Trim();
+ query = query.Where(tool => MatchesSearch(tool, keyword));
+ }
+ else if (SelectedCategory is not null)
+ {
+ query = query.Where(tool => tool.CategoryId == SelectedCategory.Id);
+ }
+
+ foreach (var tool in query.OrderBy(tool => tool.SortOrder).ThenBy(tool => tool.Name))
+ {
+ Tools.Add(new ToolCardViewModel(tool, ResolveCategoryName));
+ }
+
+ SelectedTool = Tools.FirstOrDefault(tool => tool.Id == selectedId);
+ RaiseSelectionCommandState();
+ }
+
+ private bool MatchesSearch(ToolItem tool, string keyword)
+ {
+ return Contains(tool.Name, keyword)
+ || Contains(tool.Description, keyword)
+ || Contains(tool.LaunchTarget, keyword)
+ || Contains(tool.Url, keyword)
+ || Contains(ResolveCategoryName(tool.CategoryId), keyword);
+ }
+
+ private static bool Contains(string? source, string keyword)
+ {
+ return !string.IsNullOrWhiteSpace(source)
+ && source.Contains(keyword, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ private string ResolveCategoryName(string categoryId)
+ {
+ return _data.Categories.FirstOrDefault(category => category.Id == categoryId)?.Name ?? "未分类";
+ }
+
+ private void RaiseSelectionCommandState()
+ {
+ LaunchSelectedCommand.RaiseCanExecuteChanged();
+ EditSelectedCommand.RaiseCanExecuteChanged();
+ DeleteSelectedCommand.RaiseCanExecuteChanged();
+ RenameCategoryCommand.RaiseCanExecuteChanged();
+ DeleteCategoryCommand.RaiseCanExecuteChanged();
+ }
+
+ private static void CopyToolValues(ToolItem source, ToolItem target)
+ {
+ target.Name = source.Name;
+ target.Description = source.Description;
+ target.Type = source.Type;
+ target.CategoryId = source.CategoryId;
+ target.IconKey = source.IconKey;
+ target.LaunchTarget = source.LaunchTarget;
+ target.Url = source.Url;
+ target.Arguments = source.Arguments;
+ target.WorkingDirectory = source.WorkingDirectory;
+ target.SystemToolKey = source.SystemToolKey;
+ target.Hotkey = source.Hotkey;
+ target.AutoRunEnabled = source.AutoRunEnabled;
+ target.RunAsAdmin = source.RunAsAdmin;
+ target.Combination = source.Combination?.Clone();
+ }
+}
diff --git a/src/ToolboxApp/ViewModels/ToolCardViewModel.cs b/src/ToolboxApp/ViewModels/ToolCardViewModel.cs
new file mode 100644
index 0000000..03a24e1
--- /dev/null
+++ b/src/ToolboxApp/ViewModels/ToolCardViewModel.cs
@@ -0,0 +1,124 @@
+using ToolboxApp.Models;
+
+namespace ToolboxApp.ViewModels;
+
+public sealed class ToolCardViewModel : ObservableObject
+{
+ private readonly Func _categoryNameResolver;
+
+ public ToolCardViewModel(ToolItem tool, Func categoryNameResolver)
+ {
+ Tool = tool;
+ _categoryNameResolver = categoryNameResolver;
+ }
+
+ public ToolItem Tool { get; }
+
+ public string Id => Tool.Id;
+ public string Name => Tool.Name;
+ public string Description => string.IsNullOrWhiteSpace(Tool.Description) ? "暂无说明" : Tool.Description;
+ public string CategoryName => _categoryNameResolver(Tool.CategoryId);
+ public string IconText => IconKeyToText(Tool.IconKey, Tool.Type);
+ public string TypeLabel => Tool.Type switch
+ {
+ ToolType.System => "系统",
+ ToolType.Local => "本地",
+ ToolType.Url => "网址",
+ ToolType.Combination => "组合",
+ _ => "工具"
+ };
+
+ public IReadOnlyList StatusBadges
+ {
+ get
+ {
+ var badges = new List();
+ if (Tool.PathInvalid)
+ {
+ badges.Add("路径失效");
+ }
+
+ if (!string.IsNullOrWhiteSpace(Tool.Hotkey))
+ {
+ badges.Add("快捷键");
+ }
+
+ if (Tool.AutoRunEnabled)
+ {
+ badges.Add("自动运行");
+ }
+
+ if (Tool.RunAsAdmin)
+ {
+ badges.Add("管理员");
+ }
+
+ return badges;
+ }
+ }
+
+ public string DetailText
+ {
+ get
+ {
+ return Tool.Type switch
+ {
+ ToolType.Local => Tool.LaunchTarget ?? "",
+ ToolType.Url => Tool.Url ?? "",
+ ToolType.System => Tool.LaunchTarget ?? "",
+ ToolType.Combination => $"{Tool.Combination?.Members.Count ?? 0} 个成员",
+ _ => ""
+ };
+ }
+ }
+
+ public void Refresh()
+ {
+ OnPropertyChanged(nameof(Name));
+ OnPropertyChanged(nameof(Description));
+ OnPropertyChanged(nameof(CategoryName));
+ OnPropertyChanged(nameof(IconText));
+ OnPropertyChanged(nameof(TypeLabel));
+ OnPropertyChanged(nameof(StatusBadges));
+ OnPropertyChanged(nameof(DetailText));
+ }
+
+ private static string IconKeyToText(string iconKey, ToolType type)
+ {
+ if (!string.IsNullOrWhiteSpace(iconKey))
+ {
+ var mapped = iconKey.ToLowerInvariant() switch
+ {
+ "notepad" => "TXT",
+ "calculator" => "123",
+ "taskmgr" => "CPU",
+ "control" => "CTL",
+ "settings" => "SET",
+ "device" => "DEV",
+ "disk" => "DSK",
+ "service" => "SVC",
+ "registry" => "REG",
+ "network" => "NET",
+ "apps" => "APP",
+ "link" => "URL",
+ "folder" => "DIR",
+ "combination" => "COM",
+ _ => ""
+ };
+
+ if (!string.IsNullOrWhiteSpace(mapped))
+ {
+ return mapped;
+ }
+ }
+
+ return type switch
+ {
+ ToolType.System => "SYS",
+ ToolType.Local => "LOC",
+ ToolType.Url => "URL",
+ ToolType.Combination => "COM",
+ _ => "BOX"
+ };
+ }
+}
diff --git a/src/ToolboxApp/Views/CombinationEditorWindow.xaml b/src/ToolboxApp/Views/CombinationEditorWindow.xaml
new file mode 100644
index 0000000..0205c12
--- /dev/null
+++ b/src/ToolboxApp/Views/CombinationEditorWindow.xaml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ToolboxApp/Views/CombinationEditorWindow.xaml.cs b/src/ToolboxApp/Views/CombinationEditorWindow.xaml.cs
new file mode 100644
index 0000000..4df3873
--- /dev/null
+++ b/src/ToolboxApp/Views/CombinationEditorWindow.xaml.cs
@@ -0,0 +1,189 @@
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using ToolboxApp.Models;
+using ToolboxApp.Services;
+using ToolboxApp.ViewModels;
+using MessageBox = System.Windows.MessageBox;
+
+namespace ToolboxApp.Views;
+
+public partial class CombinationEditorWindow : Window
+{
+ private readonly ToolItem _tool;
+ private readonly IReadOnlyList _allTools;
+ private readonly ToolLaunchService _launchService;
+ private readonly ObservableCollection _members = [];
+
+ private CombinationEditorWindow(
+ ToolItem tool,
+ IEnumerable categories,
+ IEnumerable allTools,
+ ToolLaunchService launchService)
+ {
+ InitializeComponent();
+ _tool = tool;
+ _allTools = allTools.Where(item => !item.IsDeleted).ToList();
+ _launchService = launchService;
+
+ _tool.Combination ??= new CombinationConfig();
+ var combination = _tool.Combination;
+
+ CategoryComboBox.ItemsSource = categories.ToList();
+ CategoryComboBox.SelectedValue = tool.CategoryId;
+ ToolPickerComboBox.ItemsSource = _allTools.Where(item => item.Id != tool.Id).OrderBy(item => item.Name).ToList();
+ MembersDataGrid.ItemsSource = _members;
+
+ NameTextBox.Text = tool.Name;
+ DescriptionTextBox.Text = tool.Description;
+ HotkeyTextBox.Text = tool.Hotkey;
+ AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
+ FailurePolicyComboBox.SelectedIndex = combination.FailurePolicy == FailurePolicy.Stop ? 1 : 0;
+
+ foreach (var member in combination.Members.OrderBy(member => member.SortOrder))
+ {
+ _members.Add(new CombinationMemberViewModel(member.Clone(), ResolveTool));
+ }
+ }
+
+ public ToolItem EditedTool => _tool;
+
+ public static ToolItem? Edit(
+ ToolItem tool,
+ IEnumerable categories,
+ IEnumerable allTools,
+ ToolLaunchService launchService,
+ Window? owner)
+ {
+ var window = new CombinationEditorWindow(tool, categories, allTools, launchService)
+ {
+ Owner = owner
+ };
+
+ return window.ShowDialog() == true ? window.EditedTool : null;
+ }
+
+ private void AddMemberButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (ToolPickerComboBox.SelectedItem is not ToolItem selectedTool)
+ {
+ return;
+ }
+
+ if (_members.Any(member => member.ToolId == selectedTool.Id))
+ {
+ MessageBox.Show(this, "该成员已经在当前组合中。", "添加成员", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ _members.Add(new CombinationMemberViewModel(new CombinationMember
+ {
+ ToolId = selectedTool.Id,
+ Enabled = true,
+ SortOrder = _members.Count,
+ IntervalAfterMs = 0
+ }, ResolveTool));
+ }
+
+ private void RemoveMemberButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (MembersDataGrid.SelectedItem is CombinationMemberViewModel selected)
+ {
+ _members.Remove(selected);
+ ReorderMembers();
+ }
+ }
+
+ private void MoveUpButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ MoveSelected(-1);
+ }
+
+ private void MoveDownButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ MoveSelected(1);
+ }
+
+ private void SaveButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var name = NameTextBox.Text.Trim();
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ MessageBox.Show(this, "请输入组合名称。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (CategoryComboBox.SelectedValue is not string categoryId || string.IsNullOrWhiteSpace(categoryId))
+ {
+ MessageBox.Show(this, "请选择分类。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var hotkey = HotkeyTextBox.Text.Trim();
+ if (!string.IsNullOrWhiteSpace(hotkey) && !HotkeyParser.TryParse(hotkey, out _, out _))
+ {
+ MessageBox.Show(this, "快捷键格式无效,请使用类似 Ctrl + Alt + D 的格式。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ ReorderMembers();
+ _tool.Name = name;
+ _tool.Description = DescriptionTextBox.Text.Trim();
+ _tool.CategoryId = categoryId;
+ _tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
+ _tool.AutoRunEnabled = AutoRunCheckBox.IsChecked == true;
+ _tool.IconKey = "combination";
+ _tool.Combination = new CombinationConfig
+ {
+ FailurePolicy = FailurePolicyComboBox.SelectedIndex == 1 ? FailurePolicy.Stop : FailurePolicy.Continue,
+ Members = _members.Select(member => member.Member.Clone()).ToList()
+ };
+
+ var validationTools = _allTools.Where(item => item.Id != _tool.Id).Concat([_tool]).ToList();
+ var validation = _launchService.ValidateCombination(_tool, validationTools);
+ if (!validation.Success)
+ {
+ MessageBox.Show(this, validation.ErrorMessage ?? "组合校验失败。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ DialogResult = true;
+ }
+
+ private void CancelButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+
+ private ToolItem? ResolveTool(string toolId)
+ {
+ return _allTools.FirstOrDefault(tool => tool.Id == toolId);
+ }
+
+ private void MoveSelected(int offset)
+ {
+ if (MembersDataGrid.SelectedItem is not CombinationMemberViewModel selected)
+ {
+ return;
+ }
+
+ var index = _members.IndexOf(selected);
+ var targetIndex = index + offset;
+ if (targetIndex < 0 || targetIndex >= _members.Count)
+ {
+ return;
+ }
+
+ _members.Move(index, targetIndex);
+ MembersDataGrid.SelectedItem = selected;
+ ReorderMembers();
+ }
+
+ private void ReorderMembers()
+ {
+ for (var index = 0; index < _members.Count; index++)
+ {
+ _members[index].Member.SortOrder = index;
+ }
+ }
+}
diff --git a/src/ToolboxApp/Views/PromptWindow.xaml b/src/ToolboxApp/Views/PromptWindow.xaml
new file mode 100644
index 0000000..bb5204f
--- /dev/null
+++ b/src/ToolboxApp/Views/PromptWindow.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ToolboxApp/Views/PromptWindow.xaml.cs b/src/ToolboxApp/Views/PromptWindow.xaml.cs
new file mode 100644
index 0000000..5496735
--- /dev/null
+++ b/src/ToolboxApp/Views/PromptWindow.xaml.cs
@@ -0,0 +1,47 @@
+using System.Windows;
+using System.Windows.Input;
+
+namespace ToolboxApp.Views;
+
+public partial class PromptWindow : Window
+{
+ private PromptWindow(string title, string prompt, string defaultValue)
+ {
+ InitializeComponent();
+ Title = title;
+ PromptText.Text = prompt;
+ ValueTextBox.Text = defaultValue;
+ ValueTextBox.SelectAll();
+ Loaded += (_, _) => ValueTextBox.Focus();
+ }
+
+ public string Value => ValueTextBox.Text.Trim();
+
+ public static string? Prompt(string title, string prompt, string defaultValue, Window? owner)
+ {
+ var window = new PromptWindow(title, prompt, defaultValue)
+ {
+ Owner = owner
+ };
+
+ return window.ShowDialog() == true ? window.Value : null;
+ }
+
+ private void OkButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = true;
+ }
+
+ private void CancelButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+
+ private void ValueTextBox_OnKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ DialogResult = true;
+ }
+ }
+}
diff --git a/src/ToolboxApp/Views/SettingsWindow.xaml b/src/ToolboxApp/Views/SettingsWindow.xaml
new file mode 100644
index 0000000..dda89fd
--- /dev/null
+++ b/src/ToolboxApp/Views/SettingsWindow.xaml
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ToolboxApp/Views/SettingsWindow.xaml.cs b/src/ToolboxApp/Views/SettingsWindow.xaml.cs
new file mode 100644
index 0000000..769a396
--- /dev/null
+++ b/src/ToolboxApp/Views/SettingsWindow.xaml.cs
@@ -0,0 +1,302 @@
+using System.Collections.ObjectModel;
+using System.Windows;
+using ToolboxApp.Models;
+using ToolboxApp.Services;
+using MessageBox = System.Windows.MessageBox;
+using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
+using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
+
+namespace ToolboxApp.Views;
+
+public partial class SettingsWindow : Window
+{
+ private readonly ConfigurationService _configurationService;
+ private readonly StartupService _startupService;
+ private readonly PathValidationService _pathValidationService;
+ private readonly ToolLaunchService _toolLaunchService;
+ private readonly ObservableCollection _autoRunRows = [];
+ private ToolboxData _data;
+
+ public SettingsWindow(
+ ToolboxData data,
+ ConfigurationService configurationService,
+ StartupService startupService,
+ PathValidationService pathValidationService,
+ ToolLaunchService toolLaunchService)
+ {
+ InitializeComponent();
+ _configurationService = configurationService;
+ _startupService = startupService;
+ _pathValidationService = pathValidationService;
+ _toolLaunchService = toolLaunchService;
+ _data = CloneData(data);
+
+ LoadControls();
+ }
+
+ public ToolboxData Data => _data;
+
+ private void LoadControls()
+ {
+ StartHiddenCheckBox.IsChecked = _data.Settings.StartHiddenToTray;
+ HideOnCloseCheckBox.IsChecked = _data.Settings.HideOnClose;
+ ConfirmExitCheckBox.IsChecked = _data.Settings.ConfirmExit;
+ LogExpandedCheckBox.IsChecked = _data.Settings.LogPanelExpanded;
+ GlobalHotkeysCheckBox.IsChecked = _data.Settings.GlobalHotkeysEnabled;
+ StartupCheckBox.IsChecked = _startupService.IsEnabled();
+
+ RebuildAutoRunRows();
+ AutoRunDataGrid.ItemsSource = _autoRunRows;
+ HotkeysDataGrid.ItemsSource = _data.Tools.Where(tool => !tool.IsDeleted && !string.IsNullOrWhiteSpace(tool.Hotkey)).OrderBy(tool => tool.Name).ToList();
+ }
+
+ private void RebuildAutoRunRows()
+ {
+ _configurationService.MergeAutoRunEntries(_data);
+ _autoRunRows.Clear();
+ foreach (var entry in _data.AutoRunEntries.OrderBy(entry => entry.SortOrder))
+ {
+ var tool = _data.Tools.FirstOrDefault(item => item.Id == entry.ToolId);
+ if (tool is null)
+ {
+ continue;
+ }
+
+ _autoRunRows.Add(new AutoRunRow(entry, tool));
+ }
+ }
+
+ private void SaveButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ _data.Settings.StartHiddenToTray = StartHiddenCheckBox.IsChecked == true;
+ _data.Settings.HideOnClose = HideOnCloseCheckBox.IsChecked == true;
+ _data.Settings.ConfirmExit = ConfirmExitCheckBox.IsChecked == true;
+ _data.Settings.LogPanelExpanded = LogExpandedCheckBox.IsChecked == true;
+ _data.Settings.GlobalHotkeysEnabled = GlobalHotkeysCheckBox.IsChecked == true;
+
+ foreach (var row in _autoRunRows)
+ {
+ row.Apply();
+ }
+
+ _data.AutoRunEntries = _autoRunRows.Select(row => row.Entry).OrderBy(row => row.SortOrder).ToList();
+
+ try
+ {
+ _startupService.SetEnabled(StartupCheckBox.IsChecked == true);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this, $"设置开机自启失败:{ex.Message}", "保存设置", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+
+ DialogResult = true;
+ }
+
+ private void CancelButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+
+ private void AutoRunMoveUpButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ MoveAutoRunRow(-1);
+ }
+
+ private void AutoRunMoveDownButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ MoveAutoRunRow(1);
+ }
+
+ private void AutoRunRemoveButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (AutoRunDataGrid.SelectedItem is not AutoRunRow row)
+ {
+ return;
+ }
+
+ row.Tool.AutoRunEnabled = false;
+ _autoRunRows.Remove(row);
+ ReorderAutoRunRows();
+ }
+
+ private void OpenConfigButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ _configurationService.OpenConfigDirectory();
+ DataStatusTextBlock.Text = "已打开配置目录。";
+ }
+
+ private async void ExportButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var dialog = new SaveFileDialog
+ {
+ Title = "导出配置",
+ Filter = "Zip 文件 (*.zip)|*.zip",
+ FileName = $"ToolboxApp_Config_{DateTime.Now:yyyyMMdd_HHmmss}.zip"
+ };
+
+ if (dialog.ShowDialog(this) != true)
+ {
+ return;
+ }
+
+ await _configurationService.ExportAsync(dialog.FileName, _data);
+ DataStatusTextBlock.Text = $"配置已导出:{dialog.FileName}";
+ }
+
+ private async void ImportButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var confirmed = MessageBox.Show(
+ this,
+ "导入配置会先备份当前配置,然后覆盖当前配置。是否继续?",
+ "导入配置",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning) == MessageBoxResult.Yes;
+
+ if (!confirmed)
+ {
+ return;
+ }
+
+ var dialog = new OpenFileDialog
+ {
+ Title = "导入配置",
+ Filter = "Zip 文件 (*.zip)|*.zip|所有文件 (*.*)|*.*"
+ };
+
+ if (dialog.ShowDialog(this) != true)
+ {
+ return;
+ }
+
+ _data = await _configurationService.ImportAsync(dialog.FileName);
+ _pathValidationService.ValidateTools(_data.Tools);
+ LoadControls();
+ DataStatusTextBlock.Text = "配置导入完成。建议检查自动运行列表和快捷键,确认其中没有不需要启动的工具。";
+ }
+
+ private void ValidatePathsButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var invalidCount = _pathValidationService.ValidateTools(_data.Tools);
+ DataStatusTextBlock.Text = $"路径检查完成:发现 {invalidCount} 个失效工具。";
+ }
+
+ private async void ResetButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var confirmed = MessageBox.Show(
+ this,
+ "重置配置会先备份当前配置,然后恢复默认系统工具。是否继续?",
+ "重置配置",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning) == MessageBoxResult.Yes;
+
+ if (!confirmed)
+ {
+ return;
+ }
+
+ _data = await _configurationService.ResetAsync();
+ LoadControls();
+ DataStatusTextBlock.Text = "配置已重置。";
+ }
+
+ private void RestoreSystemToolsButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var added = SystemToolService.RestoreDefaultSystemTools(_data.Categories, _data.Tools);
+ SystemToolsStatusTextBlock.Text = added == 0
+ ? "默认系统工具已经完整,无需补回。"
+ : $"已补回 {added} 个默认系统工具。";
+ RebuildAutoRunRows();
+ }
+
+ private void MoveAutoRunRow(int offset)
+ {
+ if (AutoRunDataGrid.SelectedItem is not AutoRunRow selected)
+ {
+ return;
+ }
+
+ var index = _autoRunRows.IndexOf(selected);
+ var targetIndex = index + offset;
+ if (targetIndex < 0 || targetIndex >= _autoRunRows.Count)
+ {
+ return;
+ }
+
+ _autoRunRows.Move(index, targetIndex);
+ AutoRunDataGrid.SelectedItem = selected;
+ ReorderAutoRunRows();
+ }
+
+ private void ReorderAutoRunRows()
+ {
+ for (var index = 0; index < _autoRunRows.Count; index++)
+ {
+ _autoRunRows[index].SortOrder = index;
+ }
+ }
+
+ private static ToolboxData CloneData(ToolboxData source)
+ {
+ return new ToolboxData
+ {
+ Settings = new AppSettings
+ {
+ DataVersion = source.Settings.DataVersion,
+ StartHiddenToTray = source.Settings.StartHiddenToTray,
+ HideOnClose = source.Settings.HideOnClose,
+ ConfirmExit = source.Settings.ConfirmExit,
+ GlobalHotkeysEnabled = source.Settings.GlobalHotkeysEnabled,
+ LogPanelExpanded = source.Settings.LogPanelExpanded,
+ Theme = source.Settings.Theme,
+ CardSize = source.Settings.CardSize,
+ MainWindowWidth = source.Settings.MainWindowWidth,
+ MainWindowHeight = source.Settings.MainWindowHeight
+ },
+ Categories = source.Categories.Select(category => category.Clone()).ToList(),
+ Tools = source.Tools.Select(tool => tool.Clone()).ToList(),
+ AutoRunEntries = source.AutoRunEntries.Select(entry => new AutoRunEntry
+ {
+ ToolId = entry.ToolId,
+ Enabled = entry.Enabled,
+ SortOrder = entry.SortOrder,
+ IntervalAfterMs = entry.IntervalAfterMs
+ }).ToList()
+ };
+ }
+
+ public sealed class AutoRunRow
+ {
+ public AutoRunRow(AutoRunEntry entry, ToolItem tool)
+ {
+ Entry = entry;
+ Tool = tool;
+ Enabled = entry.Enabled;
+ SortOrder = entry.SortOrder;
+ IntervalAfterMs = entry.IntervalAfterMs;
+ }
+
+ public AutoRunEntry Entry { get; }
+ public ToolItem Tool { get; }
+ public string ToolName => Tool.Name;
+ public string ToolTypeLabel => Tool.Type switch
+ {
+ ToolType.System => "系统",
+ ToolType.Local => "本地",
+ ToolType.Url => "网址",
+ ToolType.Combination => "组合",
+ _ => "工具"
+ };
+
+ public bool Enabled { get; set; }
+ public int SortOrder { get; set; }
+ public int IntervalAfterMs { get; set; }
+
+ public void Apply()
+ {
+ Entry.Enabled = Enabled;
+ Entry.SortOrder = SortOrder;
+ Entry.IntervalAfterMs = Math.Max(0, IntervalAfterMs);
+ }
+ }
+}
diff --git a/src/ToolboxApp/Views/ToolEditorWindow.xaml b/src/ToolboxApp/Views/ToolEditorWindow.xaml
new file mode 100644
index 0000000..5747005
--- /dev/null
+++ b/src/ToolboxApp/Views/ToolEditorWindow.xaml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ToolboxApp/Views/ToolEditorWindow.xaml.cs b/src/ToolboxApp/Views/ToolEditorWindow.xaml.cs
new file mode 100644
index 0000000..5d0b57c
--- /dev/null
+++ b/src/ToolboxApp/Views/ToolEditorWindow.xaml.cs
@@ -0,0 +1,157 @@
+using System.Windows;
+using Forms = System.Windows.Forms;
+using ToolboxApp.Models;
+using ToolboxApp.Services;
+using MessageBox = System.Windows.MessageBox;
+using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
+
+namespace ToolboxApp.Views;
+
+public partial class ToolEditorWindow : Window
+{
+ private readonly ToolItem _tool;
+
+ private ToolEditorWindow(ToolItem tool, IEnumerable categories)
+ {
+ InitializeComponent();
+ _tool = tool;
+
+ CategoryComboBox.ItemsSource = categories.ToList();
+ CategoryComboBox.SelectedValue = tool.CategoryId;
+
+ Title = tool.Type switch
+ {
+ ToolType.System => "编辑系统工具",
+ ToolType.Local => "编辑本地工具",
+ ToolType.Url => "编辑网址工具",
+ _ => "编辑工具"
+ };
+
+ TypeTextBlock.Text = tool.Type switch
+ {
+ ToolType.System => "系统工具",
+ ToolType.Local => "本地工具",
+ ToolType.Url => "网址",
+ _ => "工具"
+ };
+
+ NameTextBox.Text = tool.Name;
+ DescriptionTextBox.Text = tool.Description;
+ TargetTextBox.Text = tool.Type == ToolType.Url ? tool.Url : tool.LaunchTarget;
+ ArgumentsTextBox.Text = tool.Arguments;
+ WorkingDirectoryTextBox.Text = tool.WorkingDirectory;
+ HotkeyTextBox.Text = tool.Hotkey;
+ AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
+ RunAsAdminCheckBox.IsChecked = tool.RunAsAdmin;
+
+ if (tool.Type == ToolType.Url)
+ {
+ TargetLabel.Text = "网址";
+ ArgumentsTextBox.IsEnabled = false;
+ WorkingDirectoryTextBox.IsEnabled = false;
+ RunAsAdminCheckBox.IsEnabled = false;
+ }
+
+ if (tool.Type == ToolType.System)
+ {
+ TargetLabel.Text = "启动目标";
+ }
+ }
+
+ public ToolItem EditedTool => _tool;
+
+ public static ToolItem? Edit(ToolItem tool, IEnumerable categories, Window? owner)
+ {
+ var window = new ToolEditorWindow(tool, categories)
+ {
+ Owner = owner
+ };
+
+ return window.ShowDialog() == true ? window.EditedTool : null;
+ }
+
+ private void BrowseFileButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var dialog = new OpenFileDialog
+ {
+ Title = "选择本地工具",
+ CheckFileExists = false,
+ Filter = "所有文件 (*.*)|*.*"
+ };
+
+ if (dialog.ShowDialog(this) == true)
+ {
+ TargetTextBox.Text = dialog.FileName;
+ }
+ }
+
+ private void BrowseFolderButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ using var dialog = new Forms.FolderBrowserDialog
+ {
+ Description = "选择文件夹作为本地工具",
+ UseDescriptionForTitle = true
+ };
+
+ if (dialog.ShowDialog() == Forms.DialogResult.OK)
+ {
+ TargetTextBox.Text = dialog.SelectedPath;
+ }
+ }
+
+ private void SaveButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ var name = NameTextBox.Text.Trim();
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ MessageBox.Show(this, "请输入工具名称。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (CategoryComboBox.SelectedValue is not string categoryId || string.IsNullOrWhiteSpace(categoryId))
+ {
+ MessageBox.Show(this, "请选择分类。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var hotkey = HotkeyTextBox.Text.Trim();
+ if (!string.IsNullOrWhiteSpace(hotkey) && !HotkeyParser.TryParse(hotkey, out _, out _))
+ {
+ MessageBox.Show(this, "快捷键格式无效,请使用类似 Ctrl + Alt + T 的格式。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (_tool.Type == ToolType.Url)
+ {
+ if (!PathValidationService.IsValidUrl(TargetTextBox.Text, out var normalizedUrl))
+ {
+ MessageBox.Show(this, "请输入有效的网址。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ _tool.Url = normalizedUrl;
+ _tool.LaunchTarget = null;
+ }
+ else
+ {
+ _tool.LaunchTarget = TargetTextBox.Text.Trim();
+ _tool.Url = null;
+ }
+
+ _tool.Name = name;
+ _tool.Description = DescriptionTextBox.Text.Trim();
+ _tool.CategoryId = categoryId;
+ _tool.Arguments = ArgumentsTextBox.Text.Trim();
+ _tool.WorkingDirectory = WorkingDirectoryTextBox.Text.Trim();
+ _tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
+ _tool.AutoRunEnabled = AutoRunCheckBox.IsChecked == true;
+ _tool.RunAsAdmin = RunAsAdminCheckBox.IsChecked == true;
+
+ DialogResult = true;
+ }
+
+ private void CancelButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+}