feat: 统一项目命名并补充路径失效报告
将内部项目目录、命名空间、配置目录、自启注册表值和设计/开发文档统一为 PersonalToolbox。 扩展路径校验服务,输出失效工具、字段、原因和路径,并在启动日志、设置页路径检查与导入配置流程中展示明细报告。 验证:dotnet build PersonalToolbox.sln
This commit is contained in:
32
src/PersonalToolbox/App.xaml
Normal file
32
src/PersonalToolbox/App.xaml
Normal file
@@ -0,0 +1,32 @@
|
||||
<Application x:Class="PersonalToolbox.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#F7F8FA" />
|
||||
<SolidColorBrush x:Key="PanelBackgroundBrush" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="BorderBrushSoft" Color="#D9DEE7" />
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB" />
|
||||
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#172033" />
|
||||
<SolidColorBrush x:Key="SecondaryTextBrush" Color="#5C667A" />
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Padding" Value="12,4" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
13
src/PersonalToolbox/App.xaml.cs
Normal file
13
src/PersonalToolbox/App.xaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace PersonalToolbox;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
}
|
||||
|
||||
10
src/PersonalToolbox/AssemblyInfo.cs
Normal file
10
src/PersonalToolbox/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly:ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
82
src/PersonalToolbox/Commands/RelayCommand.cs
Normal file
82
src/PersonalToolbox/Commands/RelayCommand.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace PersonalToolbox.Commands;
|
||||
|
||||
public sealed class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
: this(_ => execute(), canExecute is null ? null : _ => canExecute())
|
||||
{
|
||||
}
|
||||
|
||||
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return _canExecute?.Invoke(parameter) ?? true;
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
_execute(parameter);
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isRunning;
|
||||
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return !_isRunning && (_canExecute?.Invoke() ?? true);
|
||||
}
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isRunning = true;
|
||||
RaiseCanExecuteChanged();
|
||||
await _execute();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRunning = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
295
src/PersonalToolbox/MainWindow.xaml
Normal file
295
src/PersonalToolbox/MainWindow.xaml
Normal file
@@ -0,0 +1,295 @@
|
||||
<Window x:Class="PersonalToolbox.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:PersonalToolbox.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
Title="个人工具箱"
|
||||
Height="720"
|
||||
Width="1120"
|
||||
MinHeight="620"
|
||||
MinWidth="960"
|
||||
Background="{StaticResource AppBackgroundBrush}"
|
||||
Loaded="Window_OnLoaded"
|
||||
Closing="Window_OnClosing">
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Background="{StaticResource PanelBackgroundBrush}"
|
||||
BorderBrush="{StaticResource BorderBrushSoft}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Padding="12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox x:Name="SearchTextBox"
|
||||
Width="360"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="搜索工具名称、说明、分类、路径或网址。按 Ctrl + F 可快速聚焦。"
|
||||
KeyDown="SearchTextBox_OnKeyDown" />
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Content="添加本地"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding AddLocalToolCommand}"
|
||||
ToolTip="添加 exe、lnk、bat、cmd、文件或文件夹等本地工具。" />
|
||||
<Button Content="添加网址"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding AddUrlToolCommand}"
|
||||
ToolTip="添加使用默认浏览器打开的网址工具。" />
|
||||
<Button Content="添加组合"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding AddCombinationCommand}"
|
||||
ToolTip="创建由多个工具或组合组成的一键启动组合。" />
|
||||
<Button Content="启动"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding LaunchSelectedCommand}"
|
||||
ToolTip="启动当前选中的工具。也可双击卡片或按 Enter。" />
|
||||
<Button Content="编辑"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding EditSelectedCommand}"
|
||||
ToolTip="编辑当前选中的工具或组合。" />
|
||||
<Button Content="删除"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding DeleteSelectedCommand}"
|
||||
ToolTip="删除当前选中的工具。删除前会确认。" />
|
||||
<Button Content="设置"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding OpenSettingsCommand}"
|
||||
ToolTip="打开常规、自动运行、快捷键和数据设置。" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,12,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="230" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Background="{StaticResource PanelBackgroundBrush}"
|
||||
BorderBrush="{StaticResource BorderBrushSoft}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Padding="12">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="分类"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource PrimaryTextBrush}"
|
||||
ToolTip="一级分类列表,工具只能属于一个分类。" />
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
Margin="0,10,0,10"
|
||||
ItemsSource="{Binding Categories}"
|
||||
SelectedItem="{Binding SelectedCategory}"
|
||||
BorderThickness="0"
|
||||
ToolTip="单击切换分类;搜索时会跨分类显示结果。">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}"
|
||||
Padding="8,7"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding Name}" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="2">
|
||||
<Button Content="新建分类"
|
||||
Command="{Binding AddCategoryCommand}"
|
||||
ToolTip="创建一个新的一级分类。" />
|
||||
<Button Content="重命名分类"
|
||||
Margin="0,8,0,0"
|
||||
Command="{Binding RenameCategoryCommand}"
|
||||
ToolTip="重命名当前选中的分类。" />
|
||||
<Button Content="删除分类"
|
||||
Margin="0,8,0,0"
|
||||
Command="{Binding DeleteCategoryCommand}"
|
||||
ToolTip="删除当前分类,并把其中工具移动到未分类。" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2"
|
||||
Background="{StaticResource PanelBackgroundBrush}"
|
||||
BorderBrush="{StaticResource BorderBrushSoft}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Padding="12">
|
||||
<ListBox x:Name="ToolsListBox"
|
||||
ItemsSource="{Binding Tools}"
|
||||
SelectedItem="{Binding SelectedTool}"
|
||||
BorderThickness="0"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
MouseDoubleClick="ToolsListBox_OnMouseDoubleClick"
|
||||
PreviewMouseRightButtonDown="ToolsListBox_OnPreviewMouseRightButtonDown"
|
||||
KeyDown="ToolsListBox_OnKeyDown"
|
||||
ToolTip="单击选中卡片,双击启动,右键打开管理菜单。">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel IsItemsHost="True" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:ToolCardViewModel}">
|
||||
<Border Width="210"
|
||||
Height="146"
|
||||
Margin="6"
|
||||
Padding="12"
|
||||
CornerRadius="8"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{StaticResource BorderBrushSoft}"
|
||||
Background="#FBFCFE"
|
||||
ToolTip="{Binding DetailText}">
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="启动"
|
||||
ToolTip="启动该工具或组合。"
|
||||
Click="ContextLaunch_OnClick" />
|
||||
<MenuItem Header="编辑"
|
||||
ToolTip="编辑该工具或组合。"
|
||||
Click="ContextEdit_OnClick" />
|
||||
<MenuItem Header="删除"
|
||||
ToolTip="删除该工具。"
|
||||
Click="ContextDelete_OnClick" />
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel>
|
||||
<Border Width="42"
|
||||
Height="32"
|
||||
CornerRadius="6"
|
||||
Background="#E8F0FF"
|
||||
DockPanel.Dock="Left"
|
||||
ToolTip="工具图标占位;后续会扩展自动图标和图标库。">
|
||||
<TextBlock Text="{Binding IconText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource PrimaryBrush}" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding TypeLabel}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{StaticResource SecondaryTextBrush}"
|
||||
ToolTip="工具类型标记。" />
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{Binding Name}"
|
||||
Margin="0,10,0,0"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource PrimaryTextBrush}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding Name}" />
|
||||
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding Description}"
|
||||
Margin="0,6,0,0"
|
||||
Foreground="{StaticResource SecondaryTextBrush}"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding Description}" />
|
||||
|
||||
<ItemsControl Grid.Row="3"
|
||||
ItemsSource="{Binding StatusBadges}"
|
||||
Margin="0,8,0,0"
|
||||
ToolTip="状态标记:路径失效、快捷键、自动运行或管理员。">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#EDF2F7"
|
||||
CornerRadius="4"
|
||||
Padding="5,2"
|
||||
Margin="0,0,5,4">
|
||||
<TextBlock Text="{Binding}"
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource SecondaryTextBrush}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Expander Grid.Row="2"
|
||||
Header="底部信息"
|
||||
IsExpanded="{Binding IsLogPanelExpanded}"
|
||||
ToolTip="显示启动、组合、自动运行、导入导出和错误信息。">
|
||||
<Border Background="{StaticResource PanelBackgroundBrush}"
|
||||
BorderBrush="{StaticResource BorderBrushSoft}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Padding="10"
|
||||
Margin="0,8,0,0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="120" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="复制"
|
||||
Width="70"
|
||||
Command="{Binding CopyLogsCommand}"
|
||||
ToolTip="复制当前底部信息区内容。" />
|
||||
<Button Content="清空"
|
||||
Width="70"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding ClearLogsCommand}"
|
||||
ToolTip="清空本次运行的底部信息。" />
|
||||
</StackPanel>
|
||||
<ListBox Grid.Row="1"
|
||||
Margin="0,8,0,0"
|
||||
ItemsSource="{Binding Logs}"
|
||||
BorderThickness="0"
|
||||
ToolTip="普通错误不会打断操作,会显示在这里。">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayText}"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Expander>
|
||||
</Grid>
|
||||
</Window>
|
||||
202
src/PersonalToolbox/MainWindow.xaml.cs
Normal file
202
src/PersonalToolbox/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using PersonalToolbox.Models;
|
||||
using PersonalToolbox.Services;
|
||||
using PersonalToolbox.ViewModels;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace PersonalToolbox;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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<ListBoxItem>((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<T>(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;
|
||||
}
|
||||
}
|
||||
234
src/PersonalToolbox/Models/ToolModels.cs
Normal file
234
src/PersonalToolbox/Models/ToolModels.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PersonalToolbox.Models;
|
||||
|
||||
public enum ToolType
|
||||
{
|
||||
System,
|
||||
Local,
|
||||
Url,
|
||||
Combination
|
||||
}
|
||||
|
||||
public enum FailurePolicy
|
||||
{
|
||||
Continue,
|
||||
Stop
|
||||
}
|
||||
|
||||
public enum LaunchResultKind
|
||||
{
|
||||
Success,
|
||||
PathMissing,
|
||||
InvalidUrl,
|
||||
AccessDenied,
|
||||
UserCancelledElevation,
|
||||
ProcessStartFailed,
|
||||
CircularReference,
|
||||
DuplicateTool,
|
||||
MissingReference,
|
||||
UnknownError,
|
||||
Skipped
|
||||
}
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public sealed class ToolItem
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public ToolType Type { get; set; }
|
||||
public string CategoryId { get; set; } = "";
|
||||
public string IconKey { get; set; } = "toolbox";
|
||||
public string? LaunchTarget { get; set; }
|
||||
public string? Url { get; set; }
|
||||
public string? Arguments { get; set; }
|
||||
public string? WorkingDirectory { get; set; }
|
||||
public string? SystemToolKey { get; set; }
|
||||
public string? Hotkey { get; set; }
|
||||
public string HotkeyStatus { get; set; } = "未设置";
|
||||
public bool AutoRunEnabled { get; set; }
|
||||
public bool RunAsAdmin { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public bool PathInvalid { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public CombinationConfig? Combination { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsCombination => Type == ToolType.Combination;
|
||||
|
||||
public ToolItem Clone()
|
||||
{
|
||||
return new ToolItem
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
Type = Type,
|
||||
CategoryId = CategoryId,
|
||||
IconKey = IconKey,
|
||||
LaunchTarget = LaunchTarget,
|
||||
Url = Url,
|
||||
Arguments = Arguments,
|
||||
WorkingDirectory = WorkingDirectory,
|
||||
SystemToolKey = SystemToolKey,
|
||||
Hotkey = Hotkey,
|
||||
HotkeyStatus = HotkeyStatus,
|
||||
AutoRunEnabled = AutoRunEnabled,
|
||||
RunAsAdmin = RunAsAdmin,
|
||||
IsDeleted = IsDeleted,
|
||||
PathInvalid = PathInvalid,
|
||||
SortOrder = SortOrder,
|
||||
Combination = Combination?.Clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CombinationConfig
|
||||
{
|
||||
public FailurePolicy FailurePolicy { get; set; } = FailurePolicy.Continue;
|
||||
public List<CombinationMember> Members { get; set; } = [];
|
||||
|
||||
public CombinationConfig Clone()
|
||||
{
|
||||
return new CombinationConfig
|
||||
{
|
||||
FailurePolicy = FailurePolicy,
|
||||
Members = Members.Select(member => member.Clone()).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CombinationMember
|
||||
{
|
||||
public string ToolId { get; set; } = "";
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
public int IntervalAfterMs { get; set; }
|
||||
|
||||
public CombinationMember Clone()
|
||||
{
|
||||
return new CombinationMember
|
||||
{
|
||||
ToolId = ToolId,
|
||||
Enabled = Enabled,
|
||||
SortOrder = SortOrder,
|
||||
IntervalAfterMs = IntervalAfterMs
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CategoryItem
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Name { get; set; } = "";
|
||||
public string IconKey { get; set; } = "category";
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsBuiltIn { get; set; }
|
||||
|
||||
public CategoryItem Clone()
|
||||
{
|
||||
return new CategoryItem
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name,
|
||||
IconKey = IconKey,
|
||||
SortOrder = SortOrder,
|
||||
IsBuiltIn = IsBuiltIn
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AutoRunEntry
|
||||
{
|
||||
public string ToolId { get; set; } = "";
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
public int IntervalAfterMs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AppSettings
|
||||
{
|
||||
public int DataVersion { get; set; } = 1;
|
||||
public bool StartHiddenToTray { get; set; }
|
||||
public bool HideOnClose { get; set; } = true;
|
||||
public bool ConfirmExit { get; set; } = true;
|
||||
public bool GlobalHotkeysEnabled { get; set; } = true;
|
||||
public bool LogPanelExpanded { get; set; } = true;
|
||||
public string Theme { get; set; } = "FollowSystem";
|
||||
public string CardSize { get; set; } = "Medium";
|
||||
public double MainWindowWidth { get; set; } = 1120;
|
||||
public double MainWindowHeight { get; set; } = 720;
|
||||
}
|
||||
|
||||
public sealed class ToolboxData
|
||||
{
|
||||
public AppSettings Settings { get; set; } = new();
|
||||
public List<CategoryItem> Categories { get; set; } = [];
|
||||
public List<ToolItem> Tools { get; set; } = [];
|
||||
public List<AutoRunEntry> AutoRunEntries { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class DataEnvelope<T>
|
||||
{
|
||||
public int DataVersion { get; set; } = 1;
|
||||
public List<T> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class LaunchResult
|
||||
{
|
||||
public string ToolId { get; set; } = "";
|
||||
public string ToolName { get; set; } = "";
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public LaunchResultKind Kind { get; set; }
|
||||
|
||||
public static LaunchResult Ok(ToolItem tool)
|
||||
{
|
||||
return new LaunchResult
|
||||
{
|
||||
ToolId = tool.Id,
|
||||
ToolName = tool.Name,
|
||||
Success = true,
|
||||
Kind = LaunchResultKind.Success
|
||||
};
|
||||
}
|
||||
|
||||
public static LaunchResult Fail(ToolItem tool, LaunchResultKind kind, string message)
|
||||
{
|
||||
return new LaunchResult
|
||||
{
|
||||
ToolId = tool.Id,
|
||||
ToolName = tool.Name,
|
||||
Success = false,
|
||||
Kind = kind,
|
||||
ErrorMessage = message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LogMessage
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.Now;
|
||||
public LogLevel Level { get; init; }
|
||||
public string Message { get; init; } = "";
|
||||
|
||||
[JsonIgnore]
|
||||
public string LevelText => Level switch
|
||||
{
|
||||
LogLevel.Success => "Success",
|
||||
LogLevel.Warning => "Warning",
|
||||
LogLevel.Error => "Error",
|
||||
_ => "Info"
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public string DisplayText => $"[{Timestamp:HH:mm:ss}] [{LevelText}] {Message}";
|
||||
}
|
||||
12
src/PersonalToolbox/PersonalToolbox.csproj
Normal file
12
src/PersonalToolbox/PersonalToolbox.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
69
src/PersonalToolbox/Services/AutoRunService.cs
Normal file
69
src/PersonalToolbox/Services/AutoRunService.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class AutoRunService
|
||||
{
|
||||
private readonly ToolLaunchService _launchService;
|
||||
|
||||
public AutoRunService(ToolLaunchService launchService)
|
||||
{
|
||||
_launchService = launchService;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(ToolboxData data, Action<LogMessage> log, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entries = data.AutoRunEntries
|
||||
.Where(entry => entry.Enabled)
|
||||
.OrderBy(entry => entry.SortOrder)
|
||||
.ToList();
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, "开始执行启动时自动运行"));
|
||||
var success = 0;
|
||||
var failed = 0;
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var tool = data.Tools.FirstOrDefault(item => item.Id == entry.ToolId && item.AutoRunEnabled && !item.IsDeleted);
|
||||
if (tool is null)
|
||||
{
|
||||
failed++;
|
||||
log(CreateLog(LogLevel.Warning, $"自动运行项不存在或已禁用:{entry.ToolId}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = await _launchService.LaunchAsync(tool, data.Tools, log, cancellationToken);
|
||||
if (result.Success)
|
||||
{
|
||||
success++;
|
||||
}
|
||||
else
|
||||
{
|
||||
failed++;
|
||||
}
|
||||
|
||||
if (entry.IntervalAfterMs > 0)
|
||||
{
|
||||
await Task.Delay(entry.IntervalAfterMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, $"启动时自动运行完成:成功 {success} 项,失败 {failed} 项"));
|
||||
}
|
||||
|
||||
private static LogMessage CreateLog(LogLevel level, string message)
|
||||
{
|
||||
return new LogMessage
|
||||
{
|
||||
Level = level,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
269
src/PersonalToolbox/Services/ConfigurationService.cs
Normal file
269
src/PersonalToolbox/Services/ConfigurationService.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class ConfigurationService
|
||||
{
|
||||
private const int CurrentDataVersion = 1;
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
public string ConfigDirectory { get; }
|
||||
public string IconsDirectory => Path.Combine(ConfigDirectory, "icons");
|
||||
|
||||
private string SettingsPath => Path.Combine(ConfigDirectory, "appsettings.json");
|
||||
private string CategoriesPath => Path.Combine(ConfigDirectory, "categories.json");
|
||||
private string ToolsPath => Path.Combine(ConfigDirectory, "tools.json");
|
||||
private string AutoRunPath => Path.Combine(ConfigDirectory, "autorun.json");
|
||||
|
||||
public ConfigurationService()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
ConfigDirectory = Path.Combine(appData, "PersonalToolbox");
|
||||
}
|
||||
|
||||
public async Task<ToolboxData> LoadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureDirectories();
|
||||
var toolsFileExists = File.Exists(ToolsPath);
|
||||
|
||||
var data = new ToolboxData
|
||||
{
|
||||
Settings = await ReadFileAsync<AppSettings>(SettingsPath, cancellationToken) ?? new AppSettings(),
|
||||
Categories = (await ReadEnvelopeAsync<CategoryItem>(CategoriesPath, cancellationToken)).Items,
|
||||
Tools = (await ReadEnvelopeAsync<ToolItem>(ToolsPath, cancellationToken)).Items,
|
||||
AutoRunEntries = (await ReadEnvelopeAsync<AutoRunEntry>(AutoRunPath, cancellationToken)).Items
|
||||
};
|
||||
|
||||
data.Settings.DataVersion = CurrentDataVersion;
|
||||
SystemToolService.EnsureDefaultCategories(data.Categories);
|
||||
if (!toolsFileExists || data.Tools.Count == 0)
|
||||
{
|
||||
SystemToolService.RestoreDefaultSystemTools(data.Categories, data.Tools);
|
||||
}
|
||||
|
||||
MergeAutoRunEntries(data);
|
||||
|
||||
await SaveAsync(data, cancellationToken);
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(ToolboxData data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureDirectories();
|
||||
data.Settings.DataVersion = CurrentDataVersion;
|
||||
|
||||
await WriteFileAtomicAsync(SettingsPath, data.Settings, cancellationToken);
|
||||
await WriteFileAtomicAsync(CategoriesPath, new DataEnvelope<CategoryItem>
|
||||
{
|
||||
DataVersion = CurrentDataVersion,
|
||||
Items = data.Categories.OrderBy(category => category.SortOrder).ToList()
|
||||
}, cancellationToken);
|
||||
await WriteFileAtomicAsync(ToolsPath, new DataEnvelope<ToolItem>
|
||||
{
|
||||
DataVersion = CurrentDataVersion,
|
||||
Items = data.Tools.OrderBy(tool => tool.SortOrder).ToList()
|
||||
}, cancellationToken);
|
||||
await WriteFileAtomicAsync(AutoRunPath, new DataEnvelope<AutoRunEntry>
|
||||
{
|
||||
DataVersion = CurrentDataVersion,
|
||||
Items = data.AutoRunEntries.OrderBy(entry => entry.SortOrder).ToList()
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ToolboxData> ResetAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
BackupCurrentConfig();
|
||||
|
||||
DeleteIfExists(SettingsPath);
|
||||
DeleteIfExists(CategoriesPath);
|
||||
DeleteIfExists(ToolsPath);
|
||||
DeleteIfExists(AutoRunPath);
|
||||
|
||||
return await LoadAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ExportAsync(string zipPath, ToolboxData data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await SaveAsync(data, cancellationToken);
|
||||
|
||||
if (File.Exists(zipPath))
|
||||
{
|
||||
File.Delete(zipPath);
|
||||
}
|
||||
|
||||
ZipFile.CreateFromDirectory(ConfigDirectory, zipPath, CompressionLevel.Optimal, false);
|
||||
}
|
||||
|
||||
public async Task<ToolboxData> ImportAsync(string sourcePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
BackupCurrentConfig();
|
||||
|
||||
var importDirectory = sourcePath;
|
||||
var tempDirectory = "";
|
||||
if (File.Exists(sourcePath) && Path.GetExtension(sourcePath).Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tempDirectory = Path.Combine(Path.GetTempPath(), "PersonalToolboxImport_" + Guid.NewGuid().ToString("N"));
|
||||
ZipFile.ExtractToDirectory(sourcePath, tempDirectory);
|
||||
importDirectory = tempDirectory;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CopyIfExists(Path.Combine(importDirectory, "appsettings.json"), SettingsPath);
|
||||
CopyIfExists(Path.Combine(importDirectory, "categories.json"), CategoriesPath);
|
||||
CopyIfExists(Path.Combine(importDirectory, "tools.json"), ToolsPath);
|
||||
CopyIfExists(Path.Combine(importDirectory, "autorun.json"), AutoRunPath);
|
||||
|
||||
var importIcons = Path.Combine(importDirectory, "icons");
|
||||
if (Directory.Exists(importIcons))
|
||||
{
|
||||
CopyDirectory(importIcons, IconsDirectory);
|
||||
}
|
||||
|
||||
return await LoadAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tempDirectory) && Directory.Exists(tempDirectory))
|
||||
{
|
||||
Directory.Delete(tempDirectory, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenConfigDirectory()
|
||||
{
|
||||
EnsureDirectories();
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = ConfigDirectory,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
|
||||
public void MergeAutoRunEntries(ToolboxData data)
|
||||
{
|
||||
var existingIds = data.AutoRunEntries.Select(entry => entry.ToolId).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var nextOrder = data.AutoRunEntries.Count == 0 ? 0 : data.AutoRunEntries.Max(entry => entry.SortOrder) + 1;
|
||||
|
||||
foreach (var tool in data.Tools.Where(tool => tool.AutoRunEnabled && !tool.IsDeleted))
|
||||
{
|
||||
if (existingIds.Contains(tool.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
data.AutoRunEntries.Add(new AutoRunEntry
|
||||
{
|
||||
ToolId = tool.Id,
|
||||
Enabled = true,
|
||||
SortOrder = nextOrder++,
|
||||
IntervalAfterMs = 0
|
||||
});
|
||||
}
|
||||
|
||||
var enabledToolIds = data.Tools.Where(tool => tool.AutoRunEnabled && !tool.IsDeleted)
|
||||
.Select(tool => tool.Id)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
data.AutoRunEntries.RemoveAll(entry => !enabledToolIds.Contains(entry.ToolId));
|
||||
}
|
||||
|
||||
private async Task<T?> ReadFileAsync<T>(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
await using var stream = File.OpenRead(path);
|
||||
return await JsonSerializer.DeserializeAsync<T>(stream, _jsonOptions, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<DataEnvelope<T>> ReadEnvelopeAsync<T>(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ReadFileAsync<DataEnvelope<T>>(path, cancellationToken) ?? new DataEnvelope<T> { DataVersion = CurrentDataVersion };
|
||||
}
|
||||
|
||||
private async Task WriteFileAtomicAsync<T>(string path, T value, CancellationToken cancellationToken)
|
||||
{
|
||||
var tempPath = path + ".tmp";
|
||||
await using (var stream = File.Create(tempPath))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, value, _jsonOptions, cancellationToken);
|
||||
}
|
||||
|
||||
File.Move(tempPath, path, true);
|
||||
}
|
||||
|
||||
private void EnsureDirectories()
|
||||
{
|
||||
Directory.CreateDirectory(ConfigDirectory);
|
||||
Directory.CreateDirectory(IconsDirectory);
|
||||
Directory.CreateDirectory(Path.Combine(IconsDirectory, "cache"));
|
||||
Directory.CreateDirectory(Path.Combine(IconsDirectory, "custom"));
|
||||
}
|
||||
|
||||
private void BackupCurrentConfig()
|
||||
{
|
||||
if (!Directory.Exists(ConfigDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var backupRoot = Path.Combine(ConfigDirectory, "backups");
|
||||
Directory.CreateDirectory(backupRoot);
|
||||
var backupDirectory = Path.Combine(backupRoot, DateTime.Now.ToString("yyyyMMdd_HHmmss"));
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
|
||||
foreach (var file in new[] { SettingsPath, CategoriesPath, ToolsPath, AutoRunPath })
|
||||
{
|
||||
if (File.Exists(file))
|
||||
{
|
||||
File.Copy(file, Path.Combine(backupDirectory, Path.GetFileName(file)), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteIfExists(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyIfExists(string source, string target)
|
||||
{
|
||||
if (File.Exists(source))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||
File.Copy(source, target, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string source, string target)
|
||||
{
|
||||
Directory.CreateDirectory(target);
|
||||
foreach (var directory in Directory.EnumerateDirectories(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
Directory.CreateDirectory(directory.Replace(source, target, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var targetPath = file.Replace(source, target, StringComparison.OrdinalIgnoreCase);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
||||
File.Copy(file, targetPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
285
src/PersonalToolbox/Services/HotkeyService.cs
Normal file
285
src/PersonalToolbox/Services/HotkeyService.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class HotkeyService : IDisposable
|
||||
{
|
||||
private const int WmHotkey = 0x0312;
|
||||
private readonly Dictionary<int, string> _registeredHotkeys = new();
|
||||
private HwndSource? _source;
|
||||
private Func<string, Task>? _onTriggered;
|
||||
private Action<LogMessage>? _log;
|
||||
private int _nextId = 1000;
|
||||
private nint _handle;
|
||||
|
||||
public void RegisterAll(
|
||||
Window window,
|
||||
IEnumerable<ToolItem> tools,
|
||||
bool enabled,
|
||||
Func<string, Task> onTriggered,
|
||||
Action<LogMessage> log)
|
||||
{
|
||||
UnregisterAll();
|
||||
|
||||
_onTriggered = onTriggered;
|
||||
_log = log;
|
||||
|
||||
var helper = new WindowInteropHelper(window);
|
||||
_handle = helper.Handle;
|
||||
if (_handle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_source ??= HwndSource.FromHwnd(_handle);
|
||||
_source?.RemoveHook(WndProc);
|
||||
_source?.AddHook(WndProc);
|
||||
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
tool.HotkeyStatus = string.IsNullOrWhiteSpace(tool.Hotkey) ? "未设置" : "等待注册";
|
||||
}
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
foreach (var tool in tools.Where(tool => !string.IsNullOrWhiteSpace(tool.Hotkey)))
|
||||
{
|
||||
tool.HotkeyStatus = "已禁用";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var activeTools = tools
|
||||
.Where(tool => !tool.IsDeleted && !string.IsNullOrWhiteSpace(tool.Hotkey))
|
||||
.ToList();
|
||||
var conflicts = activeTools
|
||||
.GroupBy(tool => HotkeyParser.Normalize(tool.Hotkey!))
|
||||
.Where(group => group.Count() > 1)
|
||||
.SelectMany(group => group)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var conflict in conflicts)
|
||||
{
|
||||
conflict.HotkeyStatus = "内部冲突";
|
||||
log(CreateLog(LogLevel.Warning, $"快捷键内部冲突:{conflict.Hotkey},{conflict.Name}"));
|
||||
}
|
||||
|
||||
foreach (var tool in activeTools.Where(tool => !conflicts.Contains(tool)))
|
||||
{
|
||||
if (!HotkeyParser.TryParse(tool.Hotkey, out var modifiers, out var key))
|
||||
{
|
||||
tool.HotkeyStatus = "格式无效";
|
||||
log(CreateLog(LogLevel.Warning, $"快捷键格式无效:{tool.Name},{tool.Hotkey}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = _nextId++;
|
||||
if (RegisterHotKey(_handle, id, modifiers, key))
|
||||
{
|
||||
_registeredHotkeys[id] = tool.Id;
|
||||
tool.HotkeyStatus = "正常";
|
||||
}
|
||||
else
|
||||
{
|
||||
tool.HotkeyStatus = "系统注册失败";
|
||||
log(CreateLog(LogLevel.Warning, $"快捷键注册失败:{tool.Name},{tool.Hotkey}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterAll()
|
||||
{
|
||||
if (_handle != 0)
|
||||
{
|
||||
foreach (var id in _registeredHotkeys.Keys.ToList())
|
||||
{
|
||||
UnregisterHotKey(_handle, id);
|
||||
}
|
||||
}
|
||||
|
||||
_registeredHotkeys.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
UnregisterAll();
|
||||
_source?.RemoveHook(WndProc);
|
||||
}
|
||||
|
||||
private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WmHotkey && _registeredHotkeys.TryGetValue(wParam.ToInt32(), out var toolId))
|
||||
{
|
||||
handled = true;
|
||||
_ = TriggerAsync(toolId);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task TriggerAsync(string toolId)
|
||||
{
|
||||
if (_onTriggered is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _onTriggered(toolId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.Invoke(CreateLog(LogLevel.Error, $"快捷键触发失败:{ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static LogMessage CreateLog(LogLevel level, string message)
|
||||
{
|
||||
return new LogMessage
|
||||
{
|
||||
Level = level,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool RegisterHotKey(nint hWnd, int id, uint fsModifiers, uint vk);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnregisterHotKey(nint hWnd, int id);
|
||||
}
|
||||
|
||||
public static class HotkeyParser
|
||||
{
|
||||
private const uint ModAlt = 0x0001;
|
||||
private const uint ModControl = 0x0002;
|
||||
private const uint ModShift = 0x0004;
|
||||
private const uint ModWin = 0x0008;
|
||||
|
||||
public static string Normalize(string hotkey)
|
||||
{
|
||||
return string.Join("+", hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(NormalizePart)
|
||||
.Where(part => part.Length > 0)
|
||||
.OrderBy(GetPartOrder)
|
||||
.ThenBy(part => part)
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
public static bool TryParse(string? hotkey, out uint modifiers, out uint key)
|
||||
{
|
||||
modifiers = 0;
|
||||
key = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(hotkey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var keyPart = "";
|
||||
foreach (var rawPart in hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var part = NormalizePart(rawPart);
|
||||
switch (part)
|
||||
{
|
||||
case "Ctrl":
|
||||
modifiers |= ModControl;
|
||||
break;
|
||||
case "Alt":
|
||||
modifiers |= ModAlt;
|
||||
break;
|
||||
case "Shift":
|
||||
modifiers |= ModShift;
|
||||
break;
|
||||
case "Win":
|
||||
modifiers |= ModWin;
|
||||
break;
|
||||
default:
|
||||
keyPart = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers == 0 || string.IsNullOrWhiteSpace(keyPart))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
key = KeyToVirtualKey(keyPart);
|
||||
return key != 0;
|
||||
}
|
||||
|
||||
private static string NormalizePart(string value)
|
||||
{
|
||||
var part = value.Trim();
|
||||
return part.ToLowerInvariant() switch
|
||||
{
|
||||
"control" or "ctrl" => "Ctrl",
|
||||
"menu" or "alt" => "Alt",
|
||||
"shift" => "Shift",
|
||||
"windows" or "win" => "Win",
|
||||
_ => part.Length == 1 ? part.ToUpperInvariant() : part.ToUpperInvariant().StartsWith("F", StringComparison.Ordinal) ? part.ToUpperInvariant() : part
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetPartOrder(string part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
"Ctrl" => 0,
|
||||
"Alt" => 1,
|
||||
"Shift" => 2,
|
||||
"Win" => 3,
|
||||
_ => 4
|
||||
};
|
||||
}
|
||||
|
||||
private static uint KeyToVirtualKey(string keyPart)
|
||||
{
|
||||
if (keyPart.Length == 1)
|
||||
{
|
||||
var character = keyPart[0];
|
||||
if (character is >= 'A' and <= 'Z')
|
||||
{
|
||||
return character;
|
||||
}
|
||||
|
||||
if (character is >= '0' and <= '9')
|
||||
{
|
||||
return character;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyPart.StartsWith("F", StringComparison.OrdinalIgnoreCase)
|
||||
&& int.TryParse(keyPart[1..], out var functionKey)
|
||||
&& functionKey is >= 1 and <= 24)
|
||||
{
|
||||
return (uint)(0x70 + functionKey - 1);
|
||||
}
|
||||
|
||||
return keyPart.ToLowerInvariant() switch
|
||||
{
|
||||
"escape" or "esc" => 0x1B,
|
||||
"tab" => 0x09,
|
||||
"space" => 0x20,
|
||||
"enter" => 0x0D,
|
||||
"backspace" => 0x08,
|
||||
"delete" => 0x2E,
|
||||
"insert" => 0x2D,
|
||||
"home" => 0x24,
|
||||
"end" => 0x23,
|
||||
"pageup" => 0x21,
|
||||
"pagedown" => 0x22,
|
||||
"left" => 0x25,
|
||||
"up" => 0x26,
|
||||
"right" => 0x27,
|
||||
"down" => 0x28,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
126
src/PersonalToolbox/Services/PathValidationService.cs
Normal file
126
src/PersonalToolbox/Services/PathValidationService.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class PathValidationService
|
||||
{
|
||||
public PathValidationReport ValidateTools(IEnumerable<ToolItem> tools)
|
||||
{
|
||||
var issues = new List<PathValidationIssue>();
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
tool.PathInvalid = false;
|
||||
|
||||
if (tool.IsDeleted || tool.Type != ToolType.Local)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tool.LaunchTarget) || !PathExists(tool.LaunchTarget))
|
||||
{
|
||||
tool.PathInvalid = true;
|
||||
issues.Add(CreateIssue(tool, "启动目标", tool.LaunchTarget, string.IsNullOrWhiteSpace(tool.LaunchTarget) ? "启动目标为空" : "启动目标不存在"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tool.WorkingDirectory) && !Directory.Exists(tool.WorkingDirectory))
|
||||
{
|
||||
tool.PathInvalid = true;
|
||||
issues.Add(CreateIssue(tool, "工作目录", tool.WorkingDirectory, "工作目录不存在"));
|
||||
}
|
||||
}
|
||||
|
||||
return new PathValidationReport(issues);
|
||||
}
|
||||
|
||||
public static bool IsValidUrl(string? rawUrl, out string normalizedUrl)
|
||||
{
|
||||
normalizedUrl = "";
|
||||
if (string.IsNullOrWhiteSpace(rawUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var value = rawUrl.Trim();
|
||||
if (!value.Contains("://", StringComparison.Ordinal))
|
||||
{
|
||||
value = "https://" + value;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
normalizedUrl = uri.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool PathExists(string path)
|
||||
{
|
||||
return File.Exists(path) || Directory.Exists(path);
|
||||
}
|
||||
|
||||
private static PathValidationIssue CreateIssue(ToolItem tool, string fieldName, string? path, string reason)
|
||||
{
|
||||
return new PathValidationIssue
|
||||
{
|
||||
ToolId = tool.Id,
|
||||
ToolName = tool.Name,
|
||||
FieldName = fieldName,
|
||||
Path = path ?? "",
|
||||
Reason = reason
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PathValidationReport
|
||||
{
|
||||
public PathValidationReport(IReadOnlyList<PathValidationIssue> issues)
|
||||
{
|
||||
Issues = issues;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PathValidationIssue> Issues { get; }
|
||||
public bool HasIssues => Issues.Count > 0;
|
||||
public int InvalidToolCount => Issues.Select(issue => issue.ToolId).Distinct(StringComparer.OrdinalIgnoreCase).Count();
|
||||
|
||||
public string ToStatusText(int maxIssues = 8)
|
||||
{
|
||||
if (!HasIssues)
|
||||
{
|
||||
return "路径检查完成:未发现失效工具。";
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"路径检查完成:发现 {InvalidToolCount} 个失效工具,{Issues.Count} 个问题。");
|
||||
|
||||
foreach (var issue in Issues.Take(maxIssues))
|
||||
{
|
||||
var pathText = string.IsNullOrWhiteSpace(issue.Path) ? "" : $"({issue.Path})";
|
||||
builder.AppendLine($"- {issue.ToolName}:{issue.FieldName} {issue.Reason}{pathText}");
|
||||
}
|
||||
|
||||
if (Issues.Count > maxIssues)
|
||||
{
|
||||
builder.AppendLine($"还有 {Issues.Count - maxIssues} 个问题未显示,请逐项检查本地工具路径。");
|
||||
}
|
||||
|
||||
return builder.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PathValidationIssue
|
||||
{
|
||||
public string ToolId { get; init; } = "";
|
||||
public string ToolName { get; init; } = "";
|
||||
public string FieldName { get; init; } = "";
|
||||
public string Path { get; init; } = "";
|
||||
public string Reason { get; init; } = "";
|
||||
}
|
||||
31
src/PersonalToolbox/Services/StartupService.cs
Normal file
31
src/PersonalToolbox/Services/StartupService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class StartupService
|
||||
{
|
||||
private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string RunValueName = "PersonalToolbox";
|
||||
|
||||
public bool IsEnabled()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath, false);
|
||||
return key?.GetValue(RunValueName) is string value && !string.IsNullOrWhiteSpace(value);
|
||||
}
|
||||
|
||||
public void SetEnabled(bool enabled)
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath, true)
|
||||
?? Registry.CurrentUser.CreateSubKey(RunKeyPath, true);
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
var exePath = Environment.ProcessPath ?? "";
|
||||
key.SetValue(RunValueName, $"\"{exePath}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
key.DeleteValue(RunValueName, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/PersonalToolbox/Services/SystemToolService.cs
Normal file
101
src/PersonalToolbox/Services/SystemToolService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public static class SystemToolService
|
||||
{
|
||||
public const string SystemCategoryId = "category-system-tools";
|
||||
public const string UncategorizedCategoryId = "category-uncategorized";
|
||||
|
||||
public static void EnsureDefaultCategories(ICollection<CategoryItem> categories)
|
||||
{
|
||||
if (categories.All(category => category.Id != SystemCategoryId))
|
||||
{
|
||||
categories.Add(new CategoryItem
|
||||
{
|
||||
Id = SystemCategoryId,
|
||||
Name = "系统工具",
|
||||
IconKey = "system",
|
||||
SortOrder = 0,
|
||||
IsBuiltIn = true
|
||||
});
|
||||
}
|
||||
|
||||
if (categories.All(category => category.Id != UncategorizedCategoryId))
|
||||
{
|
||||
categories.Add(new CategoryItem
|
||||
{
|
||||
Id = UncategorizedCategoryId,
|
||||
Name = "未分类",
|
||||
IconKey = "category",
|
||||
SortOrder = 999,
|
||||
IsBuiltIn = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static int RestoreDefaultSystemTools(ICollection<CategoryItem> categories, ICollection<ToolItem> tools)
|
||||
{
|
||||
EnsureDefaultCategories(categories);
|
||||
|
||||
var added = 0;
|
||||
foreach (var definition in GetDefaultDefinitions())
|
||||
{
|
||||
var exists = tools.Any(tool => tool.Type == ToolType.System && tool.SystemToolKey == definition.SystemToolKey && !tool.IsDeleted);
|
||||
if (exists)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tools.Add(definition);
|
||||
added++;
|
||||
}
|
||||
|
||||
ReorderTools(tools);
|
||||
return added;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ToolItem> GetDefaultDefinitions()
|
||||
{
|
||||
return
|
||||
[
|
||||
Create("system-notepad", "记事本", "快速打开 Windows 记事本。", "notepad.exe", "notepad", 0),
|
||||
Create("system-calculator", "计算器", "快速打开 Windows 计算器。", "calc.exe", "calculator", 1),
|
||||
Create("system-task-manager", "任务管理器", "查看和管理正在运行的应用与进程。", "taskmgr.exe", "taskmgr", 2),
|
||||
Create("system-control-panel", "控制面板", "打开传统控制面板。", "control", "control", 3),
|
||||
Create("system-settings", "系统设置", "打开 Windows 设置主页。", "ms-settings:", "settings", 4),
|
||||
Create("system-windows-update", "Windows 更新", "打开 Windows 更新设置页。", "ms-settings:windowsupdate", "settings", 5),
|
||||
Create("system-device-manager", "设备管理器", "查看和管理硬件设备。", "devmgmt.msc", "device", 6),
|
||||
Create("system-disk-management", "磁盘管理", "打开磁盘和分区管理工具。", "diskmgmt.msc", "disk", 7),
|
||||
Create("system-services", "服务", "查看和管理 Windows 服务。", "services.msc", "service", 8),
|
||||
Create("system-registry-editor", "注册表编辑器", "打开注册表编辑器,需要谨慎使用。", "regedit.exe", "registry", 9),
|
||||
Create("system-network-connections", "网络连接", "打开网络适配器列表。", "ncpa.cpl", "network", 10),
|
||||
Create("system-apps-folder", "应用列表", "打开 shell:AppsFolder 以查看 Store 应用和快捷方式。", "shell:AppsFolder", "apps", 11)
|
||||
];
|
||||
}
|
||||
|
||||
private static ToolItem Create(string key, string name, string description, string target, string iconKey, int order)
|
||||
{
|
||||
return new ToolItem
|
||||
{
|
||||
Id = key,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Type = ToolType.System,
|
||||
CategoryId = SystemCategoryId,
|
||||
IconKey = iconKey,
|
||||
LaunchTarget = target,
|
||||
SystemToolKey = key,
|
||||
SortOrder = order
|
||||
};
|
||||
}
|
||||
|
||||
private static void ReorderTools(IEnumerable<ToolItem> tools)
|
||||
{
|
||||
var order = 0;
|
||||
foreach (var tool in tools.OrderBy(tool => tool.CategoryId).ThenBy(tool => tool.SortOrder).ThenBy(tool => tool.Name))
|
||||
{
|
||||
tool.SortOrder = order++;
|
||||
}
|
||||
}
|
||||
}
|
||||
251
src/PersonalToolbox/Services/ToolLaunchService.cs
Normal file
251
src/PersonalToolbox/Services/ToolLaunchService.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class ToolLaunchService
|
||||
{
|
||||
public async Task<LaunchResult> LaunchAsync(
|
||||
ToolItem tool,
|
||||
IReadOnlyCollection<ToolItem> allTools,
|
||||
Action<LogMessage> log,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (tool.IsDeleted)
|
||||
{
|
||||
var result = LaunchResult.Fail(tool, LaunchResultKind.Skipped, "工具已删除。");
|
||||
log(CreateLog(LogLevel.Warning, $"跳过已删除工具:{tool.Name}"));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (tool.Type == ToolType.Combination)
|
||||
{
|
||||
return await LaunchCombinationAsync(tool, allTools, log, cancellationToken);
|
||||
}
|
||||
|
||||
return LaunchSingleTool(tool, log);
|
||||
}
|
||||
|
||||
private async Task<LaunchResult> LaunchCombinationAsync(
|
||||
ToolItem combination,
|
||||
IReadOnlyCollection<ToolItem> allTools,
|
||||
Action<LogMessage> log,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = ValidateCombination(combination, allTools);
|
||||
if (!validation.Success)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"组合校验失败:{combination.Name},{validation.ErrorMessage}"));
|
||||
return validation;
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, $"启动组合:{combination.Name}"));
|
||||
var successCount = 0;
|
||||
var failedCount = 0;
|
||||
var members = combination.Combination?.Members
|
||||
.Where(member => member.Enabled)
|
||||
.OrderBy(member => member.SortOrder)
|
||||
.ToList() ?? [];
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var memberTool = allTools.FirstOrDefault(tool => tool.Id == member.ToolId && !tool.IsDeleted);
|
||||
if (memberTool is null)
|
||||
{
|
||||
failedCount++;
|
||||
log(CreateLog(LogLevel.Error, $"组合成员不存在:{member.ToolId}"));
|
||||
if (combination.Combination?.FailurePolicy == FailurePolicy.Stop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = await LaunchAsync(memberTool, allTools, log, cancellationToken);
|
||||
if (result.Success)
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
failedCount++;
|
||||
if (combination.Combination?.FailurePolicy == FailurePolicy.Stop)
|
||||
{
|
||||
log(CreateLog(LogLevel.Warning, $"组合已停止:{combination.Name}"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (member.IntervalAfterMs > 0)
|
||||
{
|
||||
await Task.Delay(member.IntervalAfterMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, $"组合执行完成:{combination.Name},成功 {successCount} 项,失败 {failedCount} 项"));
|
||||
return failedCount == 0
|
||||
? LaunchResult.Ok(combination)
|
||||
: LaunchResult.Fail(combination, LaunchResultKind.UnknownError, $"组合执行完成,但有 {failedCount} 项失败。");
|
||||
}
|
||||
|
||||
public LaunchResult ValidateCombination(ToolItem combination, IReadOnlyCollection<ToolItem> allTools)
|
||||
{
|
||||
if (combination.Type != ToolType.Combination)
|
||||
{
|
||||
return LaunchResult.Ok(combination);
|
||||
}
|
||||
|
||||
var stack = new Stack<string>();
|
||||
var finalToolIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var toolMap = allTools.Where(tool => !tool.IsDeleted).ToDictionary(tool => tool.Id, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
LaunchResult? failure = null;
|
||||
|
||||
void Visit(ToolItem current)
|
||||
{
|
||||
if (failure is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.Type != ToolType.Combination)
|
||||
{
|
||||
if (!finalToolIds.Add(current.Id))
|
||||
{
|
||||
failure = LaunchResult.Fail(combination, LaunchResultKind.DuplicateTool, $"展开后存在重复工具:{current.Name}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (stack.Contains(current.Id, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var path = string.Join(" -> ", stack.Reverse().Concat([current.Id]).Select(id => toolMap.TryGetValue(id, out var tool) ? tool.Name : id));
|
||||
failure = LaunchResult.Fail(combination, LaunchResultKind.CircularReference, $"存在循环引用:{path}");
|
||||
return;
|
||||
}
|
||||
|
||||
stack.Push(current.Id);
|
||||
var members = current.Combination?.Members
|
||||
.Where(member => member.Enabled)
|
||||
.OrderBy(member => member.SortOrder)
|
||||
?? Enumerable.Empty<CombinationMember>();
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (!toolMap.TryGetValue(member.ToolId, out var memberTool))
|
||||
{
|
||||
failure = LaunchResult.Fail(combination, LaunchResultKind.MissingReference, $"成员引用不存在:{member.ToolId}");
|
||||
break;
|
||||
}
|
||||
|
||||
Visit(memberTool);
|
||||
}
|
||||
|
||||
stack.Pop();
|
||||
}
|
||||
|
||||
Visit(combination);
|
||||
return failure ?? LaunchResult.Ok(combination);
|
||||
}
|
||||
|
||||
private LaunchResult LaunchSingleTool(ToolItem tool, Action<LogMessage> log)
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = CreateStartInfo(tool, out var validationFailure);
|
||||
if (validationFailure is not null)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"启动失败:{tool.Name},{validationFailure.ErrorMessage}"));
|
||||
return validationFailure;
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, $"开始启动工具:{tool.Name}"));
|
||||
Process.Start(startInfo!);
|
||||
log(CreateLog(LogLevel.Success, $"启动成功:{tool.Name}"));
|
||||
return LaunchResult.Ok(tool);
|
||||
}
|
||||
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
||||
{
|
||||
log(CreateLog(LogLevel.Warning, $"用户取消管理员权限:{tool.Name}"));
|
||||
return LaunchResult.Fail(tool, LaunchResultKind.UserCancelledElevation, "用户取消管理员权限。");
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"访问被拒绝:{tool.Name},{ex.Message}"));
|
||||
return LaunchResult.Fail(tool, LaunchResultKind.AccessDenied, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"启动失败:{tool.Name},{ex.Message}"));
|
||||
return LaunchResult.Fail(tool, LaunchResultKind.ProcessStartFailed, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessStartInfo? CreateStartInfo(ToolItem tool, out LaunchResult? validationFailure)
|
||||
{
|
||||
validationFailure = null;
|
||||
|
||||
if (tool.Type == ToolType.Url)
|
||||
{
|
||||
if (!PathValidationService.IsValidUrl(tool.Url, out var normalizedUrl))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.InvalidUrl, "网址格式无效。");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProcessStartInfo
|
||||
{
|
||||
FileName = normalizedUrl,
|
||||
UseShellExecute = true
|
||||
};
|
||||
}
|
||||
|
||||
var target = tool.LaunchTarget;
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.PathMissing, "启动目标为空。");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tool.Type == ToolType.Local && !File.Exists(target) && !Directory.Exists(target))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.PathMissing, "路径不存在。");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tool.WorkingDirectory) && !Directory.Exists(tool.WorkingDirectory))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.PathMissing, "工作目录不存在。");
|
||||
return null;
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = target,
|
||||
Arguments = tool.Arguments ?? "",
|
||||
WorkingDirectory = tool.WorkingDirectory ?? "",
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
if (tool.RunAsAdmin)
|
||||
{
|
||||
startInfo.Verb = "runas";
|
||||
}
|
||||
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
private static LogMessage CreateLog(LogLevel level, string message)
|
||||
{
|
||||
return new LogMessage
|
||||
{
|
||||
Level = level,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
89
src/PersonalToolbox/Services/TrayService.cs
Normal file
89
src/PersonalToolbox/Services/TrayService.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using Forms = System.Windows.Forms;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class TrayService : IDisposable
|
||||
{
|
||||
private readonly Window _window;
|
||||
private readonly Action _openSettings;
|
||||
private readonly Action<bool> _setHotkeysEnabled;
|
||||
private readonly Func<bool> _getHotkeysEnabled;
|
||||
private readonly Action _requestExit;
|
||||
private readonly Forms.NotifyIcon _notifyIcon;
|
||||
private readonly Forms.ToolStripMenuItem _toggleWindowItem;
|
||||
private readonly Forms.ToolStripMenuItem _toggleHotkeysItem;
|
||||
|
||||
public TrayService(
|
||||
Window window,
|
||||
Action openSettings,
|
||||
Action<bool> setHotkeysEnabled,
|
||||
Func<bool> getHotkeysEnabled,
|
||||
Action requestExit)
|
||||
{
|
||||
_window = window;
|
||||
_openSettings = openSettings;
|
||||
_setHotkeysEnabled = setHotkeysEnabled;
|
||||
_getHotkeysEnabled = getHotkeysEnabled;
|
||||
_requestExit = requestExit;
|
||||
|
||||
_toggleWindowItem = new Forms.ToolStripMenuItem("显示 / 隐藏主界面", null, (_, _) => ToggleWindow());
|
||||
_toggleHotkeysItem = new Forms.ToolStripMenuItem("全局快捷键:开启", null, (_, _) => ToggleHotkeys());
|
||||
var settingsItem = new Forms.ToolStripMenuItem("设置", null, (_, _) => _openSettings());
|
||||
var exitItem = new Forms.ToolStripMenuItem("退出", null, (_, _) => _requestExit());
|
||||
|
||||
_notifyIcon = new Forms.NotifyIcon
|
||||
{
|
||||
Icon = SystemIcons.Application,
|
||||
Text = "个人工具箱",
|
||||
Visible = true,
|
||||
ContextMenuStrip = new Forms.ContextMenuStrip()
|
||||
};
|
||||
_notifyIcon.ContextMenuStrip.Items.AddRange([_toggleWindowItem, _toggleHotkeysItem, settingsItem, exitItem]);
|
||||
_notifyIcon.MouseClick += NotifyIconOnMouseClick;
|
||||
RefreshMenuText();
|
||||
}
|
||||
|
||||
public void RefreshMenuText()
|
||||
{
|
||||
_toggleWindowItem.Text = _window.IsVisible ? "隐藏主界面" : "显示主界面";
|
||||
_toggleHotkeysItem.Text = _getHotkeysEnabled() ? "全局快捷键:关闭" : "全局快捷键:开启";
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
if (_window.IsVisible)
|
||||
{
|
||||
_window.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.Show();
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
RefreshMenuText();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_notifyIcon.Visible = false;
|
||||
_notifyIcon.MouseClick -= NotifyIconOnMouseClick;
|
||||
_notifyIcon.Dispose();
|
||||
}
|
||||
|
||||
private void ToggleHotkeys()
|
||||
{
|
||||
_setHotkeysEnabled(!_getHotkeysEnabled());
|
||||
RefreshMenuText();
|
||||
}
|
||||
|
||||
private void NotifyIconOnMouseClick(object? sender, Forms.MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == Forms.MouseButtons.Left)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/PersonalToolbox/ViewModels/CombinationMemberViewModel.cs
Normal file
55
src/PersonalToolbox/ViewModels/CombinationMemberViewModel.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.ViewModels;
|
||||
|
||||
public sealed class CombinationMemberViewModel : ObservableObject
|
||||
{
|
||||
private readonly Func<string, ToolItem?> _toolResolver;
|
||||
private bool _enabled;
|
||||
private int _intervalAfterMs;
|
||||
|
||||
public CombinationMemberViewModel(CombinationMember member, Func<string, ToolItem?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
568
src/PersonalToolbox/ViewModels/MainWindowViewModel.cs
Normal file
568
src/PersonalToolbox/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,568 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using PersonalToolbox.Commands;
|
||||
using PersonalToolbox.Models;
|
||||
using PersonalToolbox.Services;
|
||||
using PersonalToolbox.Views;
|
||||
using Clipboard = System.Windows.Clipboard;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace PersonalToolbox.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<CategoryItem> Categories { get; } = [];
|
||||
public ObservableCollection<ToolCardViewModel> Tools { get; } = [];
|
||||
public ObservableCollection<LogMessage> 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 pathReport = _pathValidationService.ValidateTools(_data.Tools);
|
||||
RefreshCategories();
|
||||
RefreshTools();
|
||||
|
||||
AddLog(LogLevel.Info, $"配置目录:{_configurationService.ConfigDirectory}");
|
||||
AddLog(LogLevel.Success, "个人工具箱已启动。");
|
||||
if (pathReport.HasIssues)
|
||||
{
|
||||
LogPathValidationReport(pathReport);
|
||||
}
|
||||
|
||||
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 void LogPathValidationReport(PathValidationReport report)
|
||||
{
|
||||
AddLog(LogLevel.Warning, $"路径检查完成:发现 {report.InvalidToolCount} 个失效工具,{report.Issues.Count} 个问题。");
|
||||
|
||||
foreach (var issue in report.Issues.Take(8))
|
||||
{
|
||||
var pathText = string.IsNullOrWhiteSpace(issue.Path) ? "" : $",{issue.Path}";
|
||||
AddLog(LogLevel.Warning, $"路径失效:{issue.ToolName},{issue.FieldName} {issue.Reason}{pathText}");
|
||||
}
|
||||
|
||||
if (report.Issues.Count > 8)
|
||||
{
|
||||
AddLog(LogLevel.Warning, $"还有 {report.Issues.Count - 8} 个路径问题未显示,可在设置中查看完整报告。");
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
26
src/PersonalToolbox/ViewModels/ObservableObject.cs
Normal file
26
src/PersonalToolbox/ViewModels/ObservableObject.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace PersonalToolbox.ViewModels;
|
||||
|
||||
public abstract class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(storage, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
storage = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
124
src/PersonalToolbox/ViewModels/ToolCardViewModel.cs
Normal file
124
src/PersonalToolbox/ViewModels/ToolCardViewModel.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.ViewModels;
|
||||
|
||||
public sealed class ToolCardViewModel : ObservableObject
|
||||
{
|
||||
private readonly Func<string, string> _categoryNameResolver;
|
||||
|
||||
public ToolCardViewModel(ToolItem tool, Func<string, string> 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<string> StatusBadges
|
||||
{
|
||||
get
|
||||
{
|
||||
var badges = new List<string>();
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
166
src/PersonalToolbox/Views/CombinationEditorWindow.xaml
Normal file
166
src/PersonalToolbox/Views/CombinationEditorWindow.xaml
Normal file
@@ -0,0 +1,166 @@
|
||||
<Window x:Class="PersonalToolbox.Views.CombinationEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="编辑组合"
|
||||
Width="760"
|
||||
Height="640"
|
||||
MinWidth="680"
|
||||
MinHeight="560"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="96" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="96" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="名称" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="NameTextBox"
|
||||
Grid.Column="1"
|
||||
MinHeight="32"
|
||||
Margin="0,0,16,8"
|
||||
ToolTip="组合卡片显示名称。" />
|
||||
|
||||
<TextBlock Grid.Column="2" Text="分类" VerticalAlignment="Center" />
|
||||
<ComboBox x:Name="CategoryComboBox"
|
||||
Grid.Column="3"
|
||||
MinHeight="32"
|
||||
Margin="0,0,0,8"
|
||||
DisplayMemberPath="Name"
|
||||
SelectedValuePath="Id"
|
||||
ToolTip="组合也作为一种工具,只能属于一个一级分类。" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="说明" VerticalAlignment="Top" Margin="0,8,0,0" />
|
||||
<TextBox x:Name="DescriptionTextBox"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
MinHeight="58"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
ToolTip="描述这个组合会打开哪些环境或工具。" />
|
||||
|
||||
<TextBlock Grid.Row="2" Text="快捷键" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="HotkeyTextBox"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
MinHeight="32"
|
||||
Margin="0,8,16,0"
|
||||
ToolTip="格式示例:Ctrl + Alt + D。组合可被全局快捷键直接启动。" />
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Orientation="Horizontal"
|
||||
Margin="0,8,0,0">
|
||||
<CheckBox x:Name="AutoRunCheckBox"
|
||||
Content="工具箱启动时自动运行"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="开启后组合会出现在自动运行列表中。" />
|
||||
<ComboBox x:Name="FailurePolicyComboBox"
|
||||
Width="180"
|
||||
Margin="18,0,0,0"
|
||||
ToolTip="设置成员启动失败后继续执行,还是停止整个组合。">
|
||||
<ComboBoxItem Content="失败后继续" Tag="Continue" />
|
||||
<ComboBoxItem Content="失败后停止" Tag="Stop" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,16,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ComboBox x:Name="ToolPickerComboBox"
|
||||
Width="300"
|
||||
DisplayMemberPath="Name"
|
||||
ToolTip="从已有工具或组合中选择一个成员。" />
|
||||
<Button Content="添加成员"
|
||||
Width="96"
|
||||
Margin="8,0,0,0"
|
||||
ToolTip="把选择的工具加入组合成员列表。"
|
||||
Click="AddMemberButton_OnClick" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Content="上移"
|
||||
Width="70"
|
||||
Margin="0,0,8,0"
|
||||
ToolTip="将选中成员提前执行。"
|
||||
Click="MoveUpButton_OnClick" />
|
||||
<Button Content="下移"
|
||||
Width="70"
|
||||
Margin="0,0,8,0"
|
||||
ToolTip="将选中成员延后执行。"
|
||||
Click="MoveDownButton_OnClick" />
|
||||
<Button Content="移除"
|
||||
Width="70"
|
||||
ToolTip="从组合中移除选中成员。"
|
||||
Click="RemoveMemberButton_OnClick" />
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid x:Name="MembersDataGrid"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,10,0,0"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
SelectionMode="Single"
|
||||
ToolTip="组合成员会按列表顺序启动,可临时禁用成员并设置执行后的等待间隔。">
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Header="启用"
|
||||
Width="70"
|
||||
Binding="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<DataGridTextColumn Header="名称"
|
||||
Width="*"
|
||||
IsReadOnly="True"
|
||||
Binding="{Binding ToolName}" />
|
||||
<DataGridTextColumn Header="类型"
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
Binding="{Binding ToolTypeLabel}" />
|
||||
<DataGridTextColumn Header="间隔(ms)"
|
||||
Width="110"
|
||||
Binding="{Binding IntervalAfterMs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,14,0,0">
|
||||
<Button Content="保存"
|
||||
Width="88"
|
||||
Margin="0,0,8,0"
|
||||
IsDefault="True"
|
||||
ToolTip="校验循环引用和重复工具后保存组合。"
|
||||
Click="SaveButton_OnClick" />
|
||||
<Button Content="取消"
|
||||
Width="88"
|
||||
IsCancel="True"
|
||||
ToolTip="放弃本次组合修改。"
|
||||
Click="CancelButton_OnClick" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
189
src/PersonalToolbox/Views/CombinationEditorWindow.xaml.cs
Normal file
189
src/PersonalToolbox/Views/CombinationEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using PersonalToolbox.Models;
|
||||
using PersonalToolbox.Services;
|
||||
using PersonalToolbox.ViewModels;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace PersonalToolbox.Views;
|
||||
|
||||
public partial class CombinationEditorWindow : Window
|
||||
{
|
||||
private readonly ToolItem _tool;
|
||||
private readonly IReadOnlyList<ToolItem> _allTools;
|
||||
private readonly ToolLaunchService _launchService;
|
||||
private readonly ObservableCollection<CombinationMemberViewModel> _members = [];
|
||||
|
||||
private CombinationEditorWindow(
|
||||
ToolItem tool,
|
||||
IEnumerable<CategoryItem> categories,
|
||||
IEnumerable<ToolItem> 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<CategoryItem> categories,
|
||||
IEnumerable<ToolItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/PersonalToolbox/Views/PromptWindow.xaml
Normal file
45
src/PersonalToolbox/Views/PromptWindow.xaml
Normal file
@@ -0,0 +1,45 @@
|
||||
<Window x:Class="PersonalToolbox.Views.PromptWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="输入"
|
||||
Width="380"
|
||||
Height="180"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock x:Name="PromptText"
|
||||
FontSize="14"
|
||||
TextWrapping="Wrap"
|
||||
ToolTip="当前需要输入的信息名称。" />
|
||||
|
||||
<TextBox x:Name="ValueTextBox"
|
||||
Grid.Row="1"
|
||||
Margin="0,12,0,0"
|
||||
MinHeight="32"
|
||||
ToolTip="请输入名称,按 Enter 确认。"
|
||||
KeyDown="ValueTextBox_OnKeyDown" />
|
||||
|
||||
<StackPanel Grid.Row="3"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<Button Content="确定"
|
||||
Width="82"
|
||||
Margin="0,0,8,0"
|
||||
IsDefault="True"
|
||||
ToolTip="保存输入内容并关闭窗口。"
|
||||
Click="OkButton_OnClick" />
|
||||
<Button Content="取消"
|
||||
Width="82"
|
||||
IsCancel="True"
|
||||
ToolTip="放弃本次输入。"
|
||||
Click="CancelButton_OnClick" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
47
src/PersonalToolbox/Views/PromptWindow.xaml.cs
Normal file
47
src/PersonalToolbox/Views/PromptWindow.xaml.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace PersonalToolbox.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
196
src/PersonalToolbox/Views/SettingsWindow.xaml
Normal file
196
src/PersonalToolbox/Views/SettingsWindow.xaml
Normal file
@@ -0,0 +1,196 @@
|
||||
<Window x:Class="PersonalToolbox.Views.SettingsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="设置"
|
||||
Width="820"
|
||||
Height="620"
|
||||
MinWidth="740"
|
||||
MinHeight="540"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TabControl>
|
||||
<TabItem Header="常规" ToolTip="管理窗口、托盘和快捷键总开关。">
|
||||
<StackPanel Margin="18">
|
||||
<CheckBox x:Name="StartHiddenCheckBox"
|
||||
Content="启动后默认隐藏到托盘"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="开启后应用启动时不显示主窗口,只保留托盘图标。" />
|
||||
<CheckBox x:Name="HideOnCloseCheckBox"
|
||||
Content="关闭窗口时隐藏到托盘"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="开启后点击窗口关闭按钮不会退出应用。" />
|
||||
<CheckBox x:Name="ConfirmExitCheckBox"
|
||||
Content="退出前确认"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="通过托盘菜单退出时是否显示确认提示。" />
|
||||
<CheckBox x:Name="LogExpandedCheckBox"
|
||||
Content="默认展开底部信息区"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="控制主界面底部日志区域的默认展开状态。" />
|
||||
<CheckBox x:Name="GlobalHotkeysCheckBox"
|
||||
Content="启用全局快捷键"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="关闭后所有已配置快捷键临时失效,但配置不会删除。" />
|
||||
<CheckBox x:Name="StartupCheckBox"
|
||||
Content="开机自启"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="使用当前用户注册表启动项,不需要管理员权限。" />
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="自动运行" ToolTip="动态汇总所有开启启动时自动运行的工具。">
|
||||
<Grid Margin="18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<DataGrid x:Name="AutoRunDataGrid"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
SelectionMode="Single"
|
||||
ToolTip="自动运行项来自工具自身设置,可调整顺序、启用状态和间隔。">
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Header="启用"
|
||||
Width="70"
|
||||
Binding="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<DataGridTextColumn Header="顺序"
|
||||
Width="70"
|
||||
Binding="{Binding SortOrder, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<DataGridTextColumn Header="工具"
|
||||
Width="*"
|
||||
IsReadOnly="True"
|
||||
Binding="{Binding ToolName}" />
|
||||
<DataGridTextColumn Header="类型"
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
Binding="{Binding ToolTypeLabel}" />
|
||||
<DataGridTextColumn Header="间隔(ms)"
|
||||
Width="110"
|
||||
Binding="{Binding IntervalAfterMs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,10,0,0">
|
||||
<Button Content="上移"
|
||||
Width="76"
|
||||
Margin="0,0,8,0"
|
||||
ToolTip="提高选中自动运行项的执行顺序。"
|
||||
Click="AutoRunMoveUpButton_OnClick" />
|
||||
<Button Content="下移"
|
||||
Width="76"
|
||||
Margin="0,0,8,0"
|
||||
ToolTip="降低选中自动运行项的执行顺序。"
|
||||
Click="AutoRunMoveDownButton_OnClick" />
|
||||
<Button Content="移除"
|
||||
Width="76"
|
||||
ToolTip="从自动运行中移除,等价于关闭该工具的自动运行设置。"
|
||||
Click="AutoRunRemoveButton_OnClick" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="快捷键" ToolTip="查看和编辑工具快捷键状态。">
|
||||
<Grid Margin="18">
|
||||
<DataGrid x:Name="HotkeysDataGrid"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
ToolTip="快捷键状态会在主窗口注册后更新;可直接编辑快捷键文本。">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="快捷键"
|
||||
Width="160"
|
||||
Binding="{Binding Hotkey, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<DataGridTextColumn Header="工具"
|
||||
Width="*"
|
||||
IsReadOnly="True"
|
||||
Binding="{Binding Name}" />
|
||||
<DataGridTextColumn Header="类型"
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
Binding="{Binding Type}" />
|
||||
<DataGridTextColumn Header="状态"
|
||||
Width="130"
|
||||
IsReadOnly="True"
|
||||
Binding="{Binding HotkeyStatus}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="数据" ToolTip="打开、导入、导出、重置和检查本地配置。">
|
||||
<StackPanel Margin="18">
|
||||
<Button Content="打开配置目录"
|
||||
Width="160"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="在资源管理器中打开 %AppData%\PersonalToolbox。"
|
||||
Click="OpenConfigButton_OnClick" />
|
||||
<Button Content="导出全部配置"
|
||||
Width="160"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="导出 appsettings、categories、tools、autorun 和 icons 目录为 zip。"
|
||||
Click="ExportButton_OnClick" />
|
||||
<Button Content="导入配置"
|
||||
Width="160"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="从 zip 导入配置,导入后会建议检查自动运行和快捷键。"
|
||||
Click="ImportButton_OnClick" />
|
||||
<Button Content="检查路径失效"
|
||||
Width="160"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,10"
|
||||
ToolTip="检查本地工具路径和工作目录是否仍然存在。"
|
||||
Click="ValidatePathsButton_OnClick" />
|
||||
<Button Content="重置配置"
|
||||
Width="160"
|
||||
HorizontalAlignment="Left"
|
||||
ToolTip="备份当前配置后恢复默认配置。"
|
||||
Click="ResetButton_OnClick" />
|
||||
<TextBlock x:Name="DataStatusTextBlock"
|
||||
Margin="0,18,0,0"
|
||||
TextWrapping="Wrap"
|
||||
ToolTip="显示最近一次配置操作结果。" />
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="系统工具" ToolTip="恢复默认 Windows 系统工具。">
|
||||
<StackPanel Margin="18">
|
||||
<Button Content="恢复默认系统工具"
|
||||
Width="180"
|
||||
HorizontalAlignment="Left"
|
||||
ToolTip="只补回缺失的默认系统工具,不重复创建已有项。"
|
||||
Click="RestoreSystemToolsButton_OnClick" />
|
||||
<TextBlock x:Name="SystemToolsStatusTextBlock"
|
||||
Margin="0,18,0,0"
|
||||
TextWrapping="Wrap"
|
||||
ToolTip="显示默认系统工具恢复结果。" />
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,14,0,0">
|
||||
<Button Content="保存"
|
||||
Width="88"
|
||||
Margin="0,0,8,0"
|
||||
IsDefault="True"
|
||||
ToolTip="保存设置并关闭窗口。"
|
||||
Click="SaveButton_OnClick" />
|
||||
<Button Content="取消"
|
||||
Width="88"
|
||||
IsCancel="True"
|
||||
ToolTip="关闭设置窗口。已执行的导入、导出和重置操作不会回滚。"
|
||||
Click="CancelButton_OnClick" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
304
src/PersonalToolbox/Views/SettingsWindow.xaml.cs
Normal file
304
src/PersonalToolbox/Views/SettingsWindow.xaml.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using PersonalToolbox.Models;
|
||||
using PersonalToolbox.Services;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
||||
using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
|
||||
|
||||
namespace PersonalToolbox.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<AutoRunRow> _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 = $"PersonalToolbox_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);
|
||||
var report = _pathValidationService.ValidateTools(_data.Tools);
|
||||
LoadControls();
|
||||
DataStatusTextBlock.Text = report.HasIssues
|
||||
? $"配置导入完成。建议检查自动运行列表和快捷键,确认其中没有不需要启动的工具。{Environment.NewLine}{report.ToStatusText(int.MaxValue)}"
|
||||
: "配置导入完成。建议检查自动运行列表和快捷键,确认其中没有不需要启动的工具。路径检查未发现失效工具。";
|
||||
}
|
||||
|
||||
private void ValidatePathsButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var report = _pathValidationService.ValidateTools(_data.Tools);
|
||||
DataStatusTextBlock.Text = report.ToStatusText(int.MaxValue);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
src/PersonalToolbox/Views/ToolEditorWindow.xaml
Normal file
154
src/PersonalToolbox/Views/ToolEditorWindow.xaml
Normal file
@@ -0,0 +1,154 @@
|
||||
<Window x:Class="PersonalToolbox.Views.ToolEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="编辑工具"
|
||||
Width="560"
|
||||
Height="560"
|
||||
MinWidth="520"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="112" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="类型" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="TypeTextBlock"
|
||||
Grid.Column="1"
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="工具类型由创建入口决定,用户侧只暴露系统、本地、网址和组合。" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="名称" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="NameTextBox"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0"
|
||||
MinHeight="32"
|
||||
ToolTip="卡片显示名称,建议简短清晰。" />
|
||||
|
||||
<TextBlock Grid.Row="2" Text="分类" VerticalAlignment="Center" />
|
||||
<ComboBox x:Name="CategoryComboBox"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0"
|
||||
MinHeight="32"
|
||||
DisplayMemberPath="Name"
|
||||
SelectedValuePath="Id"
|
||||
ToolTip="工具只能属于一个一级分类。" />
|
||||
|
||||
<TextBlock Grid.Row="3" Text="说明" VerticalAlignment="Top" Margin="0,14,0,0" />
|
||||
<TextBox x:Name="DescriptionTextBox"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0"
|
||||
MinHeight="72"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
ToolTip="用于描述这个工具的用途,会显示在卡片上。" />
|
||||
|
||||
<TextBlock x:Name="TargetLabel"
|
||||
Grid.Row="4"
|
||||
Text="目标路径"
|
||||
VerticalAlignment="Center" />
|
||||
<Grid x:Name="TargetPanel"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="TargetTextBox"
|
||||
MinHeight="32"
|
||||
ToolTip="本地工具路径、系统工具启动目标或网址。" />
|
||||
<Button Grid.Column="1"
|
||||
Content="文件"
|
||||
Width="64"
|
||||
Margin="8,0,0,0"
|
||||
ToolTip="选择本地程序、快捷方式、脚本或普通文件。"
|
||||
Click="BrowseFileButton_OnClick" />
|
||||
<Button Grid.Column="2"
|
||||
Content="文件夹"
|
||||
Width="72"
|
||||
Margin="8,0,0,0"
|
||||
ToolTip="选择一个文件夹作为本地工具。"
|
||||
Click="BrowseFolderButton_OnClick" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Row="5" Text="启动参数" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="ArgumentsTextBox"
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0"
|
||||
MinHeight="32"
|
||||
ToolTip="传给可执行文件或脚本的参数,普通文件和文件夹通常不需要。" />
|
||||
|
||||
<TextBlock Grid.Row="6" Text="工作目录" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="WorkingDirectoryTextBox"
|
||||
Grid.Row="6"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0"
|
||||
MinHeight="32"
|
||||
ToolTip="可执行文件或脚本启动时使用的工作目录,可留空。" />
|
||||
|
||||
<TextBlock Grid.Row="7" Text="快捷键" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="HotkeyTextBox"
|
||||
Grid.Row="7"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0"
|
||||
MinHeight="32"
|
||||
ToolTip="格式示例:Ctrl + Alt + T。第一版要求至少包含一个修饰键。" />
|
||||
|
||||
<StackPanel Grid.Row="8"
|
||||
Grid.Column="1"
|
||||
Margin="0,12,0,0">
|
||||
<CheckBox x:Name="AutoRunCheckBox"
|
||||
Content="工具箱启动时自动运行"
|
||||
ToolTip="开启后会出现在设置页的自动运行列表中。" />
|
||||
<CheckBox x:Name="RunAsAdminCheckBox"
|
||||
Content="以管理员身份运行"
|
||||
Margin="0,8,0,0"
|
||||
ToolTip="仅启动该工具时触发 UAC,工具箱自身不提权。" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,14,0,0">
|
||||
<Button Content="保存"
|
||||
Width="88"
|
||||
Margin="0,0,8,0"
|
||||
IsDefault="True"
|
||||
ToolTip="校验并保存工具设置。"
|
||||
Click="SaveButton_OnClick" />
|
||||
<Button Content="取消"
|
||||
Width="88"
|
||||
IsCancel="True"
|
||||
ToolTip="放弃本次修改。"
|
||||
Click="CancelButton_OnClick" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
157
src/PersonalToolbox/Views/ToolEditorWindow.xaml.cs
Normal file
157
src/PersonalToolbox/Views/ToolEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using System.Windows;
|
||||
using Forms = System.Windows.Forms;
|
||||
using PersonalToolbox.Models;
|
||||
using PersonalToolbox.Services;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
||||
|
||||
namespace PersonalToolbox.Views;
|
||||
|
||||
public partial class ToolEditorWindow : Window
|
||||
{
|
||||
private readonly ToolItem _tool;
|
||||
|
||||
private ToolEditorWindow(ToolItem tool, IEnumerable<CategoryItem> 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<CategoryItem> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user