From 0047631da6a1e02133af0ef9db9acbc9a631be35 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:03:39 -0400 Subject: [PATCH 1/3] Start CompactGUI UI overhaul foundation --- .../Components/CompactGuiNavigationIcon.vb | 48 +++++ CompactGUI/MainWindow.xaml.vb | 16 ++ CompactGUI/ViewModels/FolderViewModel.vb | 28 ++- CompactGUI/Views/Pages/FolderView.xaml.vb | 180 +++++++++++++++++- CompactGUI/Views/Pages/HomePage.xaml.vb | 41 +++- 5 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 CompactGUI/Components/CompactGuiNavigationIcon.vb diff --git a/CompactGUI/Components/CompactGuiNavigationIcon.vb b/CompactGUI/Components/CompactGuiNavigationIcon.vb new file mode 100644 index 00000000..b54fc84c --- /dev/null +++ b/CompactGUI/Components/CompactGuiNavigationIcon.vb @@ -0,0 +1,48 @@ +Imports System.Windows.Media +Imports System.Windows.Shapes + +Imports Wpf.Ui.Controls + +Public NotInheritable Class CompactGuiNavigationIcon + Inherits IconElement + + Private _northWestArrow As Path + Private _southEastArrow As Path + + Protected Overrides Function InitializeChildren() As UIElement + Dim canvas As New Canvas With { + .Width = 20, + .Height = 20, + .SnapsToDevicePixels = True + } + + 'Two opposing hollow arrows mirror CompactGUI's application icon without + 'introducing the four-arrow "move inward" glyph used by Fluent Icons. + _northWestArrow = CreateArrowPath("M 2.8,2.8 L 8.2,4.0 L 6.35,5.85 L 9.75,9.25 L 9.25,9.75 L 5.85,6.35 L 4.0,8.2 Z") + _southEastArrow = CreateArrowPath("M 17.2,17.2 L 11.8,16.0 L 13.65,14.15 L 10.25,10.75 L 10.75,10.25 L 14.15,13.65 L 16.0,11.8 Z") + + canvas.Children.Add(_northWestArrow) + canvas.Children.Add(_southEastArrow) + Return canvas + End Function + + Private Function CreateArrowPath(data As String) As Path + Return New Path With { + .Data = Geometry.Parse(data), + .Stroke = Foreground, + .StrokeThickness = 1.15, + .StrokeStartLineCap = PenLineCap.Round, + .StrokeEndLineCap = PenLineCap.Round, + .StrokeLineJoin = PenLineJoin.Round, + .Fill = Brushes.Transparent + } + End Function + + Protected Overrides Sub OnForegroundChanged(args As DependencyPropertyChangedEventArgs) + MyBase.OnForegroundChanged(args) + + Dim brush = TryCast(args.NewValue, Brush) + If _northWestArrow IsNot Nothing Then _northWestArrow.Stroke = brush + If _southEastArrow IsNot Nothing Then _southEastArrow.Stroke = brush + End Sub +End Class diff --git a/CompactGUI/MainWindow.xaml.vb b/CompactGUI/MainWindow.xaml.vb index fcc9ed09..c0d4beca 100644 --- a/CompactGUI/MainWindow.xaml.vb +++ b/CompactGUI/MainWindow.xaml.vb @@ -33,6 +33,8 @@ Class MainWindow : Implements INavigationWindow, INotifyPropertyChanged _SettingsService = settingsService DataContext = viewmodel + ConfigureCompressNavigationItem() + NotifyIconTrayMenu.DataContext = viewmodel AddHandler Application.GetService(Of HomeViewModel)().PropertyChanged, AddressOf HVPropertyChanged @@ -54,6 +56,20 @@ Class MainWindow : Implements INavigationWindow, INotifyPropertyChanged End Sub + Private Sub ConfigureCompressNavigationItem() + If NavigationView.MenuItems.Count = 0 Then Return + + Dim compressItem = TryCast(NavigationView.MenuItems(0), NavigationViewItem) + If compressItem Is Nothing Then Return + + compressItem.Content = "Compress" + compressItem.Icon = New CompactGuiNavigationIcon With { + .Width = 20, + .Height = 20 + } + End Sub + + Private _isOnHomePage As Boolean Private Sub OnNavigated(sender As NavigationView, args As NavigatedEventArgs) diff --git a/CompactGUI/ViewModels/FolderViewModel.vb b/CompactGUI/ViewModels/FolderViewModel.vb index 89c7f9e9..0e423ba7 100644 --- a/CompactGUI/ViewModels/FolderViewModel.vb +++ b/CompactGUI/ViewModels/FolderViewModel.vb @@ -239,10 +239,30 @@ Public NotInheritable Class FolderViewModel : Inherits ObservableObject : Implem End Function - Private Sub ApplyToAll() + Private Async Function ApplyToAll() As Task Dim allFolders = Application.GetService(Of HomeViewModel)().Folders - - For Each fl In allFolders.Where(Function(f) f.FolderActionState <> ActionState.Analysing AndAlso f.FolderActionState <> ActionState.Working AndAlso f.FolderActionState <> ActionState.Paused) + Dim targetFolders = allFolders.Where( + Function(f) f.FolderActionState <> ActionState.Analysing AndAlso + f.FolderActionState <> ActionState.Working AndAlso + f.FolderActionState <> ActionState.Paused).ToList() + + Dim yesText = LanguageHelper.GetString("UniYes") + Dim noText = "No" + Dim options = Folder.CompressionOptions + Dim message = + $"Apply the settings currently shown for {Folder.DisplayName} to all added folders?{Environment.NewLine}{Environment.NewLine}" & + $"{LanguageHelper.GetString("CompressionMode")}: {options.SelectedCompressionMode}{Environment.NewLine}" & + $"{LanguageHelper.GetString("CompressionConfiguration_SkipFileTypes")}: {If(options.SkipPoorlyCompressedFileTypes, yesText, noText)}{Environment.NewLine}" & + $"{LanguageHelper.GetString("CompressionConfiguration_SkipFileTypesLikelyPoorly")}: {If(options.SkipUserSubmittedFiletypes, yesText, noText)}{Environment.NewLine}" & + $"{LanguageHelper.GetString("CompressionConfiguration_WatchFolderChanges")}: {If(options.WatchFolderForChanges, yesText, noText)}{Environment.NewLine}{Environment.NewLine}" & + "Folders that are currently busy will be left unchanged." + + Dim confirmed = Await Application.GetService(Of IWindowService)().ShowMessageBox( + LanguageHelper.GetString("CompressionConfiguration_ApplyAll"), + message) + If Not confirmed Then Return + + For Each fl In targetFolders If fl IsNot Folder Then fl.CompressionOptions = Folder.CompressionOptions.Clone fl.FolderActionState = ActionState.Idle @@ -250,7 +270,7 @@ Public NotInheritable Class FolderViewModel : Inherits ObservableObject : Implem Next _snackbarService.ShowAppliedToAllFolders() - End Sub + End Function Private Sub Pause() diff --git a/CompactGUI/Views/Pages/FolderView.xaml.vb b/CompactGUI/Views/Pages/FolderView.xaml.vb index 31a44f6c..19056b92 100644 --- a/CompactGUI/Views/Pages/FolderView.xaml.vb +++ b/CompactGUI/Views/Pages/FolderView.xaml.vb @@ -1,9 +1,185 @@ -Public Class FolderView +Imports System.Windows.Controls.Primitives +Imports System.Windows.Media +Imports System.Windows.Shapes +Imports System.Windows.Threading + +Public Class FolderView + + Private Shared ReadOnly AutoFollowDebounce As TimeSpan = TimeSpan.FromSeconds(5) + Private Const MinimumScrollThumbHeight As Double = 28 + + Private _compressionDetailsGrid As DataGrid + Private _scrollThumb As Thumb + Private _stopButton As Wpf.Ui.Controls.Button + Private _isAutoFollowing As Boolean + Private _isScrollThumbDragging As Boolean + Private _suppressAutoFollowUntil As DateTime = DateTime.MinValue + + Public Sub New() + InitializeComponent() + AddHandler LayoutUpdated, AddressOf FolderView_LayoutUpdated + End Sub + + Private Sub FolderView_LayoutUpdated(sender As Object, e As EventArgs) + ConfigureCompressionDetailsGrid() + ConfigureStopButton() + End Sub + + Private Sub ConfigureCompressionDetailsGrid() + Dim grid = FindVisualDescendant(Of DataGrid)(Me) + If grid Is Nothing Then Return + + If Not Object.ReferenceEquals(_compressionDetailsGrid, grid) Then + If _compressionDetailsGrid IsNot Nothing Then + _compressionDetailsGrid.RemoveHandler( + ScrollViewer.ScrollChangedEvent, + New ScrollChangedEventHandler(AddressOf CompressionDetailsGrid_ScrollChanged)) + End If + + _compressionDetailsGrid = grid + _compressionDetailsGrid.AddHandler( + ScrollViewer.ScrollChangedEvent, + New ScrollChangedEventHandler(AddressOf CompressionDetailsGrid_ScrollChanged), + True) + End If + + ConfigureScrollThumb(grid) + End Sub + + Private Sub ConfigureScrollThumb(grid As DataGrid) + Dim verticalScrollBar = FindVisualDescendant(Of ScrollBar)( + grid, + Function(scrollBar) scrollBar.Orientation = Orientation.Vertical) + If verticalScrollBar Is Nothing Then Return + + Dim thumb = FindVisualDescendant(Of Thumb)(verticalScrollBar) + If thumb Is Nothing Then Return + + thumb.MinHeight = MinimumScrollThumbHeight + + If Object.ReferenceEquals(_scrollThumb, thumb) Then Return + + If _scrollThumb IsNot Nothing Then + RemoveHandler _scrollThumb.DragStarted, AddressOf CompressionScrollThumb_DragStarted + RemoveHandler _scrollThumb.DragDelta, AddressOf CompressionScrollThumb_DragDelta + RemoveHandler _scrollThumb.DragCompleted, AddressOf CompressionScrollThumb_DragCompleted + End If + + _scrollThumb = thumb + AddHandler _scrollThumb.DragStarted, AddressOf CompressionScrollThumb_DragStarted + AddHandler _scrollThumb.DragDelta, AddressOf CompressionScrollThumb_DragDelta + AddHandler _scrollThumb.DragCompleted, AddressOf CompressionScrollThumb_DragCompleted + End Sub + + Private Sub CompressionScrollThumb_DragStarted(sender As Object, e As DragStartedEventArgs) + _isScrollThumbDragging = True + SuppressAutoFollow() + End Sub + + Private Sub CompressionScrollThumb_DragDelta(sender As Object, e As DragDeltaEventArgs) + SuppressAutoFollow() + End Sub + + Private Sub CompressionScrollThumb_DragCompleted(sender As Object, e As DragCompletedEventArgs) + _isScrollThumbDragging = False + SuppressAutoFollow() + End Sub + + Private Sub CompressionDetailsGrid_ScrollChanged(sender As Object, e As ScrollChangedEventArgs) + If _isAutoFollowing Then Return + If e.VerticalChange = 0 Then Return + + SuppressAutoFollow() + End Sub + + Private Sub SuppressAutoFollow() + _suppressAutoFollowUntil = DateTime.UtcNow.Add(AutoFollowDebounce) + End Sub Private Sub CompressionDetailsGrid_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Dim grid = TryCast(sender, DataGrid) If grid?.SelectedItem Is Nothing Then Return - grid.ScrollIntoView(grid.SelectedItem) + ConfigureCompressionDetailsGrid() + + If _isScrollThumbDragging OrElse DateTime.UtcNow < _suppressAutoFollowUntil Then Return + + _isAutoFollowing = True + Try + grid.ScrollIntoView(grid.SelectedItem) + grid.UpdateLayout() + Finally + grid.Dispatcher.BeginInvoke( + DispatcherPriority.Background, + New Action(Sub() _isAutoFollowing = False)) + End Try End Sub + + Private Sub ConfigureStopButton() + Dim viewModel = TryCast(DataContext, FolderViewModel) + If viewModel Is Nothing Then Return + + Dim stopButton = FindVisualDescendant(Of Wpf.Ui.Controls.Button)( + Me, + Function(button) Object.ReferenceEquals(button.Command, viewModel.CancelCommand)) + If stopButton Is Nothing OrElse Object.ReferenceEquals(_stopButton, stopButton) Then Return + + _stopButton = stopButton + _stopButton.Content = CreateStopIcon() + End Sub + + Private Shared Function CreateStopIcon() As FrameworkElement + Dim redBrush = TryCast(Application.Current.TryFindResource("PaletteRedBrush"), Brush) + If redBrush Is Nothing Then redBrush = Brushes.IndianRed + + Dim icon As New Grid With { + .Width = 18, + .Height = 18 + } + + Dim octagon As New Polygon With { + .Points = New PointCollection From { + New Point(6, 1), New Point(12, 1), + New Point(17, 6), New Point(17, 12), + New Point(12, 17), New Point(6, 17), + New Point(1, 12), New Point(1, 6) + }, + .Fill = Brushes.Transparent, + .Stroke = redBrush, + .StrokeThickness = 1.7, + .StrokeLineJoin = PenLineJoin.Round + } + + Dim stopSquare As New Rectangle With { + .Width = 6, + .Height = 6, + .HorizontalAlignment = HorizontalAlignment.Center, + .VerticalAlignment = VerticalAlignment.Center, + .Fill = redBrush, + .RadiusX = 1, + .RadiusY = 1 + } + + icon.Children.Add(octagon) + icon.Children.Add(stopSquare) + Return icon + End Function + + Private Shared Function FindVisualDescendant(Of T As DependencyObject)(root As DependencyObject, Optional predicate As Func(Of T, Boolean) = Nothing) As T + If root Is Nothing Then Return Nothing + + For index = 0 To VisualTreeHelper.GetChildrenCount(root) - 1 + Dim child = VisualTreeHelper.GetChild(root, index) + Dim candidate = TryCast(child, T) + + If candidate IsNot Nothing AndAlso (predicate Is Nothing OrElse predicate(candidate)) Then + Return candidate + End If + + Dim nested = FindVisualDescendant(child, predicate) + If nested IsNot Nothing Then Return nested + Next + + Return Nothing + End Function End Class diff --git a/CompactGUI/Views/Pages/HomePage.xaml.vb b/CompactGUI/Views/Pages/HomePage.xaml.vb index 32e38e6c..7c038127 100644 --- a/CompactGUI/Views/Pages/HomePage.xaml.vb +++ b/CompactGUI/Views/Pages/HomePage.xaml.vb @@ -1,4 +1,8 @@ -Class HomePage +Imports System.ComponentModel +Imports System.Windows.Media +Imports System.Windows.Threading + +Class HomePage Private _viewModel As HomeViewModel @@ -12,10 +16,45 @@ ScrollViewer.SetCanContentScroll(Me, False) + AddHandler Loaded, AddressOf HomePage_Loaded + AddHandler _viewModel.PropertyChanged, AddressOf ViewModel_PropertyChanged + ' Add any initialization after the InitializeComponent() call. End Sub + Private Sub HomePage_Loaded(sender As Object, e As RoutedEventArgs) + UpdateCompressButtonText() + End Sub + + Private Sub ViewModel_PropertyChanged(sender As Object, e As PropertyChangedEventArgs) + If e.PropertyName <> NameOf(HomeViewModel.HomeViewModelState) Then Return + Dispatcher.BeginInvoke(AddressOf UpdateCompressButtonText, DispatcherPriority.Loaded) + End Sub + + Private Sub UpdateCompressButtonText() + Dim compressButton = FindVisualDescendant(Of Button)( + Me, + Function(button) Object.ReferenceEquals(button.Command, _viewModel.CompressAllCommand)) + + If compressButton IsNot Nothing Then compressButton.Content = "Compress Now" + End Sub + + Private Shared Function FindVisualDescendant(Of T As DependencyObject)(root As DependencyObject, predicate As Func(Of T, Boolean)) As T + If root Is Nothing Then Return Nothing + + For index = 0 To VisualTreeHelper.GetChildrenCount(root) - 1 + Dim child = VisualTreeHelper.GetChild(root, index) + Dim candidate = TryCast(child, T) + If candidate IsNot Nothing AndAlso predicate(candidate) Then Return candidate + + Dim nested = FindVisualDescendant(child, predicate) + If nested IsNot Nothing Then Return nested + Next + + Return Nothing + End Function + Private Async Sub AddFolderButton_Click(sender As Object, e As RoutedEventArgs) Handles BtnAddFolder1.Click, BtnAddFolder2.Click Dim folderBrowser As New Microsoft.Win32.OpenFolderDialog With { .Title = "Select a folder to compress", From 56aae6120df2f662e0b9c2ba5e29e96c9ed959fc Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:31:27 -0400 Subject: [PATCH 2/3] Refine Compress navigation icon --- CompactGUI/Components/CompactGuiNavigationIcon.vb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CompactGUI/Components/CompactGuiNavigationIcon.vb b/CompactGUI/Components/CompactGuiNavigationIcon.vb index b54fc84c..b2c02961 100644 --- a/CompactGUI/Components/CompactGuiNavigationIcon.vb +++ b/CompactGUI/Components/CompactGuiNavigationIcon.vb @@ -16,10 +16,10 @@ Public NotInheritable Class CompactGuiNavigationIcon .SnapsToDevicePixels = True } - 'Two opposing hollow arrows mirror CompactGUI's application icon without - 'introducing the four-arrow "move inward" glyph used by Fluent Icons. - _northWestArrow = CreateArrowPath("M 2.8,2.8 L 8.2,4.0 L 6.35,5.85 L 9.75,9.25 L 9.25,9.75 L 5.85,6.35 L 4.0,8.2 Z") - _southEastArrow = CreateArrowPath("M 17.2,17.2 L 11.8,16.0 L 13.65,14.15 L 10.25,10.75 L 10.75,10.25 L 14.15,13.65 L 16.0,11.8 Z") + 'CompactGUI's mark is two opposing arrows compacting toward the centre. + 'Keep them as simple open outlines so the navigation glyph reads cleanly at 20 px. + _northWestArrow = CreateArrowPath("M 2.7,2.7 L 9.0,9.0 M 9.0,9.0 L 9.0,5.6 M 9.0,9.0 L 5.6,9.0") + _southEastArrow = CreateArrowPath("M 17.3,17.3 L 11.0,11.0 M 11.0,11.0 L 11.0,14.4 M 11.0,11.0 L 14.4,11.0") canvas.Children.Add(_northWestArrow) canvas.Children.Add(_southEastArrow) @@ -30,7 +30,7 @@ Public NotInheritable Class CompactGuiNavigationIcon Return New Path With { .Data = Geometry.Parse(data), .Stroke = Foreground, - .StrokeThickness = 1.15, + .StrokeThickness = 1.55, .StrokeStartLineCap = PenLineCap.Round, .StrokeEndLineCap = PenLineCap.Round, .StrokeLineJoin = PenLineJoin.Round, From ecc568addbc71e0d03b13268f657d5fd8ce67dd8 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:31:59 -0400 Subject: [PATCH 3/3] Polish compression details grid --- CompactGUI/Views/Pages/FolderView.xaml.vb | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CompactGUI/Views/Pages/FolderView.xaml.vb b/CompactGUI/Views/Pages/FolderView.xaml.vb index 19056b92..adf0e11d 100644 --- a/CompactGUI/Views/Pages/FolderView.xaml.vb +++ b/CompactGUI/Views/Pages/FolderView.xaml.vb @@ -6,6 +6,7 @@ Imports System.Windows.Threading Public Class FolderView Private Shared ReadOnly AutoFollowDebounce As TimeSpan = TimeSpan.FromSeconds(5) + Private Shared ReadOnly ColumnSeparatorBrush As Brush = New SolidColorBrush(Color.FromArgb(&H48, &HFF, &HFF, &HFF)) Private Const MinimumScrollThumbHeight As Double = 28 Private _compressionDetailsGrid As DataGrid @@ -43,9 +44,26 @@ Public Class FolderView True) End If + 'The details table is designed to fit its available width. Do not expose a horizontal + 'scrollbar just because the DataGrid template reserves one for overflow scenarios. + ScrollViewer.SetHorizontalScrollBarVisibility(grid, ScrollBarVisibility.Disabled) + grid.CanUserResizeColumns = True + + ConfigureColumnHeaderSeparators(grid) ConfigureScrollThumb(grid) End Sub + Private Shared Sub ConfigureColumnHeaderSeparators(grid As DataGrid) + ForEachVisualDescendant(Of DataGridColumnHeader)( + grid, + Sub(header) + 'WPF-UI makes the resize boundary obvious only on hover. Keep a subtle line + 'visible at all times while preserving the normal resize gripper hit target. + header.BorderBrush = ColumnSeparatorBrush + header.BorderThickness = New Thickness(0, 0, 1, 0) + End Sub) + End Sub + Private Sub ConfigureScrollThumb(grid As DataGrid) Dim verticalScrollBar = FindVisualDescendant(Of ScrollBar)( grid, @@ -165,6 +183,18 @@ Public Class FolderView Return icon End Function + Private Shared Sub ForEachVisualDescendant(Of T As DependencyObject)(root As DependencyObject, action As Action(Of T)) + If root Is Nothing OrElse action Is Nothing Then Return + + For index = 0 To VisualTreeHelper.GetChildrenCount(root) - 1 + Dim child = VisualTreeHelper.GetChild(root, index) + Dim candidate = TryCast(child, T) + If candidate IsNot Nothing Then action(candidate) + + ForEachVisualDescendant(child, action) + Next + End Sub + Private Shared Function FindVisualDescendant(Of T As DependencyObject)(root As DependencyObject, Optional predicate As Func(Of T, Boolean) = Nothing) As T If root Is Nothing Then Return Nothing