using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LanDiscovery.Models; using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp; using System.Collections.ObjectModel; namespace LanDiscovery.ViewModels { public partial class MainViewModel : ObservableObject { private readonly Services.NetworkInterfaceService _nicService; private readonly Services.IpRangeService _ipRangeService; private readonly Services.ScanOrchestrator _orchestrator; [ObservableProperty] private ObservableCollection _devices; [ObservableProperty] private ObservableCollection _availableNics; [ObservableProperty] [NotifyPropertyChangedFor(nameof(SelectedNicDescription))] private Services.NetworkInterfaceInfo? _selectedNic; public string SelectedNicDescription => SelectedNic != null ? $"{SelectedNic.Description} | {SelectedNic.IpAddress} / {SelectedNic.SubnetMask}" : "No NIC Selected"; [ObservableProperty] private ObservableCollection _scanRanges; [ObservableProperty] private string? _newRangeInput; [ObservableProperty] private double _scanProgress; [ObservableProperty] private string _statusMessage; [ObservableProperty] private int _deviceCount; [ObservableProperty] private string _searchText = ""; [ObservableProperty] private string _logText = "Log Initialized...\n"; public void Log(string message) { if (string.IsNullOrWhiteSpace(message)) return; var time = DateTime.Now.ToString("HH:mm:ss"); LogText += $"[{time}] {message}\n"; } public System.ComponentModel.ICollectionView DevicesView { get; private set; } public ISeries[] Series { get; set; } = { new PieSeries { Values = new int[] { 0 }, Name = "Scanning...", InnerRadius = 50 } }; private readonly Services.SettingsService _settingsService; public MainViewModel() { _nicService = new Services.NetworkInterfaceService(); _ipRangeService = new Services.IpRangeService(); _orchestrator = new Services.ScanOrchestrator(); _settingsService = new Services.SettingsService(); Devices = new ObservableCollection(); // Setup CollectionView for filtering/sorting DevicesView = System.Windows.Data.CollectionViewSource.GetDefaultView(Devices); DevicesView.Filter = FilterDevices; AvailableNics = new ObservableCollection(_nicService.GetAllIpv4Interfaces()); ScanRanges = new ObservableCollection(); StatusMessage = "Ready to scan"; ScanProgress = 0; DeviceCount = 0; // Load Settings var settings = _settingsService.Load(); // Restore Ranges if (settings.LastRanges != null && settings.LastRanges.Count > 0) { foreach(var r in settings.LastRanges) ScanRanges.Add(r); } // Restore NIC if (AvailableNics.Count > 0) { if (!string.IsNullOrEmpty(settings.LastNicId)) { SelectedNic = AvailableNics.FirstOrDefault(n => n.Id == settings.LastNicId) ?? AvailableNics[0]; } else { SelectedNic = AvailableNics[0]; } } // If no ranges were loaded, auto-add default if (ScanRanges.Count == 0 && SelectedNic != null) { AutoAddDefaultRange(); } // Hook Orchestrator Events _orchestrator.DeviceDiscovered += OnDeviceDiscovered; _orchestrator.DeviceUpdated += OnDeviceUpdated; _orchestrator.StatusChanged += OnStatusChanged; _orchestrator.ProgressChanged += OnProgressChanged; _orchestrator.ScanCompleted += OnScanCompleted; } partial void OnSearchTextChanged(string value) { DevicesView.Refresh(); } private bool FilterDevices(object item) { if (string.IsNullOrWhiteSpace(SearchText)) return true; if (item is DiscoveredDevice device) { var q = SearchText.ToLower(); return (device.Hostname != null && device.Hostname.ToLower().Contains(q)) || (device.IpAddress != null && device.IpAddress.Contains(q)) || (device.MacAddress != null && device.MacAddress.ToLower().Contains(q)) || (device.Vendor != null && device.Vendor.ToLower().Contains(q)); } return false; } private void SaveSettings() { _settingsService.Save(new Services.UserSettings { LastNicId = SelectedNic?.Id, LastRanges = new List(ScanRanges) }); } private void OnDeviceDiscovered(DiscoveredDevice device) { App.Current.Dispatcher.Invoke(() => { Devices.Add(device); DeviceCount = Devices.Count; UpdateChart(); }); } private void OnDeviceUpdated(DiscoveredDevice device) { App.Current.Dispatcher.Invoke(() => { // Force UI refresh if needed (ObservableObject handles property changes usually) // But we might need to refresh chart if type changed UpdateChart(); }); } private void UpdateChart() { // Simple aggregation var groups = Devices.GroupBy(d => d.DeviceTypeHint) .Select(g => new { Type = g.Key, Count = g.Count() }) .OrderByDescending(x => x.Count); var newSeries = new List(); foreach (var g in groups) { newSeries.Add(new PieSeries { Values = new int[] { g.Count }, Name = g.Type.ToString(), InnerRadius = 50 }); } if (newSeries.Count == 0) { newSeries.Add(new PieSeries { Values = new int[] { 1 }, Name = "Empty", InnerRadius = 50 }); } Series = newSeries.ToArray(); OnPropertyChanged(nameof(Series)); } [RelayCommand] private async Task Export() { var dialog = new Microsoft.Win32.SaveFileDialog(); dialog.FileName = $"Scan_Export_{DateTime.Now:yyyyMMdd_HHmm}"; dialog.DefaultExt = ".csv"; dialog.Filter = "CSV Documents (.csv)|*.csv"; if (dialog.ShowDialog() == true) { var service = new Services.ExportService(); await service.ExportToCsvAsync(Devices, dialog.FileName); StatusMessage = $"Exported to {dialog.FileName}"; } } [RelayCommand] private void Capture(System.Windows.FrameworkElement? element) { if (element == null) return; var dialog = new Microsoft.Win32.SaveFileDialog(); dialog.FileName = $"Scan_Capture_{DateTime.Now:yyyyMMdd_HHmm}"; dialog.DefaultExt = ".png"; dialog.Filter = "PNG Images (.png)|*.png"; if (dialog.ShowDialog() == true) { var service = new Services.ScreenshotService(); try { service.CaptureToFile(element, dialog.FileName); StatusMessage = $"Screenshot saved to {dialog.FileName}"; } catch (Exception ex) { StatusMessage = $"Capture failed: {ex.Message}"; } } } private void OnStatusChanged(string status) { App.Current.Dispatcher.Invoke(() => { StatusMessage = status; Log(status); }); } private void OnProgressChanged(double progress) { App.Current.Dispatcher.Invoke(() => ScanProgress = progress); } private void OnScanCompleted() { App.Current.Dispatcher.Invoke(() => { if (ScanProgress < 100) ScanProgress = 100; }); } partial void OnSelectedNicChanged(Services.NetworkInterfaceInfo? value) { if (ScanRanges.Count == 0) AutoAddDefaultRange(); SaveSettings(); } private void AutoAddDefaultRange() { // 1. Add Local Subnet (if any) if (SelectedNic != null) { var suggested = _ipRangeService.SuggestCidrFromNic(SelectedNic.IpAddress, SelectedNic.SubnetMask); if (!string.IsNullOrEmpty(suggested) && !ScanRanges.Contains(suggested)) { ScanRanges.Add(suggested); } } // 2. Add User Requested Defaults // 10.10.7.0/24 10.10.5.0/24 10.10.8.0/24 10.10.9.0/24 10.10.10.0/24 10.10.15.0/24 10.10.21.0/24 var defaults = new[] { "10.10.7.0/24", "10.10.5.0/24", "10.10.8.0/24", "10.10.9.0/24", "10.10.10.0/24", "10.10.15.0/24", "10.10.21.0/24" }; foreach (var def in defaults) { if (!ScanRanges.Contains(def)) { ScanRanges.Add(def); } } } [RelayCommand] private void OpenWebInterface(object? parameter) { if (parameter is DiscoveredDevice device) { // Prefer Hostname, fallback to IP var target = !string.IsNullOrEmpty(device.Hostname) ? device.Hostname : device.IpAddress; OpenUrl($"https://{target}"); } else if (parameter is string url) { OpenUrl(url); } } [RelayCommand] private void CopyIp(DiscoveredDevice device) { if (device != null && !string.IsNullOrEmpty(device.IpAddress)) { System.Windows.Clipboard.SetText(device.IpAddress); Log($"Copied IP {device.IpAddress}"); } } [RelayCommand] private void CopyMac(DiscoveredDevice device) { if (device != null && !string.IsNullOrEmpty(device.MacAddress)) { System.Windows.Clipboard.SetText(device.MacAddress); Log($"Copied MAC {device.MacAddress}"); } } private void OpenUrl(string url) { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true }); Log($"Opening URL: {url}"); } catch (Exception ex) { Log($"Failed to open URL: {ex.Message}"); } } [RelayCommand] private void AddRange() { if (!string.IsNullOrWhiteSpace(NewRangeInput)) { var input = NewRangeInput.Trim(); if (!ScanRanges.Contains(input)) { ScanRanges.Add(input); StatusMessage = $"Added range {input}"; SaveSettings(); NewRangeInput = ""; } else { StatusMessage = "Range already exists."; } } else { StatusMessage = "Please enter a valid CIDR (e.g., 192.168.1.0/24)"; } } [RelayCommand] private void RemoveRange(string range) { if (ScanRanges.Contains(range)) { ScanRanges.Remove(range); SaveSettings(); } } [RelayCommand] private async Task StartScan() { if (SelectedNic == null || ScanRanges.Count == 0) { StatusMessage = "Select a NIC and at least one range."; return; } if (_orchestrator.IsScanning) return; Devices.Clear(); DeviceCount = 0; await Task.Run(() => _orchestrator.StartScanAsync(ScanRanges, SelectedNic)); } [RelayCommand] private void StopScan() { _orchestrator.StopScan(); } } }