From 74bd0e13e48288bdc74bf959e6b6158f7b258485 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Sun, 23 Aug 2026 18:33:26 +1000 Subject: [PATCH 01/38] Add support for folders in exclusion list - Uses globbing for wildcard support - Also fix bug where a full fileneame would be *included* not excluded from the exclusions - whoops --- CompactGUI.Core/CompactGUI.Core.csproj | 1 + CompactGUI.Core/Compactor.cs | 14 +++-- CompactGUI.Core/SkipListMatcher.cs | 61 +++++++++++++++++++ .../Settings/Settings_skiplistflyout.xaml.vb | 2 +- .../CompressableFolders/CompressableFolder.vb | 6 +- 5 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 CompactGUI.Core/SkipListMatcher.cs diff --git a/CompactGUI.Core/CompactGUI.Core.csproj b/CompactGUI.Core/CompactGUI.Core.csproj index a60df545..b28d8054 100644 --- a/CompactGUI.Core/CompactGUI.Core.csproj +++ b/CompactGUI.Core/CompactGUI.Core.csproj @@ -14,6 +14,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CompactGUI.Core/Compactor.cs b/CompactGUI.Core/Compactor.cs index 0c6071f9..7b43e3d1 100644 --- a/CompactGUI.Core/Compactor.cs +++ b/CompactGUI.Core/Compactor.cs @@ -15,7 +15,7 @@ public sealed class Compactor : ICompressor, IDisposable { private readonly string workingDirectory; - private readonly HashSet excludedFileExtensions; + private readonly HashSet exclusionList; private readonly WOFCompressionAlgorithm wofCompressionAlgorithm; @@ -33,7 +33,7 @@ public sealed class Compactor : ICompressor, IDisposable public Compactor(string folderPath, WOFCompressionAlgorithm compressionLevel, string[] excludedFileTypes, Analyser analyser, ILogger? logger = null) { workingDirectory = folderPath; - excludedFileExtensions = new HashSet(excludedFileTypes); + exclusionList = new HashSet(excludedFileTypes, StringComparer.OrdinalIgnoreCase); wofCompressionAlgorithm = compressionLevel; _logger = logger ?? NullLogger.Instance; _analyser = analyser; @@ -128,16 +128,18 @@ public async Task> BuildWorkingFilesList() var analysedFiles = await _analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token); - var filesList = analysedFiles? + if (analysedFiles is null) return Enumerable.Empty(); + + var excludedFiles = SkipListMatcher.GetExcludedFiles(workingDirectory, analysedFiles.Select(f => f.FileName), exclusionList); + + return analysedFiles .Where(fl => fl.CompressionMode != wofCompressionAlgorithm && fl.UncompressedSize > clusterSize - && ((fl.FileInfo != null && !excludedFileExtensions.Contains(fl.FileInfo.Extension)) || excludedFileExtensions.Contains(fl.FileName)) + && !excludedFiles.Contains(fl.FileName) ) .Select(fl => new FileDetails(fl.FileName, fl.UncompressedSize)) .ToList(); - - return filesList; } diff --git a/CompactGUI.Core/SkipListMatcher.cs b/CompactGUI.Core/SkipListMatcher.cs new file mode 100644 index 00000000..79a52ef0 --- /dev/null +++ b/CompactGUI.Core/SkipListMatcher.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.FileSystemGlobbing; + +namespace CompactGUI.Core; + +public static class SkipListMatcher +{ + public static HashSet GetExcludedFiles(string rootDirectory, IEnumerable allFiles, IEnumerable patterns) + { + var excluded = new HashSet(StringComparer.OrdinalIgnoreCase); + if (!patterns.Any()) return excluded; + + var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); + foreach (string glob in NormalisePatterns(patterns)) + { + matcher.AddInclude(glob); + } + + var relativeFiles = allFiles.Select(f => Path.GetRelativePath(rootDirectory, f).Replace('\\', '/')); + + foreach (FilePatternMatch match in matcher.Match(relativeFiles).Files) + { + excluded.Add(Path.GetFullPath(Path.Combine(rootDirectory, match.Path))); + } + + return excluded; + } + + private static IEnumerable NormalisePatterns(IEnumerable entries) + { + foreach (string entry in entries) + { + string pattern = entry.Trim().Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (pattern.Length == 0) continue; + + bool hasWildcard = pattern.Contains('*') || pattern.Contains('?'); + bool hasSeparator = pattern.Contains(Path.DirectorySeparatorChar) || pattern.Contains(Path.AltDirectorySeparatorChar); + + if (!hasSeparator && IsExtensionPattern(pattern)) + { + // Extension patterns (".mp4", "*.mp4") match any file with that extension at any depth to keep existnig skips working. + yield return "**/*" + (pattern.StartsWith('.') ? pattern : pattern[1..]); + } + else if (!hasWildcard && !hasSeparator) + { + // Legacy exact file names ("Thumbs.db"), matched at any depth. + yield return "**/" + pattern; + } + else + { + yield return NormalisePath(pattern); + } + } + } + + private static bool IsExtensionPattern(string pattern) + { + return pattern.StartsWith('.') || pattern.StartsWith("*."); + } + + private static string NormalisePath(string path) => path.Replace(Path.DirectorySeparatorChar, '/').Replace(Path.AltDirectorySeparatorChar, '/'); +} diff --git a/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb b/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb index 749ec77a..47bb2e1a 100644 --- a/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb +++ b/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb @@ -36,7 +36,7 @@ Public Class Settings_skiplistflyout Return cl.Content.ToString End Function).ToList - _settingsService.AppSettings.NonCompressableList = allObj.Where(Function(c) c.StartsWith("."c) AndAlso c.Length > 1).Distinct().ToList + _settingsService.AppSettings.NonCompressableList = allObj.Select(Function(c) c.Trim()).Where(Function(c) c.Length > 0).Distinct().ToList _settingsService.SaveSettings() PopulateTokens() diff --git a/CompactGUI/Models/CompressableFolders/CompressableFolder.vb b/CompactGUI/Models/CompressableFolders/CompressableFolder.vb index 815050f0..1c25870c 100644 --- a/CompactGUI/Models/CompressableFolders/CompressableFolder.vb +++ b/CompactGUI/Models/CompressableFolders/CompressableFolder.vb @@ -60,8 +60,10 @@ Public MustInherit Class CompressableFolder : Inherits ObservableObject : Implem Public ReadOnly Property GlobalPoorlyCompressedFileCount Get - If AnalysisResults Is Nothing OrElse Application.GetService(Of ISettingsService).AppSettings.NonCompressableList.Count = 0 Then Return 0 - Return AnalysisResults.Where(Function(fl) Application.GetService(Of ISettingsService).AppSettings.NonCompressableList.Contains(New IO.FileInfo(fl.FileName).Extension)).Count + Dim skipList = Application.GetService(Of ISettingsService).AppSettings.NonCompressableList + If AnalysisResults Is Nothing OrElse skipList.Count = 0 Then Return 0 + Dim excluded = SkipListMatcher.GetExcludedFiles(FolderName, AnalysisResults.Select(Function(fl) fl.FileName), skipList) + Return AnalysisResults.Where(Function(fl) excluded.Contains(fl.FileName)).Count End Get End Property From 86e32c4658dbd9ecc6e526229af3c01ffb41c987 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Sun, 23 Aug 2026 18:37:27 +1000 Subject: [PATCH 02/38] Switch skiplist tokeniser to use semicolon and comma instead of space to allow spaces in folder names to work --- CompactGUI/Components/Custom/TokenizedTextBox.vb | 4 +++- .../Components/Settings/Settings_skiplistflyout.xaml.vb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CompactGUI/Components/Custom/TokenizedTextBox.vb b/CompactGUI/Components/Custom/TokenizedTextBox.vb index 68a26f00..ad4123d8 100644 --- a/CompactGUI/Components/Custom/TokenizedTextBox.vb +++ b/CompactGUI/Components/Custom/TokenizedTextBox.vb @@ -14,6 +14,8 @@ Public Class TokenizedTextBox : Inherits RichTextBox Public Property TokenMatcher As Func(Of String, Object) + Public Property TokenDelimiter As String = ";" + Public Sub New() AddHandler TextChanged, AddressOf OnTokenTextChanged @@ -32,7 +34,7 @@ Public Class TokenizedTextBox : Inherits RichTextBox End Sub Public Sub InsertText(text As String) - text &= " " + text &= TokenDelimiter AppendText(text) If TokenMatcher Is Nothing Then Return Dim token = TokenMatcher(text) diff --git a/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb b/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb index 47bb2e1a..611a907d 100644 --- a/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb +++ b/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb @@ -8,7 +8,7 @@ Public Class Settings_skiplistflyout InitializeComponent() _settingsService = Application.GetService(Of ISettingsService)() - UiTokenizedText.TokenMatcher = Function(text) If(text.EndsWith(" "c) OrElse text.EndsWith(";"c) OrElse text.EndsWith(","c), text.Substring(0, text.Length - 1).Trim(), Nothing) + UiTokenizedText.TokenMatcher = Function(text) If(text.EndsWith(";"c) OrElse text.EndsWith(","c), text.Substring(0, text.Length - 1).Trim(), Nothing) PopulateTokens() End Sub From 45827007355a37b4313558fb274d6ac51b70b592 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Sun, 23 Aug 2026 19:12:30 +1000 Subject: [PATCH 03/38] Updated skiplist editor to remove laggy tokenized box --- .../Components/Custom/TokenizedTextBox.vb | 86 ----------- .../Settings/Settings_skiplistflyout.xaml | 134 ++++++++++++------ .../Settings/Settings_skiplistflyout.xaml.vb | 98 +++++++++++-- CompactGUI/Themes/Generic.xaml | 17 +-- 4 files changed, 175 insertions(+), 160 deletions(-) delete mode 100644 CompactGUI/Components/Custom/TokenizedTextBox.vb diff --git a/CompactGUI/Components/Custom/TokenizedTextBox.vb b/CompactGUI/Components/Custom/TokenizedTextBox.vb deleted file mode 100644 index ad4123d8..00000000 --- a/CompactGUI/Components/Custom/TokenizedTextBox.vb +++ /dev/null @@ -1,86 +0,0 @@ -'Credit: https://blog.pixelingene.com/2010/10/tokenizing-control-convert-text-to-tokens - -Public Class TokenizedTextBox : Inherits RichTextBox - - Public Shared ReadOnly TokenTemplateProperty As DependencyProperty = DependencyProperty.Register("TokenTemplate", GetType(DataTemplate), GetType(TokenizedTextBox)) - Public Property TokenTemplate As DataTemplate - Get - Return GetValue(TokenTemplateProperty) - End Get - Set(value As DataTemplate) - SetValue(TokenTemplateProperty, value) - End Set - End Property - - Public Property TokenMatcher As Func(Of String, Object) - - Public Property TokenDelimiter As String = ";" - - Public Sub New() - - AddHandler TextChanged, AddressOf OnTokenTextChanged - - End Sub - - Private Sub OnTokenTextChanged(sender As Object, e As TextChangedEventArgs) - Dim text = CaretPosition.GetTextInRun(LogicalDirection.Backward) - If TokenMatcher Is Nothing Then Return - - Dim token = TokenMatcher(text) - If token IsNot Nothing Then - ReplaceTextWithToken(text, token) - End If - - End Sub - - Public Sub InsertText(text As String) - text &= TokenDelimiter - AppendText(text) - If TokenMatcher Is Nothing Then Return - Dim token = TokenMatcher(text) - If token IsNot Nothing Then - ReplaceTextWithToken(text, token) - End If - End Sub - - Private Sub ReplaceTextWithToken(inputText As String, token As Object) - - RemoveHandler TextChanged, AddressOf OnTokenTextChanged - - Dim para = CaretPosition.Paragraph - - Dim matchedRun As Run = para.Inlines.FirstOrDefault(Function(inline) - Dim run = TryCast(inline, Run) - Return (run IsNot Nothing AndAlso run.Text.EndsWith(inputText)) - End Function) - - If matchedRun IsNot Nothing Then - - Dim tokenContainer = CreateTokenContainer(inputText, token) - para.Inlines.InsertBefore(matchedRun, tokenContainer) - - If matchedRun.Text = inputText Then - para.Inlines.Remove(matchedRun) - Else - Dim index = matchedRun.Text.IndexOf(inputText) + inputText.Length - Dim tailEnd = New Run(matchedRun.Text.Substring(index)) - para.Inlines.InsertAfter(matchedRun, tailEnd) - para.Inlines.Remove(matchedRun) - End If - - End If - - AddHandler TextChanged, AddressOf OnTokenTextChanged - - End Sub - - Private Function CreateTokenContainer(inputText As String, token As Object) As InlineUIContainer - Dim presenter = New ContentPresenter() With { - .Content = token, - .ContentTemplate = TokenTemplate - } - - Return New InlineUIContainer(presenter) With {.BaselineAlignment = BaselineAlignment.Bottom} - - End Function -End Class diff --git a/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml b/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml index cca0717c..7e09c35a 100644 --- a/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml +++ b/CompactGUI/Components/Settings/Settings_skiplistflyout.xaml @@ -5,14 +5,18 @@ xmlns:local="clr-namespace:CompactGUI" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" - Width="620" Height="400" - d:DesignHeight="400" d:DesignWidth="500" - Background="{DynamicResource ApplicationBackgroundBrush}" + Width="620" + Height="400" + d:DesignHeight="400" + d:DesignWidth="500" ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}" + Background="{DynamicResource ApplicationBackgroundBrush}" ExtendsContentIntoTitleBar="True" Foreground="{DynamicResource TextFillColorPrimaryBrush}" - RenderOptions.BitmapScalingMode="HighQuality" RenderOptions.ClearTypeHint="Enabled" - WindowBackdropType="Mica" WindowStartupLocation="CenterScreen" + RenderOptions.BitmapScalingMode="HighQuality" + RenderOptions.ClearTypeHint="Enabled" + WindowBackdropType="Mica" + WindowStartupLocation="CenterScreen" mc:Ignorable="d"> @@ -25,17 +29,51 @@ - - - + Height="28" MinWidth="70" + Margin="5 8 5 0" Padding="10 0" + Background="#7AB3C6D8" BorderThickness="0" CornerRadius="5" Cursor="Hand"> + + + + + + + + + + + @@ -43,39 +81,47 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -43,39 +81,99 @@ - - + + + + + + + - - - - - - - + d:Text="edit skipped filetypes" FontSize="22" FontWeight="SemiBold" + Text="{local:Localize SetFiletypeManagement_LocalSkipFiletypesEdit}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +