using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; using NetTool.Client.Models; using NetTool.Client.Services; namespace NetTool.Client; public partial class MainWindow : Window { private readonly NetworkAdapterService _networkAdapterService = new(); private readonly PasswordStoreService _passwordStoreService = new(); private readonly DiscoveryService _discoveryService = new(); private readonly ServerApiService _serverApiService = new(); private IReadOnlyList _allAdapters = []; private IReadOnlyList _adapters = []; private readonly ObservableCollection _discoveredDevices = []; private bool _isBusy; private bool _isSearchingDevices; private CancellationTokenSource? _deviceSearchCts; private CancellationTokenSource? _statusMessageCts; public MainWindow() { InitializeComponent(); Title = $"NetTool {GetClientVersion()}"; Loaded += MainWindow_OnLoaded; } private static string GetClientVersion() { return Assembly.GetExecutingAssembly() .GetCustomAttribute()? .InformationalVersion.Split('+')[0] ?? "unknown"; } private void MainWindow_OnLoaded(object sender, RoutedEventArgs e) { LoadInitialState(); } private void LoadInitialState() { _allAdapters = _networkAdapterService.GetAdapters(); ApplyAdapterFilter(); UpdateButtonStates(); } private void ApplyAdapterFilter(string? selectedAdapterId = null) { _adapters = _allAdapters .Where(adapter => ShowUsableAdaptersOnlyCheckBox.IsChecked != true || IsUsableAdapter(adapter)) .OrderByDescending(adapter => adapter.RecommendationScore) .ThenBy(adapter => adapter.Name) .ToList(); AdapterComboBox.ItemsSource = _adapters; var selected = selectedAdapterId is null ? null : _adapters.FirstOrDefault(adapter => adapter.Id == selectedAdapterId); if (selected is not null) { AdapterComboBox.SelectedItem = selected; } else { AdapterComboBox.SelectedIndex = -1; } UpdateAdapterPlaceholder(); } private static bool IsUsableAdapter(AdapterInfo adapter) { return adapter.HasLink && !string.IsNullOrWhiteSpace(adapter.IPv4Address); } private void AdapterComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (AdapterComboBox.SelectedItem is not AdapterInfo adapter) { CancelDeviceSearch(); ClearDiscoveredDevices(); UpdateAdapterPlaceholder(); SetStatus("请选择一块网卡。", StatusMessageType.Warning, false); UpdateButtonStates(); return; } UpdateAdapterPlaceholder(); if (!adapter.HasLink) { CancelDeviceSearch(); ClearDiscoveredDevices(); SetStatus("当前网卡未检测到链路,请检查网线连接。", StatusMessageType.Warning, true); UpdateButtonStates(); return; } UpdateButtonStates(); _ = SearchDevicesAsync(adapter); } private void UpdateAdapterPlaceholder() { AdapterPlaceholderTextBlock.Visibility = AdapterComboBox.SelectedItem is null ? Visibility.Visible : Visibility.Collapsed; } private async void RefreshAdaptersButton_OnClick(object sender, RoutedEventArgs e) { SetBusyState(true, "正在刷新本机网卡..."); try { var selectedAdapterId = (AdapterComboBox.SelectedItem as AdapterInfo)?.Id; RefreshAdapters(selectedAdapterId); SetStatus("已刷新本机网卡。", StatusMessageType.Success, true); } catch (Exception ex) { SetStatus($"刷新本机网卡失败:{ex.Message}", StatusMessageType.Error, true); MessageBox.Show(this, ex.Message, "刷新失败", MessageBoxButton.OK, MessageBoxImage.Error); } finally { SetBusyState(false); } if (AdapterComboBox.SelectedItem is AdapterInfo adapter) { await SearchDevicesAsync(adapter); } } private async void SearchDevicesButton_OnClick(object sender, RoutedEventArgs e) { if (AdapterComboBox.SelectedItem is AdapterInfo adapter) { await SearchDevicesAsync(adapter); } else { SetStatus("请先选择一块网卡。", StatusMessageType.Warning, true); } } private void ShowUsableAdaptersOnlyCheckBox_OnChanged(object sender, RoutedEventArgs e) { if (AdapterComboBox is null || ShowUsableAdaptersOnlyCheckBox is null) { return; } var selectedAdapterId = (AdapterComboBox.SelectedItem as AdapterInfo)?.Id; ApplyAdapterFilter(selectedAdapterId); UpdateButtonStates(); } private async Task SearchDevicesAsync(AdapterInfo adapter) { if (_isBusy) { return; } if (string.IsNullOrWhiteSpace(adapter.IPv4Address)) { CancelDeviceSearch(); ClearDiscoveredDevices(); SetStatus("当前网卡没有可用 IPv4,无法搜索设备。", StatusMessageType.Error, true); return; } if (!adapter.HasLink) { CancelDeviceSearch(); ClearDiscoveredDevices(); SetStatus("当前网卡未检测到链路,请检查网线连接。", StatusMessageType.Warning, true); return; } _deviceSearchCts?.Cancel(); var searchCts = new CancellationTokenSource(); _deviceSearchCts = searchCts; SetDeviceSearchState(true); ClearDiscoveredDevices(); await Dispatcher.InvokeAsync(() => { }, System.Windows.Threading.DispatcherPriority.Render); try { await _discoveryService.DiscoverManyAsync( adapter.IPv4Address, device => Dispatcher.Invoke(() => UpsertDiscoveredDevice(device)), searchCts.Token); if (_discoveredDevices.Count == 0) { SetStatus("未发现 169.254 开头的设备 IP,请确认网卡、网线、远端服务和维护网段配置。", StatusMessageType.Warning, true); return; } SetStatus($"已发现 {_discoveredDevices.Count} 台设备,请点击右侧连接。", StatusMessageType.Success, true); } catch (OperationCanceledException) { } catch (Exception ex) { SetStatus($"搜索设备失败:{ex.Message}", StatusMessageType.Error, true); MessageBox.Show(this, ex.Message, "搜索设备失败", MessageBoxButton.OK, MessageBoxImage.Error); } finally { if (ReferenceEquals(_deviceSearchCts, searchCts)) { SetDeviceSearchState(false); } } } private void SavePasswordForDevice(DiscoveredDevice device, string password) { var deviceKey = GetDevicePasswordKey(device); if (!string.IsNullOrWhiteSpace(deviceKey) && !string.IsNullOrWhiteSpace(password)) { _passwordStoreService.SavePassword(deviceKey, password); } } private void UpdateButtonStates() { var adapter = AdapterComboBox.SelectedItem as AdapterInfo; var hasAdapter = adapter is not null; RefreshAdaptersButton.IsEnabled = !_isBusy; SearchDevicesButton.IsEnabled = !_isBusy && !_isSearchingDevices && hasAdapter && adapter!.HasLink; } private void RefreshAdapters(string? selectedAdapterId = null) { _allAdapters = _networkAdapterService.GetAdapters(); ApplyAdapterFilter(selectedAdapterId); } private void ClearDiscoveredDevices() { _discoveredDevices.Clear(); DiscoveredDevicesListView.ItemsSource = _discoveredDevices; } private void UpsertDiscoveredDevice(DiscoveredDevice device) { var existing = _discoveredDevices.FirstOrDefault(value => string.Equals(value.Lan2Ip, device.Lan2Ip, StringComparison.OrdinalIgnoreCase)); if (existing is not null) { var index = _discoveredDevices.IndexOf(existing); _discoveredDevices[index] = device; return; } var insertIndex = 0; while (insertIndex < _discoveredDevices.Count && string.Compare(_discoveredDevices[insertIndex].Lan2Ip, device.Lan2Ip, StringComparison.OrdinalIgnoreCase) < 0) { insertIndex++; } _discoveredDevices.Insert(insertIndex, device); SetStatus($"已发现 {_discoveredDevices.Count} 台设备,搜索仍在继续。", StatusMessageType.Success, true); } private async void ConnectDeviceButton_OnClick(object sender, RoutedEventArgs e) { if (!_isBusy && sender is Button { DataContext: DiscoveredDevice device }) { await ConnectToDeviceAsync(device); } } private void DiscoveredDevicesListView_OnSizeChanged(object sender, SizeChangedEventArgs e) { var availableWidth = DiscoveredDevicesListView.ActualWidth - 36; if (availableWidth <= 0) { return; } DeviceActionColumn.Width = 88; var contentWidth = Math.Max(0, availableWidth - DeviceActionColumn.Width); DeviceIpColumn.Width = Math.Max(130, contentWidth * 0.26); DeviceHostnameColumn.Width = Math.Max(150, contentWidth * 0.30); DeviceMacColumn.Width = Math.Max(180, contentWidth - DeviceIpColumn.Width - DeviceHostnameColumn.Width); } private void DiscoveredDevicesListView_OnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) { if (sender is not ListView listView) { return; } Dispatcher.BeginInvoke(() => { if (!listView.IsKeyboardFocusWithin) { listView.SelectedItem = null; } }, DispatcherPriority.Input); } private async Task ConnectToDeviceAsync(DiscoveredDevice device) { var deviceKey = GetDevicePasswordKey(device); var savedPassword = _passwordStoreService.LoadPassword(deviceKey); var passwordConfirmed = TryPromptForPassword(device, savedPassword, out var password, out var clearSavedPasswordRequested); if (clearSavedPasswordRequested) { _passwordStoreService.ClearPassword(deviceKey); } if (!passwordConfirmed) { return; } if (string.IsNullOrWhiteSpace(password)) { SetStatus("请输入管理密码。", StatusMessageType.Warning, true); return; } var selectedAdapter = AdapterComboBox.SelectedItem as AdapterInfo; var httpPort = device.HttpPort > 0 ? device.HttpPort : 48888; var baseAddress = $"http://{device.Lan2Ip}:{httpPort}"; while (true) { SetBusyState(true, $"正在连接 {device.Lan2Ip}..."); try { var result = await _serverApiService.CheckHealthAsync(baseAddress, password, selectedAdapter?.IPv4Address ?? string.Empty); if (result.Success) { if (!clearSavedPasswordRequested) { SavePasswordForDevice(device, password); } SetStatus("连接成功。", StatusMessageType.Success, true); OpenDeviceDetailsWindow(baseAddress, selectedAdapter?.IPv4Address ?? string.Empty, password); return; } if (result.StatusCode == 401) { _passwordStoreService.ClearPassword(deviceKey); SetStatus("管理密码错误,请重新输入。", StatusMessageType.Error, true); SetBusyState(false); MessageBox.Show(this, "管理密码校验失败,请重新输入管理密码。", "密码错误", MessageBoxButton.OK, MessageBoxImage.Warning); if (!TryPromptForPassword(device, string.Empty, out password, out _)) { return; } continue; } SetStatus($"设备已发现,但 HTTP 验证失败:{result.Message}", StatusMessageType.Error, true); return; } catch (Exception ex) { SetStatus($"连接失败:{ex.Message}", StatusMessageType.Error, true); MessageBox.Show(this, ex.Message, "连接失败", MessageBoxButton.OK, MessageBoxImage.Error); return; } finally { SetBusyState(false); } } } private void OpenDeviceDetailsWindow(string baseAddress, string localIPv4, string password) { var window = new DeviceDetailsWindow(baseAddress, localIPv4, password) { Owner = this, }; window.ShowDialog(); } private bool TryPromptForPassword(DiscoveredDevice device, string savedPassword, out string password, out bool clearSavedPasswordRequested) { var label = string.IsNullOrWhiteSpace(device.Mac) ? device.Lan2Ip : device.Mac; var window = new PasswordPromptWindow(label, savedPassword) { Owner = this, }; var dialogResult = window.ShowDialog(); clearSavedPasswordRequested = window.ClearSavedPasswordRequested; if (dialogResult == true) { password = window.Password; return true; } password = string.Empty; return false; } private static string GetDevicePasswordKey(DiscoveredDevice device) { if (!string.IsNullOrWhiteSpace(device.Mac)) { return device.Mac; } return device.DeviceId; } private void SetStatus(string message, StatusMessageType type, bool addLog) { ApplyStatusMessageStyle(type); StatusTextBlock.Text = message; StatusMessageBorder.Opacity = 0; StatusMessageBorder.Visibility = Visibility.Visible; StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160))); _statusMessageCts?.Cancel(); _statusMessageCts = new CancellationTokenSource(); _ = HideStatusMessageAsync(_statusMessageCts.Token); _ = addLog; } private async Task HideStatusMessageAsync(CancellationToken cancellationToken) { try { await Task.Delay(3000, cancellationToken); await Dispatcher.InvokeAsync(() => { var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200)); animation.Completed += (_, _) => { if (!cancellationToken.IsCancellationRequested) { StatusMessageBorder.Visibility = Visibility.Collapsed; } }; StatusMessageBorder.BeginAnimation(OpacityProperty, animation); }); } catch (TaskCanceledException) { } } private void ApplyStatusMessageStyle(StatusMessageType type) { var (background, icon) = GetStatusMessageVisuals(type); StatusIconBorder.Background = background; StatusIconTextBlock.Text = icon; StatusTextBlock.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1F2937")); } private static (Brush Background, string Icon) GetStatusMessageVisuals(StatusMessageType type) { return type switch { StatusMessageType.Success => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27C346")), "✓"), StatusMessageType.Error => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F76965")), "×"), StatusMessageType.Warning => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF9626")), "!"), _ => (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#508DF8")), "i"), }; } private void SetBusyState(bool isBusy, string? message = null) { _isBusy = isBusy; BusyOverlay.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed; BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message; AdapterComboBox.IsEnabled = !isBusy; RefreshAdaptersButton.IsEnabled = !isBusy; SearchDevicesButton.IsEnabled = !isBusy && !_isSearchingDevices && AdapterComboBox.SelectedItem is AdapterInfo adapter && adapter.HasLink; } private void SetDeviceSearchState(bool isSearching) { _isSearchingDevices = isSearching; SearchProgressBar.Visibility = isSearching ? Visibility.Visible : Visibility.Collapsed; SearchProgressTextBlock.Visibility = isSearching ? Visibility.Visible : Visibility.Collapsed; SearchDevicesButton.Content = isSearching ? "搜索中..." : "重新搜索设备"; UpdateButtonStates(); } private void CancelDeviceSearch() { _deviceSearchCts?.Cancel(); SetDeviceSearchState(false); } }