From 7e773585547fd9cc501fd1e8ac1a2893c29f744b Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Wed, 13 May 2026 10:34:08 +0300 Subject: [PATCH] feat: Windows optional features toggle tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add WindowsFeature model with category classification - Add WindowsFeaturesService wrapping Get-WindowsOptionalFeature PowerShell - Add WindowsFeaturesViewModel with scan, toggle, filter, admin check - Add WindowsFeaturesView.xaml with DataGrid, category/state/toggle columns - Replace WipWindowsFeatures placeholder with real implementation - Register WindowsFeaturesService and VM in DI container - Add 10 unit tests (ParseFeatureList, CategorizeFeature, HumanizeName, VM state) - Update README: remove ⚙️ from Windows Features, add feature description - Update CHANGELOG with v0.46.0 entry Closes #5 --- CHANGELOG.md | 9 + README.md | 10 +- .../SysManager.Tests/WindowsFeaturesTests.cs | 132 +++++++++++++ .../SysManager/Models/WindowsFeature.cs | 55 ++++++ SysManager/SysManager/ServiceRegistration.cs | 2 + .../Services/WindowsFeaturesService.cs | 161 ++++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 9 +- .../ViewModels/WindowsFeaturesViewModel.cs | 181 ++++++++++++++++++ .../SysManager/Views/WindowsFeaturesView.xaml | 175 +++++++++++++++++ .../Views/WindowsFeaturesView.xaml.cs | 12 ++ 10 files changed, 741 insertions(+), 5 deletions(-) create mode 100644 SysManager/SysManager.Tests/WindowsFeaturesTests.cs create mode 100644 SysManager/SysManager/Models/WindowsFeature.cs create mode 100644 SysManager/SysManager/Services/WindowsFeaturesService.cs create mode 100644 SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs create mode 100644 SysManager/SysManager/Views/WindowsFeaturesView.xaml create mode 100644 SysManager/SysManager/Views/WindowsFeaturesView.xaml.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e00b625..5a261dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.46.0] - 2026-05-13 + +### Added +- **Windows Features tab** — list, enable, and disable Windows optional + features (Hyper-V, WSL, .NET 3.5, Telnet, etc.) directly from SysManager. + Features are categorized (Virtualization, Networking, Development, Media, + Legacy). Toggle requires admin. Shows reboot-required status. Includes + search/filter. Closes #5. + ## [0.45.0] - 2026-05-13 ### Added diff --git a/README.md b/README.md index f4e2bbdc..0510298f 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ implemented; 12 are work-in-progress placeholders marked with ⚙️: | Group | Tabs | |-------|------| | 🏠 Dashboard | Dashboard | -| 🔧 System | System Health · Windows Update · Performance Mode · Services · Startup Manager · Windows Features ⚙️ | +| 🔧 System | System Health · Windows Update · Performance Mode · Services · Startup Manager · Windows Features | | 📊 Monitor | Process Manager · Resource History ⚙️ · App Alerts · Privacy Monitor ⚙️ | | 🧹 Cleanup | Quick Cleanup · Deep Cleanup · Shortcut Cleaner · File Shredder ⚙️ | | 💾 Storage | Disk Analyzer · Duplicate Finder | @@ -142,6 +142,14 @@ long-running operation, so you always know which tab is working. - Shows name, publisher, command, and enabled/disabled status - Open file location in Explorer +### Windows Features +- Lists all Windows optional features with current state (Enabled/Disabled) +- Toggle enable/disable per feature with confirmation dialog +- Categorized: Virtualization, Networking, Development, Media & Print, Legacy +- Shows reboot-required status after toggling +- Search/filter across all features +- Requires administrator privileges for modifications + ### Duplicate File Finder - Two-pass scan: group by size, then SHA-256 hash only size-matched files - Duplicate groups sorted by wasted space (descending) diff --git a/SysManager/SysManager.Tests/WindowsFeaturesTests.cs b/SysManager/SysManager.Tests/WindowsFeaturesTests.cs new file mode 100644 index 00000000..b7907565 --- /dev/null +++ b/SysManager/SysManager.Tests/WindowsFeaturesTests.cs @@ -0,0 +1,132 @@ +// SysManager · WindowsFeaturesTests +// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager +// License: MIT + +using SysManager.Models; +using SysManager.Services; +using SysManager.ViewModels; +using Xunit; + +namespace SysManager.Tests; + +/// +/// Tests for and . +/// +public class WindowsFeaturesTests +{ + // ── ParseFeatureList ── + + [Fact] + public void ParseFeatureList_EmptyInput_ReturnsEmpty() + { + var result = WindowsFeaturesService.ParseFeatureList(new List()); + Assert.Empty(result); + } + + [Fact] + public void ParseFeatureList_ValidLines_ParsesCorrectly() + { + var lines = new List + { + "Microsoft-Hyper-V-All|Enabled", + "TelnetClient|Disabled", + "NetFx3|Enabled" + }; + + var result = WindowsFeaturesService.ParseFeatureList(lines); + + Assert.Equal(3, result.Count); + var hyperV = result.First(f => f.Name == "Microsoft-Hyper-V-All"); + Assert.True(hyperV.IsEnabled); + Assert.Equal("Virtualization", hyperV.Category); + + var telnet = result.First(f => f.Name == "TelnetClient"); + Assert.False(telnet.IsEnabled); + Assert.Equal("Networking", telnet.Category); + } + + [Fact] + public void ParseFeatureList_SkipsBlankLines() + { + var lines = new List + { + "", + " ", + "SomeFeature|Disabled", + "" + }; + + var result = WindowsFeaturesService.ParseFeatureList(lines); + Assert.Single(result); + } + + [Fact] + public void ParseFeatureList_SkipsInvalidLines() + { + var lines = new List + { + "NoSeparator", + "|Enabled", + "ValidFeature|Disabled" + }; + + var result = WindowsFeaturesService.ParseFeatureList(lines); + Assert.Single(result); + Assert.Equal("ValidFeature", result[0].Name); + } + + // ── CategorizeFeature ── + + [Theory] + [InlineData("Microsoft-Hyper-V-All", "Virtualization")] + [InlineData("VirtualMachinePlatform", "Virtualization")] + [InlineData("Microsoft-Windows-Subsystem-Linux", "Virtualization")] + [InlineData("Containers", "Virtualization")] + [InlineData("Windows-Sandbox", "Virtualization")] + [InlineData("TelnetClient", "Networking")] + [InlineData("IIS-WebServer", "Networking")] + [InlineData("SMB1Protocol", "Networking")] + [InlineData("NetFx3", "Development")] + [InlineData("Microsoft-Windows-Developer-Mode", "Development")] + [InlineData("OpenSSH-Client", "Development")] + [InlineData("MediaPlayback", "Media & Print")] + [InlineData("Printing-XPSServices-Features", "Media & Print")] + [InlineData("DirectPlay", "Legacy")] + [InlineData("SomeRandomFeature", "Other")] + public void CategorizeFeature_AssignsCorrectCategory(string featureName, string expected) + { + Assert.Equal(expected, WindowsFeature.CategorizeFeature(featureName)); + } + + [Fact] + public void CategorizeFeature_NullOrEmpty_ReturnsOther() + { + Assert.Equal("Other", WindowsFeature.CategorizeFeature(null!)); + Assert.Equal("Other", WindowsFeature.CategorizeFeature("")); + Assert.Equal("Other", WindowsFeature.CategorizeFeature(" ")); + } + + // ── HumanizeName ── + + [Theory] + [InlineData("Microsoft-Hyper-V-All", "Microsoft Hyper V All")] + [InlineData("TelnetClient", "TelnetClient")] + [InlineData("Some_Feature_Name", "Some Feature Name")] + public void HumanizeName_ReplacesDelimiters(string input, string expected) + { + Assert.Equal(expected, WindowsFeaturesService.HumanizeName(input)); + } + + // ── ViewModel ── + + [Fact] + public void ViewModel_InitialState_IsCorrect() + { + var vm = new WindowsFeaturesViewModel(new PowerShellRunner()); + Assert.Empty(vm.AllFeatures); + Assert.Empty(vm.FilteredFeatures); + Assert.Equal("", vm.FilterText); + Assert.Equal(0, vm.FeatureCount); + Assert.False(vm.PendingReboot); + } +} diff --git a/SysManager/SysManager/Models/WindowsFeature.cs b/SysManager/SysManager/Models/WindowsFeature.cs new file mode 100644 index 00000000..b9410107 --- /dev/null +++ b/SysManager/SysManager/Models/WindowsFeature.cs @@ -0,0 +1,55 @@ +// SysManager · WindowsFeature — model for Windows optional features +// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager +// License: MIT + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SysManager.Models; + +/// +/// Represents a Windows optional feature with its current state. +/// +public partial class WindowsFeature : ObservableObject +{ + [ObservableProperty] private string _name = ""; + [ObservableProperty] private string _displayName = ""; + [ObservableProperty] private bool _isEnabled; + [ObservableProperty] private bool _requiresReboot; + [ObservableProperty] private string _category = "Other"; + [ObservableProperty] private string _status = ""; + + /// Category assignment based on feature name patterns. + public static string CategorizeFeature(string featureName) + { + if (string.IsNullOrWhiteSpace(featureName)) return "Other"; + + var name = featureName.ToUpperInvariant(); + + if (name.Contains("HYPER") || name.Contains("VIRTUAL") || + name.Contains("CONTAINER") || name.Contains("SANDBOX") || + name.Contains("WSL") || name.Contains("LINUX")) + return "Virtualization"; + + if (name.Contains("IIS") || name.Contains("INTERNET") || + name.Contains("TFTP") || name.Contains("TELNET") || + name.Contains("SMB") || name.Contains("NFS") || + name.Contains("SNMP") || name.Contains("RIP")) + return "Networking"; + + if (name.Contains("NETFX") || name.Contains(".NET") || + name.Contains("DEVELOPER") || name.Contains("POWERSHELL") || + name.Contains("OPENSSH") || name.Contains("BASH")) + return "Development"; + + if (name.Contains("MEDIA") || name.Contains("PRINT") || + name.Contains("XPS") || name.Contains("PDF") || + name.Contains("SCAN") || name.Contains("FAX")) + return "Media & Print"; + + if (name.Contains("LEGACY") || name.Contains("DIRECTPLAY") || + name.Contains("INDEXING") || name.Contains("WORK")) + return "Legacy"; + + return "Other"; + } +} diff --git a/SysManager/SysManager/ServiceRegistration.cs b/SysManager/SysManager/ServiceRegistration.cs index a71902e2..9e916db9 100644 --- a/SysManager/SysManager/ServiceRegistration.cs +++ b/SysManager/SysManager/ServiceRegistration.cs @@ -53,6 +53,8 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); return services; diff --git a/SysManager/SysManager/Services/WindowsFeaturesService.cs b/SysManager/SysManager/Services/WindowsFeaturesService.cs new file mode 100644 index 00000000..7623fbed --- /dev/null +++ b/SysManager/SysManager/Services/WindowsFeaturesService.cs @@ -0,0 +1,161 @@ +// SysManager · WindowsFeaturesService — list and toggle Windows optional features +// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager +// License: MIT + +using System.Text.RegularExpressions; +using Serilog; +using SysManager.Models; + +namespace SysManager.Services; + +/// +/// Wraps PowerShell commands to list, enable, and disable Windows optional features. +/// Requires administrator privileges for enable/disable operations. +/// +public sealed partial class WindowsFeaturesService +{ + private readonly PowerShellRunner _runner; + + public WindowsFeaturesService(PowerShellRunner runner) => _runner = runner; + + /// + /// Lists all Windows optional features with their current state. + /// Uses Get-WindowsOptionalFeature -Online. + /// + public async Task> ListFeaturesAsync(CancellationToken ct = default) + { + var captured = new List(); + void Collect(PowerShellLine l) + { + if (l.Kind == OutputKind.Output) captured.Add(l.Text); + } + + _runner.LineReceived += Collect; + try + { + await _runner.RunProcessAsync("powershell", + "-NoProfile -Command \"Get-WindowsOptionalFeature -Online | " + + "Select-Object FeatureName, State | " + + "ForEach-Object { $_.FeatureName + '|' + $_.State }\"", ct); + } + finally { _runner.LineReceived -= Collect; } + + return ParseFeatureList(captured); + } + + /// + /// Enables a Windows optional feature. Requires admin. + /// Returns true if successful, false otherwise. + /// + public async Task<(bool Success, bool RebootRequired)> EnableFeatureAsync( + string featureName, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(featureName) || !FeatureNamePattern().IsMatch(featureName)) + throw new ArgumentException("Invalid feature name.", nameof(featureName)); + + Log.Information("Enabling Windows feature: {Feature}", featureName); + + var captured = new List(); + void Collect(PowerShellLine l) + { + if (l.Kind == OutputKind.Output) captured.Add(l.Text); + } + + _runner.LineReceived += Collect; + try + { + var code = await _runner.RunProcessAsync("powershell", + $"-NoProfile -Command \"Enable-WindowsOptionalFeature -Online " + + $"-FeatureName '{featureName}' -NoRestart -All | " + + $"Select-Object RestartNeeded | ForEach-Object {{ $_.RestartNeeded }}\"", ct); + + var reboot = captured.Any(l => + l.Contains("True", StringComparison.OrdinalIgnoreCase)); + + return (code == 0, reboot); + } + finally { _runner.LineReceived -= Collect; } + } + + /// + /// Disables a Windows optional feature. Requires admin. + /// Returns true if successful, false otherwise. + /// + public async Task<(bool Success, bool RebootRequired)> DisableFeatureAsync( + string featureName, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(featureName) || !FeatureNamePattern().IsMatch(featureName)) + throw new ArgumentException("Invalid feature name.", nameof(featureName)); + + Log.Information("Disabling Windows feature: {Feature}", featureName); + + var captured = new List(); + void Collect(PowerShellLine l) + { + if (l.Kind == OutputKind.Output) captured.Add(l.Text); + } + + _runner.LineReceived += Collect; + try + { + var code = await _runner.RunProcessAsync("powershell", + $"-NoProfile -Command \"Disable-WindowsOptionalFeature -Online " + + $"-FeatureName '{featureName}' -NoRestart | " + + $"Select-Object RestartNeeded | ForEach-Object {{ $_.RestartNeeded }}\"", ct); + + var reboot = captured.Any(l => + l.Contains("True", StringComparison.OrdinalIgnoreCase)); + + return (code == 0, reboot); + } + finally { _runner.LineReceived -= Collect; } + } + + /// Parses the pipe-delimited feature list output. + internal static List ParseFeatureList(List lines) + { + var features = new List(); + + foreach (var line in lines) + { + if (string.IsNullOrWhiteSpace(line)) continue; + + var parts = line.Split('|', 2); + if (parts.Length < 2) continue; + + var name = parts[0].Trim(); + var state = parts[1].Trim(); + + if (string.IsNullOrWhiteSpace(name)) continue; + + var isEnabled = state.Equals("Enabled", StringComparison.OrdinalIgnoreCase); + + features.Add(new WindowsFeature + { + Name = name, + DisplayName = HumanizeName(name), + IsEnabled = isEnabled, + Category = WindowsFeature.CategorizeFeature(name) + }); + } + + return features.OrderBy(f => f.Category) + .ThenBy(f => f.DisplayName) + .ToList(); + } + + /// + /// Converts PascalCase feature names to human-readable form. + /// e.g. "Microsoft-Hyper-V-All" → "Microsoft Hyper-V All" + /// + internal static string HumanizeName(string featureName) + { + return featureName.Replace('-', ' ').Replace('_', ' '); + } + + /// + /// Validates feature names: alphanumeric, hyphens, underscores, dots. Max 128 chars. + /// + [GeneratedRegex(@"^[\w.\-]{1,128}$")] + private static partial Regex FeatureNamePattern(); +} diff --git a/SysManager/SysManager/ViewModels/MainWindowViewModel.cs b/SysManager/SysManager/ViewModels/MainWindowViewModel.cs index 36a9ae81..e51ccaf0 100644 --- a/SysManager/SysManager/ViewModels/MainWindowViewModel.cs +++ b/SysManager/SysManager/ViewModels/MainWindowViewModel.cs @@ -38,7 +38,7 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable public ServicesViewModel Services { get; } // ── Placeholder ViewModels for planned features (WIP) ────────── - public PlaceholderViewModel WipWindowsFeatures { get; private set; } = null!; + public WindowsFeaturesViewModel WindowsFeatures { get; } public PlaceholderViewModel WipResourceHistory { get; private set; } = null!; public AppAlertsViewModel AppAlerts { get; } public PlaceholderViewModel WipPrivacyMonitor { get; private set; } = null!; @@ -101,6 +101,7 @@ public MainWindowViewModel() AppAlerts = sp.GetRequiredService(); ShortcutCleaner = sp.GetRequiredService(); AppBlocker = sp.GetRequiredService(); + WindowsFeatures = sp.GetRequiredService(); } else { @@ -134,6 +135,7 @@ public MainWindowViewModel() AppAlerts = new AppAlertsViewModel(); ShortcutCleaner = new ShortcutCleanerViewModel(); AppBlocker = new AppBlockerViewModel(); + WindowsFeatures = new WindowsFeaturesViewModel(runner); } InitPlaceholders(); @@ -143,7 +145,6 @@ public MainWindowViewModel() private void InitPlaceholders() { // ── WIP placeholders for planned features ────────────────────── - WipWindowsFeatures = new PlaceholderViewModel("Windows Features", "Toggle Windows optional features on or off.", "388"); WipResourceHistory = new PlaceholderViewModel("Resource History", "Historical CPU, RAM, GPU and temperature graphs.", "377"); WipPrivacyMonitor = new PlaceholderViewModel("Privacy Monitor", "Monitor and alert on webcam, microphone, and location access.", "380"); WipFileShredder = new PlaceholderViewModel("File Shredder", "Securely delete files beyond recovery.", "386"); @@ -183,7 +184,7 @@ private void InitNavigation() new NavItem { Id = "nav-performance", Label = "Performance Mode", Glyph = "\uE945", Content = Performance, ViewType = typeof(Views.PerformanceView) }, new NavItem { Id = "nav-services", Label = "Services", Glyph = "\uE912", Content = Services, ViewType = typeof(Views.ServicesView) }, new NavItem { Id = "nav-startup", Label = "Startup Manager", Glyph = "\uE7B5", Content = Startup, ViewType = typeof(Views.StartupView) }, - new NavItem { Id = "nav-windows-features", Label = "Windows Features", Glyph = "\uE9CE", Content = WipWindowsFeatures, ViewType = typeof(Views.PlaceholderView) }, + new NavItem { Id = "nav-windows-features", Label = "Windows Features", Glyph = "\uE9CE", Content = WindowsFeatures, ViewType = typeof(Views.WindowsFeaturesView) }, }}; // 📊 Monitor (4) — NEW GROUP @@ -344,7 +345,7 @@ public void Dispose() About?.Dispose(); Services?.Dispose(); NetworkShared?.Dispose(); - WipWindowsFeatures?.Dispose(); + WindowsFeatures?.Dispose(); WipResourceHistory?.Dispose(); AppAlerts?.Dispose(); WipPrivacyMonitor?.Dispose(); diff --git a/SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs b/SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs new file mode 100644 index 00000000..89b8181d --- /dev/null +++ b/SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs @@ -0,0 +1,181 @@ +// SysManager · WindowsFeaturesViewModel — toggle Windows optional features +// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager +// License: MIT + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Serilog; +using SysManager.Helpers; +using SysManager.Models; +using SysManager.Services; + +namespace SysManager.ViewModels; + +/// +/// Windows Features tab — lists optional features, allows enable/disable toggle. +/// Requires administrator privileges for modifications. +/// +public partial class WindowsFeaturesViewModel : ViewModelBase +{ + private readonly WindowsFeaturesService _service; + private CancellationTokenSource? _cts; + + public ObservableCollection AllFeatures { get; } = new(); + public ObservableCollection FilteredFeatures { get; } = new(); + + [ObservableProperty] private string _filterText = ""; + [ObservableProperty] private int _featureCount; + [ObservableProperty] private int _enabledCount; + [ObservableProperty] private string _summary = "Click Scan to list Windows optional features."; + [ObservableProperty] private bool _isElevated; + [ObservableProperty] private bool _pendingReboot; + + partial void OnFilterTextChanged(string value) => ApplyFilter(); + + public WindowsFeaturesViewModel(PowerShellRunner runner) + { + _service = new WindowsFeaturesService(runner); + IsElevated = AdminHelper.IsElevated(); + } + + [RelayCommand] + private async Task ScanAsync() + { + IsBusy = true; + IsProgressIndeterminate = true; + StatusMessage = "Querying Windows optional features…"; + AllFeatures.Clear(); + FilteredFeatures.Clear(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + + try + { + var list = await _service.ListFeaturesAsync(_cts.Token); + foreach (var feature in list) + AllFeatures.Add(feature); + + ApplyFilter(); + EnabledCount = AllFeatures.Count(f => f.IsEnabled); + StatusMessage = $"Found {AllFeatures.Count} features ({EnabledCount} enabled)."; + } + catch (OperationCanceledException) + { + StatusMessage = "Scan cancelled."; + } + catch (InvalidOperationException ex) + { + StatusMessage = $"Error: {ex.Message}"; + } + finally + { + IsBusy = false; + IsProgressIndeterminate = false; + } + } + + [RelayCommand] + private async Task ToggleFeatureAsync(WindowsFeature? feature) + { + if (feature == null) return; + + if (!IsElevated) + { + StatusMessage = "Administrator privileges required to modify features."; + return; + } + + var action = feature.IsEnabled ? "disable" : "enable"; + if (!DialogService.Instance.Confirm( + $"Are you sure you want to {action} '{feature.DisplayName}'?\n\n" + + "This may require a system reboot to take effect.", + $"Confirm {action} feature")) + return; + + IsBusy = true; + feature.Status = feature.IsEnabled ? "Disabling…" : "Enabling…"; + StatusMessage = $"{(feature.IsEnabled ? "Disabling" : "Enabling")} {feature.DisplayName}…"; + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + + try + { + bool success; + bool reboot; + + if (feature.IsEnabled) + { + (success, reboot) = await _service.DisableFeatureAsync(feature.Name, _cts.Token); + } + else + { + (success, reboot) = await _service.EnableFeatureAsync(feature.Name, _cts.Token); + } + + if (success) + { + feature.IsEnabled = !feature.IsEnabled; + feature.RequiresReboot = reboot; + feature.Status = reboot ? "Reboot required" : "Done"; + EnabledCount = AllFeatures.Count(f => f.IsEnabled); + + if (reboot) PendingReboot = true; + + StatusMessage = $"{feature.DisplayName} {(feature.IsEnabled ? "enabled" : "disabled")}" + + (reboot ? " — reboot required." : "."); + Log.Information("Feature {Feature} toggled to {State}, reboot={Reboot}", + feature.Name, feature.IsEnabled ? "Enabled" : "Disabled", reboot); + } + else + { + feature.Status = "Failed"; + StatusMessage = $"Failed to {action} {feature.DisplayName}. Check permissions."; + } + } + catch (OperationCanceledException) + { + feature.Status = "Cancelled"; + StatusMessage = "Operation cancelled."; + } + catch (InvalidOperationException ex) + { + feature.Status = "Error"; + StatusMessage = $"Error: {ex.Message}"; + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private void Cancel() => _cts?.Cancel(); + + protected override void Dispose(bool disposing) + { + if (disposing) _cts?.Dispose(); + base.Dispose(disposing); + } + + private void ApplyFilter() + { + FilteredFeatures.Clear(); + IEnumerable source = AllFeatures; + + if (!string.IsNullOrWhiteSpace(FilterText)) + { + var f = FilterText.Trim(); + source = source.Where(feat => + feat.DisplayName.Contains(f, StringComparison.OrdinalIgnoreCase) || + feat.Name.Contains(f, StringComparison.OrdinalIgnoreCase) || + feat.Category.Contains(f, StringComparison.OrdinalIgnoreCase)); + } + + foreach (var feat in source) + FilteredFeatures.Add(feat); + + FeatureCount = FilteredFeatures.Count; + Summary = $"{FeatureCount} features{(AllFeatures.Count != FeatureCount ? $" (of {AllFeatures.Count} total)" : "")}"; + } +} diff --git a/SysManager/SysManager/Views/WindowsFeaturesView.xaml b/SysManager/SysManager/Views/WindowsFeaturesView.xaml new file mode 100644 index 00000000..c0e9a3f3 --- /dev/null +++ b/SysManager/SysManager/Views/WindowsFeaturesView.xaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysManager/SysManager/Views/WindowsFeaturesView.xaml.cs b/SysManager/SysManager/Views/WindowsFeaturesView.xaml.cs new file mode 100644 index 00000000..33acdb94 --- /dev/null +++ b/SysManager/SysManager/Views/WindowsFeaturesView.xaml.cs @@ -0,0 +1,12 @@ +// SysManager · WindowsFeaturesView +// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager +// License: MIT + +using System.Windows.Controls; + +namespace SysManager.Views; + +public partial class WindowsFeaturesView : UserControl +{ + public WindowsFeaturesView() => InitializeComponent(); +}