Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.9.0] - 2026-05-26

### Added
- **Purple toggle switch** — global ToggleButton component replacing all CheckBoxes and
enable/disable buttons. Consistent on/off/locked states across Startup Manager, Privacy,
Windows Features, and Context Menu tabs.
- **Glass toast notifications** — bottom-right overlay appears on operation completion
(scan, install, cleanup, shred, etc). Auto-dismisses after 5 seconds.
- **Inline status bar** — progress state transitions visually from purple (busy) to green (done).

### Changed
- **Startup Manager** — toggle column uses purple ToggleSwitch instead of checkbox.
- **Privacy Toggles** — scaled checkbox replaced with ToggleSwitch.
- **Windows Features** — Enable/Disable button replaced with ToggleSwitch.
- **Context Menu** — checkbox replaced with ToggleSwitch.

## [1.8.0] - 2026-05-26

### Added
Expand Down
40 changes: 40 additions & 0 deletions SysManager/SysManager/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,46 @@
</Setter>
</Style>

<!-- ============================================================ -->
<!-- ToggleSwitch — purple pill toggle (on/off/locked) -->
<!-- ============================================================ -->
<Style x:Key="ToggleSwitch" TargetType="ToggleButton">
<Setter Property="Width" Value="40"/>
<Setter Property="Height" Value="22"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid>
<Border x:Name="Track" CornerRadius="11" Background="#374151"/>
<Border x:Name="Thumb" Width="18" Height="18" CornerRadius="9"
Background="White" HorizontalAlignment="Left" Margin="2,2,0,2"
VerticalAlignment="Center">
<Border.Effect>
<DropShadowEffect ShadowDepth="1" BlurRadius="3" Opacity="0.3" Color="Black"/>
</Border.Effect>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Track" Property="Background" Value="{StaticResource Accent}"/>
<Setter TargetName="Thumb" Property="HorizontalAlignment" Value="Right"/>
<Setter TargetName="Thumb" Property="Margin" Value="0,2,2,2"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Track" Property="Opacity" Value="0.88"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<!-- Admin: golden glass button for elevation requests -->
<Style x:Key="AdminButton" TargetType="Button" BasedOn="{StaticResource ButtonBase}">
<Setter Property="Foreground" Value="#FCD34D"/>
Expand Down
39 changes: 39 additions & 0 deletions SysManager/SysManager/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,5 +269,44 @@
Content="{Binding SelectedNav.View}"/>
</Grid>
</Border>

<!-- Glass toast notification overlay -->
<Border x:Name="ToastOverlay" Grid.Column="1"
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Margin="0,0,20,20" Visibility="Collapsed">
<Border Background="#F0141B27" CornerRadius="10"
BorderBrush="#4022C55E" BorderThickness="1"
Padding="14,12,18,12">
<Border.Effect>
<DropShadowEffect ShadowDepth="4" BlurRadius="24" Opacity="0.5" Color="Black"/>
</Border.Effect>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="28" Height="28" CornerRadius="14"
Background="#2022C55E" Margin="0,0,10,0">
<TextBlock Text="&#xE73E;" FontFamily="Segoe Fluent Icons,Segoe MDL2 Assets"
FontSize="14" Foreground="#22C55E"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock x:Name="ToastTitle" FontSize="13" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimary}"/>
<TextBlock x:Name="ToastDetail" FontSize="11"
Foreground="{StaticResource TextSecondary}" Margin="0,2,0,0"/>
</StackPanel>
<Border Grid.Column="2" Margin="14,0,0,0" Cursor="Hand"
MouseLeftButtonUp="DismissToast_Click"
Background="Transparent" Padding="4">
<TextBlock Text="&#xE711;" FontFamily="Segoe Fluent Icons,Segoe MDL2 Assets"
FontSize="12" Foreground="{StaticResource TextMuted}"
VerticalAlignment="Center"/>
</Border>
</Grid>
</Border>
</Border>
</Grid>
</Window>
26 changes: 26 additions & 0 deletions SysManager/SysManager/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media.Animation;
using SysManager.Services;
using SysManager.ViewModels;

Expand All @@ -21,6 +22,31 @@ public MainWindow()
// Ensure ViewModel disposal even if OnClosed is not called (e.g. app shutdown)
if (Application.Current != null)
Application.Current.Exit += OnApplicationExit;

ToastService.Instance.ToastRequested += OnToastRequested;
ToastService.Instance.DismissRequested += OnToastDismiss;
}

private void OnToastRequested(string title, string detail)
{
ToastTitle.Text = title;
ToastDetail.Text = detail;
ToastOverlay.Visibility = Visibility.Visible;
ToastOverlay.Opacity = 0;
var fade = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200));
ToastOverlay.BeginAnimation(OpacityProperty, fade);
}

private void OnToastDismiss()
{
var fade = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(200));
fade.Completed += (_, _) => ToastOverlay.Visibility = Visibility.Collapsed;
ToastOverlay.BeginAnimation(OpacityProperty, fade);
}

private void DismissToast_Click(object sender, MouseButtonEventArgs e)
{
ToastService.Instance.Dismiss();
}

private void OnApplicationExit(object sender, ExitEventArgs e)
Expand Down
47 changes: 47 additions & 0 deletions SysManager/SysManager/Services/ToastService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SysManager · ToastService — global glass toast notifications
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using System.Windows;
using System.Windows.Threading;

namespace SysManager.Services;

public sealed class ToastService
{
private static ToastService? _instance;
public static ToastService Instance => _instance ??= new ToastService();

public event Action<string, string>? ToastRequested;
public event Action? DismissRequested;

private DispatcherTimer? _autoDismiss;

public void Show(string title, string detail, int autoHideMs = 5000)
{
if (Application.Current?.Dispatcher is not { } dispatcher) return;

dispatcher.Invoke(() =>
{
_autoDismiss?.Stop();
ToastRequested?.Invoke(title, detail);

if (autoHideMs > 0)
{
_autoDismiss = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(autoHideMs) };
_autoDismiss.Tick += (_, _) =>
{
_autoDismiss.Stop();
DismissRequested?.Invoke();
};
_autoDismiss.Start();
}
});
}

public void Dismiss()
{
_autoDismiss?.Stop();
DismissRequested?.Invoke();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ private async Task InstallSelectedAsync()

Progress = 100;
StatusMessage = $"Done. Installed: {installed}, Failed: {failed}.";
ToastService.Instance.Show("Bulk Install complete", $"Installed: {installed}, Failed: {failed}");
}
catch (OperationCanceledException)
{
Expand Down
1 change: 1 addition & 0 deletions SysManager/SysManager/ViewModels/CleanupViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ await _runner.RunScriptViaPwshAsync(@"
'Recycle Bin cleared'
", cancellationToken: _binCts.Token);
StatusMessage = "Done";
ToastService.Instance.Show("Cleanup complete", "Operation finished successfully");
await PreScanAsync();
}
catch (OperationCanceledException) { StatusMessage = "Recycle Bin cleanup cancelled."; }
Expand Down
1 change: 1 addition & 0 deletions SysManager/SysManager/ViewModels/ContextMenuViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private async Task ScanAsync()

ApplyFilter();
StatusMessage = $"Found {_allEntries.Count} context menu entries.";
ToastService.Instance.Show("Context Menu scan complete", $"Found {_allEntries.Count} entries");
}
catch (InvalidOperationException ex)
{
Expand Down
1 change: 1 addition & 0 deletions SysManager/SysManager/ViewModels/DashboardViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ private async Task RunTuneUpAsync()
TuneUpResult = await _tuneUp.RunAsync(emptyBin, progress, _tuneUpCts.Token);
HasTuneUpResult = true;
StatusMessage = $"Tune-Up complete — {TuneUpResult.FreedDisplay} freed, {TuneUpResult.OverallVerdict}";
ToastService.Instance.Show("Tune-Up complete", $"{TuneUpResult.FreedDisplay} freed");
Log.Information("Quick Tune-Up completed: {Freed} freed, {Warnings} warnings",
TuneUpResult.FreedDisplay, TuneUpResult.WarningCount);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ private async Task AnalyzeAsync()
? "No subfolders found."
: $"{EntryCount} folders · {FormatSize(TotalSize)} total · {TotalFiles:N0} files";
StatusMessage = "Analysis complete.";
ToastService.Instance.Show("Disk Analysis complete", $"{EntryCount} folders, {FormatSize(TotalSize)} total");
Log.Information("Disk analysis completed: {Folders} folders, {Size} total",
EntryCount, FormatSize(TotalSize));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ private async Task ScanAsync()
? "No duplicates found."
: $"{GroupCount} groups · {DuplicateFileCount} files · {FormatSize(TotalWasted)} wasted";
StatusMessage = "Scan complete.";
ToastService.Instance.Show("Duplicate Scan complete", $"{GroupCount} groups, {FormatSize(TotalWasted)} wasted");
Log.Information("Duplicate scan completed: {Groups} groups, {Files} files, {Wasted} wasted",
GroupCount, DuplicateFileCount, FormatSize(TotalWasted));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ private async Task ShredAllAsync()
}

StatusMessage = $"Complete — {completed} shredded, {failed} failed.";
ToastService.Instance.Show("File Shredder complete", $"{completed} shredded, {failed} failed");
}
catch (OperationCanceledException)
{
Expand Down
1 change: 1 addition & 0 deletions SysManager/SysManager/ViewModels/StartupViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ private async Task ScanAsync()
}

StatusMessage = $"Found {_allEntries.Count} startup items.";
ToastService.Instance.Show("Scan complete", $"{_allEntries.Count} startup items found");
}
catch (InvalidOperationException ex)
{
Expand Down
2 changes: 2 additions & 0 deletions SysManager/SysManager/ViewModels/UninstallerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private async Task ScanAsync()

ApplyFilter();
StatusMessage = $"Found {AllApps.Count} installed applications.";
ToastService.Instance.Show("Uninstaller scan complete", $"Found {AllApps.Count} installed applications");
}
catch (OperationCanceledException)
{
Expand Down Expand Up @@ -142,6 +143,7 @@ private async Task UninstallSelectedAsync()

Progress = 100;
StatusMessage = $"Completed {done}/{toRemove.Count} uninstalls.";
ToastService.Instance.Show("Uninstall complete", $"Completed {done}/{toRemove.Count} uninstalls");
Log.Information("Uninstall batch completed: {Done}/{Total}", done, toRemove.Count);
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private async Task ScanAsync()
ApplyFilter();
EnabledCount = AllFeatures.Count(f => f.IsEnabled);
StatusMessage = $"Found {AllFeatures.Count} features ({EnabledCount} enabled).";
ToastService.Instance.Show("Windows Features scan complete", $"Found {AllFeatures.Count} features");
}
catch (OperationCanceledException)
{
Expand Down
2 changes: 2 additions & 0 deletions SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ await _runner.RunScriptViaPwshAsync(@"
? $"{UpdateCount} updates found."
: "No updates available.";
StatusMessage = "Scan complete";
ToastService.Instance.Show("Windows Update scan complete", $"{UpdateCount} updates found");
}
catch (OperationCanceledException) { StatusMessage = "Cancelled."; }
catch (InvalidOperationException ex) { StatusMessage = $"Error: {ex.Message}"; }
Expand Down Expand Up @@ -250,6 +251,7 @@ await _runner.RunScriptViaPwshAsync(@"
UpdateCount = Updates.Count;
TableSummary = $"{UpdateCount} history entries.";
StatusMessage = "Done";
ToastService.Instance.Show("Update History complete", $"{UpdateCount} history entries");
}
catch (OperationCanceledException) { StatusMessage = "Cancelled."; }
catch (InvalidOperationException ex) { StatusMessage = $"Error: {ex.Message}"; }
Expand Down
11 changes: 6 additions & 5 deletions SysManager/SysManager/Views/ContextMenuView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,14 @@
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<DataGrid.Columns>
<DataGridTemplateColumn Header="" Width="40" CanUserSort="False">
<DataGridTemplateColumn Header="" Width="50" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged}"
Command="{Binding DataContext.ToggleEntryCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
<ToggleButton Style="{StaticResource ToggleSwitch}"
IsChecked="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged}"
Command="{Binding DataContext.ToggleEntryCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Expand Down
11 changes: 4 additions & 7 deletions SysManager/SysManager/Views/PrivacyView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,10 @@
Margin="0,2,0,0" Opacity="0.6"/>
</StackPanel>

<CheckBox Grid.Column="1" IsChecked="{Binding IsEnabled}"
VerticalAlignment="Center" Margin="8,0,0,0"
ToolTip="Toggle privacy protection on/off">
<CheckBox.LayoutTransform>
<ScaleTransform ScaleX="1.3" ScaleY="1.3"/>
</CheckBox.LayoutTransform>
</CheckBox>
<ToggleButton Grid.Column="1" Style="{StaticResource ToggleSwitch}"
IsChecked="{Binding IsEnabled}"
VerticalAlignment="Center" Margin="8,0,0,0"
ToolTip="Toggle privacy protection on/off"/>
</Grid>
</Border>
</DataTemplate>
Expand Down
11 changes: 6 additions & 5 deletions SysManager/SysManager/Views/StartupView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<DataGrid.Columns>
<DataGridTemplateColumn Header="" Width="40" CanUserSort="False">
<DataGridTemplateColumn Header="" Width="50" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged}"
Command="{Binding DataContext.ToggleEntryCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
<ToggleButton Style="{StaticResource ToggleSwitch}"
IsChecked="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged}"
Command="{Binding DataContext.ToggleEntryCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Expand Down
Loading
Loading