diff --git a/CompactGUI.Core/Analyser.cs b/CompactGUI.Core/Analyser.cs index f8118ddf..01f851f6 100644 --- a/CompactGUI.Core/Analyser.cs +++ b/CompactGUI.Core/Analyser.cs @@ -14,6 +14,7 @@ public sealed class Analyser : IDisposable private readonly FolderChangeMonitor _folderMonitor; public bool HasFolderChanged => _folderMonitor.HasChanged; public DateTime LastFolderChanged => _folderMonitor.LastChanged; + public bool IsDirectStorage { get; private set; } public Analyser(string folder, ILogger logger) @@ -65,12 +66,18 @@ private static bool GetContainsCompressedFiles(List fileCom { List? AnalysedFileDetails; + IsDirectStorage = false; AnalyserLog.StartingAnalysis(_logger, FolderName); Stopwatch sw = Stopwatch.StartNew(); try { - var allFiles = await Task.Run(() => Directory.EnumerateFiles(FolderName, "*", new EnumerationOptions { RecurseSubdirectories = true, IgnoreInaccessible = true, AttributesToSkip = FileAttributes.ReparsePoint }).AsShortPathNames(), cancellationToken).ConfigureAwait(false); + var allFiles = (await Task.Run(() => Directory.EnumerateFiles(FolderName, "*", new EnumerationOptions { RecurseSubdirectories = true, IgnoreInaccessible = true, AttributesToSkip = FileAttributes.ReparsePoint }), cancellationToken).ConfigureAwait(false)) + .AsShortPathNames() + .ToList(); + + IsDirectStorage = allFiles.Any(f => Path.GetFileName(f).Equals("dstorage.dll", StringComparison.OrdinalIgnoreCase)); + var fileDetails = allFiles .AsParallel() .WithCancellation(cancellationToken) diff --git a/CompactGUI.Core/CompactGUI.Core.csproj b/CompactGUI.Core/CompactGUI.Core.csproj index c85cd2b8..a7a6005f 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 ddc44c3f..c2a843de 100644 --- a/CompactGUI.Core/Compactor.cs +++ b/CompactGUI.Core/Compactor.cs @@ -308,6 +308,10 @@ public async Task> BuildWorkingFilesList(IReadOnlyColle uint clusterSize = SharedMethods.GetClusterSize(workingDirectory); var analysedFiles = await _analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token) ?? []; + var excludedFiles = SkipListMatcher.GetExcludedFiles( + workingDirectory, + analysedFiles.Select(fl => fl.FileName), + excludedFilesAndExtensions); return analysedFiles .Where(fl => @@ -315,8 +319,7 @@ public async Task> BuildWorkingFilesList(IReadOnlyColle && !resumeExcludedFiles.Contains(fl.FileName) && !notBeneficialCache.ShouldSkip(fl.FileName, wofCompressionAlgorithm) && fl.UncompressedSize > clusterSize - && (fl.FileInfo == null || !excludedFilesAndExtensions.Contains(fl.FileInfo.Extension)) - && !excludedFilesAndExtensions.Contains(fl.FileName) + && !excludedFiles.Contains(fl.FileName) ) .Select(fl => new FileDetails(fl.FileName, fl.UncompressedSize, fl.CompressionMode)) .ToList(); diff --git a/CompactGUI.Core/SkipListMatcher.cs b/CompactGUI.Core/SkipListMatcher.cs new file mode 100644 index 00000000..e3926ca2 --- /dev/null +++ b/CompactGUI.Core/SkipListMatcher.cs @@ -0,0 +1,75 @@ +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, rootDirectory)) + { + 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, string rootDirectory) + { + foreach (string entry in entries) + { + string pattern = entry.Trim().Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (pattern.Length == 0) continue; + + if (Path.IsPathRooted(pattern)) + { + try + { + string relative = Path.GetRelativePath(rootDirectory, pattern).Replace('\\', '/'); + if (!relative.StartsWith("../", StringComparison.Ordinal) && relative != "..") + { + pattern = relative; + } + } + catch (ArgumentException) + { + // Unrelated roots; leave as-is (won't match). + } + } + + bool hasWildcard = pattern.Contains('*') || pattern.Contains('?'); + bool hasSeparator = pattern.Contains(Path.DirectorySeparatorChar) || pattern.Contains(Path.AltDirectorySeparatorChar); + + if (!hasSeparator && IsExtensionPattern(pattern)) + { + yield return "**/*" + (pattern.StartsWith('.') ? pattern : pattern[1..]); + } + else if (!hasWildcard && !hasSeparator) + { + 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.CoreVB/Analyser.vb b/CompactGUI.CoreVB/Analyser.vb deleted file mode 100644 index e303a457..00000000 --- a/CompactGUI.CoreVB/Analyser.vb +++ /dev/null @@ -1,172 +0,0 @@ -Imports System.IO -Imports System.Security.AccessControl -Imports System.Security.Principal -Imports System.Threading - -Public Class Analyser - - Public Property FolderName As String - Public Property UncompressedBytes As Long - Public Property CompressedBytes As Long - Public Property ContainsCompressedFiles As Boolean - Public Property FileCompressionDetailsList As List(Of AnalysedFileDetails) - - Public Sub New(folder As String) - FolderName = folder - End Sub - - - Public Async Function AnalyseFolder(cancellationToken As CancellationToken) As Task(Of Boolean) - Dim allFiles = Await Task.Run(Function() Directory.EnumerateFiles(FolderName, "*", New EnumerationOptions() With {.RecurseSubdirectories = True, .IgnoreInaccessible = True}).AsShortPathNames, cancellationToken).ConfigureAwait(False) - Dim fileDetails As New List(Of AnalysedFileDetails) - Dim compressedFilesCount As Integer = 0 - - ' Use local variables to reduce contention - Dim localCompressedBytes As Long = 0 - Dim localUncompressedBytes As Long = 0 - - Try - Parallel.ForEach(allFiles, New ParallelOptions With {.CancellationToken = cancellationToken}, - Sub(file) - Dim details = AnalyseFile(file) - If details IsNot Nothing Then - SyncLock fileDetails - fileDetails.Add(details) - End SyncLock - If details.CompressionMode <> WOFCompressionAlgorithm.NO_COMPRESSION Then - Interlocked.Increment(compressedFilesCount) - End If - Interlocked.Add(localCompressedBytes, details.CompressedSize) - Interlocked.Add(localUncompressedBytes, details.UncompressedSize) - End If - End Sub) - - ' Update the shared state after the parallel loop to minimize contention - CompressedBytes = localCompressedBytes - UncompressedBytes = localUncompressedBytes - Catch ex As OperationCanceledException - Debug.WriteLine(ex.Message) - Return Nothing - End Try - - ContainsCompressedFiles = compressedFilesCount > 0 - FileCompressionDetailsList = fileDetails - Return ContainsCompressedFiles - End Function - - - Private Function AnalyseFile(file As String) As AnalysedFileDetails - Try - Dim fInfo As New FileInfo(file) - Dim unCompSize = fInfo.Length - Dim compSize = GetFileSizeOnDisk(file) - If compSize < 0 Then - compSize = unCompSize ' GetFileSizeOnDisk failed, fall back to unCompSize - End If - Dim cLevel As WOFCompressionAlgorithm = If(compSize = unCompSize, WOFCompressionAlgorithm.NO_COMPRESSION, DetectCompression(fInfo)) - - Return New AnalysedFileDetails With {.FileName = file, .CompressedSize = compSize, .UncompressedSize = unCompSize, .CompressionMode = cLevel, .FileInfo = fInfo} - Catch ex As IOException - Debug.WriteLine($"Error analysing file {file}: {ex.Message}") - Return Nothing - End Try - End Function - - - Public Async Function GetPoorlyCompressedExtensions() As Task(Of List(Of ExtensionResult)) - Dim extClassResults As New List(Of ExtensionResult) - Await Task.Run( - Sub() - Dim extRes As New Concurrent.ConcurrentDictionary(Of String, ExtensionResult) - Parallel.ForEach(FileCompressionDetailsList, - Sub(fl) - Dim xt = New FileInfo(fl.FileName).Extension - If fl.UncompressedSize = 0 Then Return - - extRes.AddOrUpdate(xt, - Function(addKey) ' Add value factory - Return New ExtensionResult With { - .extension = xt, - .totalFiles = 1, - .uncompressedBytes = fl.UncompressedSize, - .compressedBytes = fl.CompressedSize - } - End Function, - Function(updateKey, oldValue) ' Update value factory - Return New ExtensionResult With { - .extension = xt, - .totalFiles = oldValue.totalFiles + 1, - .uncompressedBytes = oldValue.uncompressedBytes + fl.UncompressedSize, - .compressedBytes = oldValue.compressedBytes + fl.CompressedSize - } - End Function) - End Sub) - - ' Filter and convert to list after aggregation - extClassResults = extRes.Values.Where(Function(f) f.cRatio > 0.95).ToList() - End Sub) - - Return extClassResults - End Function - - - Private Function DetectCompression(fInfo As FileInfo) As WOFCompressionAlgorithm - - Dim isextFile As Integer - Dim prov As ULong - Dim info As WOF_FILE_COMPRESSION_INFO_V1 - Dim buf As UInt16 = 8 - - Dim ret = WofIsExternalFile(fInfo.FullName, isextFile, prov, info, buf) - - Dim algorithm As WOFCompressionAlgorithm = info.Algorithm - - If isextFile = 0 Then algorithm = WOFCompressionAlgorithm.NO_COMPRESSION - If (fInfo.Attributes And 2048) <> 0 Then algorithm = WOFCompressionAlgorithm.LZNT1 - Return algorithm - - End Function - - - Public Shared Function HasDirectoryWritePermission(FolderName As String) As Boolean - Try - Dim directoryInfo = New DirectoryInfo(FolderName) - Dim directorySecurity = directoryInfo.GetAccessControl() - - Dim user = WindowsIdentity.GetCurrent() - Dim userSID = user.User - Dim userGroupSIDs = user.Groups - - Dim accessRules = directorySecurity.GetAccessRules(True, True, GetType(SecurityIdentifier)) - - Dim writeAllowed = False - Dim writeDenied = False - - For Each rule As FileSystemAccessRule In accessRules - Dim fileSystemRights = rule.FileSystemRights - If (fileSystemRights And FileSystemRights.Write) > 0 Then - Dim ruleSID = DirectCast(rule.IdentityReference, SecurityIdentifier) - - ' Check if the rule applies to the user or any of the user's groups - If ruleSID.Equals(userSID) OrElse userGroupSIDs.Contains(ruleSID) Then - If rule.AccessControlType = AccessControlType.Allow Then - writeAllowed = True - ElseIf rule.AccessControlType = AccessControlType.Deny Then - writeDenied = True - Exit For - End If - End If - End If - Next - - ' Write permission is considered available if it's explicitly allowed and not explicitly denied - Return writeAllowed And Not writeDenied - Catch ex As UnauthorizedAccessException - - Return False - End Try - End Function - - - -End Class diff --git a/CompactGUI.CoreVB/CompactGUI.CoreVB.vbproj b/CompactGUI.CoreVB/CompactGUI.CoreVB.vbproj deleted file mode 100644 index 7b5b9942..00000000 --- a/CompactGUI.CoreVB/CompactGUI.CoreVB.vbproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - CompactGUI.CoreVB - net10.0-windows - - - - none - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - diff --git a/CompactGUI.CoreVB/Compactor.vb b/CompactGUI.CoreVB/Compactor.vb deleted file mode 100644 index c17435d7..00000000 --- a/CompactGUI.CoreVB/Compactor.vb +++ /dev/null @@ -1,159 +0,0 @@ -Imports System.IO -Imports System.Runtime.InteropServices -Imports System.Threading - -Imports Microsoft.Win32.SafeHandles - - -Public Class Compactor : Implements IDisposable, ICompressor - - Private ReadOnly workingDirectory As String - Private ReadOnly excludedFileExtensions() As String - Private ReadOnly wofCompressionAlgorithm As WOFCompressionAlgorithm - - Private compressionInfoPtr As IntPtr - Private compressionInfoSize As UInteger - - Private totalProcessedBytes As Long = 0 - Private ReadOnly pauseSemaphore As New SemaphoreSlim(1, 2) - Private ReadOnly cancellationTokenSource As New CancellationTokenSource - - - Public Sub New(folder As String, compressionLevel As WOFCompressionAlgorithm, excludedFilesTypes As String()) - - workingDirectory = folder - wofCompressionAlgorithm = compressionLevel - excludedFileExtensions = excludedFilesTypes - - InitializeCompressionInfoPointer() - - End Sub - - - Private Sub InitializeCompressionInfoPointer() - Dim _EFInfo As New WOF_FILE_COMPRESSION_INFO_V1 With {.Algorithm = wofCompressionAlgorithm, .Flags = 0} - compressionInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_EFInfo)) - compressionInfoSize = CUInt(Marshal.SizeOf(_EFInfo)) - Marshal.StructureToPtr(_EFInfo, compressionInfoPtr, True) - End Sub - - - Public Async Function RunAsync(filesList As List(Of String), Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing, Optional MaxParallelism As Integer = 1) As Task(Of Boolean) Implements ICompressor.RunAsync - - If cancellationTokenSource.IsCancellationRequested Then Return False - - Dim workingFiles = Await BuildWorkingFilesList().ConfigureAwait(False) - Dim totalFilesSize As Long = workingFiles.Sum(Function(f) f.UncompressedSize) - totalProcessedBytes = 0 - - If MaxParallelism <= 0 Then MaxParallelism = Environment.ProcessorCount - - Dim paraOptions As New ParallelOptions With {.MaxDegreeOfParallelism = MaxParallelism} - - Try - Await Parallel.ForEachAsync(workingFiles, paraOptions, - Function(file, _ctx) As ValueTask - _ctx.ThrowIfCancellationRequested() - Return New ValueTask(PauseAndProcessFile(file, totalFilesSize, cancellationTokenSource.Token, progressMonitor)) - End Function).ConfigureAwait(False) - Catch ex As OperationCanceledException - ' Swallow cancellation, return false - Return False - End Try - - Return Not cancellationTokenSource.IsCancellationRequested - - End Function - - Private Async Function PauseAndProcessFile(details As FileDetails, totalFilesSize As Long, _ctx As CancellationToken, Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing) As Task - - Try - Await pauseSemaphore.WaitAsync(_ctx).ConfigureAwait(False) - pauseSemaphore.Release() - Catch ex As OperationCanceledException - Throw - Return - End Try - - _ctx.ThrowIfCancellationRequested() - - Dim res = WOFCompressFile(details.FileName) - Interlocked.Add(totalProcessedBytes, details.UncompressedSize) - progressMonitor?.Report(New CompressionProgress(totalProcessedBytes / totalFilesSize * 100, details.FileName)) - - - End Function - - - - Public Sub Pause() Implements ICompressor.Pause - pauseSemaphore.Wait() - End Sub - - - Public Sub [Resume]() Implements ICompressor.Resume - If pauseSemaphore.CurrentCount = 0 Then pauseSemaphore.Release() - End Sub - - Public Sub Cancel() Implements ICompressor.Cancel - [Resume]() - cancellationTokenSource.Cancel() - End Sub - - - - Private Function WOFCompressFile(path As String) As Integer - - Try - Using fs As SafeFileHandle = File.OpenHandle(path) - Return WofSetFileDataLocation(fs, WOF_PROVIDER_FILE, compressionInfoPtr, compressionInfoSize) - End Using - Catch ex As Exception - Debug.WriteLine(ex.Message) - Return Nothing - End Try - - End Function - - Private Async Function BuildWorkingFilesList() As Task(Of IEnumerable(Of FileDetails)) - - Dim clusterSize As Integer = GetClusterSize(workingDirectory) - - Dim _filesList As New Concurrent.ConcurrentBag(Of FileDetails) - 'TODO: if the user has already analysed within the last minute, then skip creating a new one and use the old one - Dim ax As New Analyser(workingDirectory) - Dim ret = Await ax.AnalyseFolder(Nothing) - - Parallel.ForEach(ax.FileCompressionDetailsList, Sub(fl) - Dim ft = fl.FileInfo - If Not (excludedFileExtensions.Contains(ft.Extension) OrElse excludedFileExtensions.Contains(fl.FileName)) AndAlso - ft.Length > clusterSize AndAlso - fl.CompressionMode <> wofCompressionAlgorithm Then - _filesList.Add((New FileDetails With {.FileName = fl.FileName, .UncompressedSize = fl.UncompressedSize})) - End If - End Sub) - - - Return _filesList.ToList - End Function - - - Public Sub Dispose() Implements IDisposable.Dispose - cancellationTokenSource?.Dispose() - pauseSemaphore?.Dispose() - If Not compressionInfoPtr.Equals(IntPtr.Zero) Then - Marshal.FreeHGlobal(compressionInfoPtr) - compressionInfoPtr = IntPtr.Zero - End If - End Sub - - - - Private Structure FileDetails - Public Property FileName As String - Public Property UncompressedSize As Long - - End Structure - - -End Class diff --git a/CompactGUI.CoreVB/Estimator.vb b/CompactGUI.CoreVB/Estimator.vb deleted file mode 100644 index 87e7903d..00000000 --- a/CompactGUI.CoreVB/Estimator.vb +++ /dev/null @@ -1,232 +0,0 @@ -Imports System.IO -Imports System.IO.Compression -Imports System.Runtime.InteropServices - -Imports K4os.Compression.LZ4 - -Imports K4os.Compression.LZ4.Streams - -Imports Microsoft.Win32.SafeHandles - - -'https://unix.stackexchange.com/questions/155901/estimate-compressibility-of-file - -Public Class Estimator - Private Const BlockSize As Integer = 8192 '16K? Should I get it from the ClusterSize? - Private Const ZScore As Single = 1.96 - Private Const ErrorMargin As Single = 0.1 - Private Const SampleSize As Single = 100 '0.25 * (ZScore / ErrorMargin) ^ 2 - - Private DiskClusterSize As Integer = 4096 - Private IsHDD As Boolean = False - - Public Sub New() - End Sub - - - Public Delegate Function CompressionStreamFactory(output As Stream) As Stream - - - Public Class FileDetails - Public AnalysedFile As AnalysedFileDetails - Public CompressionRatio As Single - End Class - - Public Shared IsAlternate As Boolean = False - - Public Function EstimateCompressability(analysisResult As List(Of AnalysedFileDetails), ishdd As Boolean, Optional MaxParallelism As Integer = 1, Optional clusterSize As Integer = 4096, Optional cancellationToken As Threading.CancellationToken = Nothing) As List(Of (AnalysedFile As AnalysedFileDetails, CompressionRatio As Single)) - - DiskClusterSize = clusterSize - Dim _filesList As New Concurrent.ConcurrentBag(Of FileDetails) - If MaxParallelism <= 0 Then MaxParallelism = Environment.ProcessorCount - - Me.IsHDD = ishdd - - - 'Filter the files based on the cluster size and sort them by cluster location if it's an HDD to minimize seek time - Dim filteredList = analysisResult.Where(Function(f) f.UncompressedSize > clusterSize) - - Dim sw = Stopwatch.StartNew() - - If ishdd Then filteredList = filteredList.OrderBy(Function(f) GetFirstLcn(f.FileName)) - Dim finalList = filteredList.ToList - sw.Stop() - Debug.WriteLine($"Filtered and sorted {finalList.Count} files in {sw.ElapsedMilliseconds} ms") - If IsAlternate Then Debug.WriteLine("Alternate mode enabled, using different handle method.") - - - Dim paraOptions As New ParallelOptions With {.MaxDegreeOfParallelism = MaxParallelism} - - Parallel.ForEach(finalList, parallelOptions:=paraOptions, Sub(fl) - cancellationToken.ThrowIfCancellationRequested() - - Dim estimatedRatio = EstimateCompressabilityLZ4(fl.FileName, fl.UncompressedSize, cancellationToken) - - _filesList.Add((New FileDetails With {.AnalysedFile = fl, .CompressionRatio = estimatedRatio})) - End Sub) - - Dim retList As New List(Of (AnalysedFile As AnalysedFileDetails, CompressionRatio As Single)) - - For Each item In _filesList - retList.Add((item.AnalysedFile, item.CompressionRatio)) - Next - - IsAlternate = Not IsAlternate ' Toggle the alternate mode for next run - - Return retList - - End Function - - - Public Function EstimateCompressabilityGZip(path As String, filesize As Long) As Double - - Using fs As FileStream = File.OpenRead(path) - Return EstimateCompressability(fs, filesize, Function(output) New GZipStream(output, CompressionLevel.Fastest, True)) - End Using - - End Function - - - Public Function EstimateCompressabilitySnappy(path As String, filesize As Long) As Double - - Using fs As FileStream = File.OpenRead(path) - Return EstimateCompressability(fs, filesize, Function(output) New Snappier.SnappyStream(output, Compression.CompressionMode.Compress, True)) - End Using - - End Function - - Public Function EstimateCompressabilityLZ4(path As String, filesize As Long, Optional cancellationToken As Threading.CancellationToken = Nothing) As Double - Try - Using fs As FileStream = File.OpenRead(path) - If IsHDD Then - Return EstimateCompressabilityHDD(fs, filesize, Function(output) LZ4Stream.Encode(output, LZ4Level.L00_FAST, 0, True), cancellationToken) - End If - Return EstimateCompressability(fs, filesize, Function(output) LZ4Stream.Encode(output, LZ4Level.L00_FAST, 0, True), cancellationToken) - End Using - Catch cancelledEx As OperationCanceledException - Throw - Catch ex As Exception - Return 1.0 ' If there's an error, assume it's incompressible - End Try - - End Function - - - Private Function EstimateCompressability(input As FileStream, fileSize As Long, compressionFactory As CompressionStreamFactory, Optional cancellationToken As Threading.CancellationToken = Nothing) As Double - - Dim totalBlocks As Long = fileSize \ BlockSize - Dim samplesNeeded As Integer = Math.Ceiling((totalBlocks * SampleSize) / (SampleSize + totalBlocks - 1)) - Dim totalWritten As Long = 0 - - Dim compressed = New MemoryStream() - Using compressionStream As Stream = compressionFactory(compressed) - - ' If file is small or sample count is zero, compress the whole file - If samplesNeeded = 0 OrElse fileSize < samplesNeeded * BlockSize * 2 Then - - Dim buffer(BlockSize - 1) As Byte - input.Position = 0 - Dim bytesRead As Integer = input.Read(buffer, 0, BlockSize) - - While bytesRead > 0 - cancellationToken.ThrowIfCancellationRequested() - compressionStream.Write(buffer, 0, bytesRead) - totalWritten += bytesRead - bytesRead = input.Read(buffer, 0, BlockSize) - End While - - Else - ' Sample evenly spaced blocks - Dim stepSize As Long = BlockSize * (totalBlocks \ samplesNeeded) - Dim buffer(BlockSize - 1) As Byte - For i As Integer = 0 To samplesNeeded - 1 - cancellationToken.ThrowIfCancellationRequested() - input.Position = stepSize * i - Dim bytesRead As Integer = input.Read(buffer, 0, BlockSize) - compressionStream.Write(buffer, 0, bytesRead) - totalWritten += bytesRead - Next - End If - End Using - - Return Math.Min(compressed.Length / Math.Max(totalWritten, 1), 1.0) - End Function - - - Private Function EstimateCompressabilityHDD(input As FileStream, fileSize As Long, compressionFactory As CompressionStreamFactory, Optional cancellationToken As Threading.CancellationToken = Nothing) As Double - Dim NumClusters As Integer = SampleSize ' or any small number you want to sample - Dim clusterSize As Integer = DiskClusterSize - - Dim middleCluster As Long = (fileSize \ 2) \ clusterSize - Dim alignedStart As Long = middleCluster * clusterSize - Dim chunkSize As Integer = CInt(Math.Min(clusterSize * NumClusters, fileSize - alignedStart)) - - Dim totalWritten As Long = 0 - Dim compressed = New MemoryStream() - - Using compressionStream As Stream = compressionFactory(compressed) - Dim buffer(chunkSize - 1) As Byte - input.Position = alignedStart - Dim bytesRead As Integer = input.Read(buffer, 0, chunkSize) - - If cancellationToken <> Nothing AndAlso cancellationToken.IsCancellationRequested Then - Throw New OperationCanceledException(cancellationToken) - End If - - If bytesRead > 0 Then - compressionStream.Write(buffer, 0, bytesRead) - totalWritten += bytesRead - End If - End Using - - Return Math.Min(compressed.Length / Math.Max(totalWritten, 1), 1.0) - End Function - - Public Function GetFirstLcn(filePath As String) As Long - - Dim handle As SafeFileHandle = File.OpenHandle(filePath) - - If handle.IsInvalid Then Throw New IOException("Failed to open file handle.") - - - Dim inBuffer As NtfsInterop.STARTING_VCN_INPUT_BUFFER - inBuffer.StartingVcn = 0 - Dim inBufferSize = Marshal.SizeOf(inBuffer) - Dim inBufferPtr = Marshal.AllocHGlobal(inBufferSize) - Marshal.StructureToPtr(inBuffer, inBufferPtr, False) - - Dim outBufferSize = 4096 - Dim outBufferPtr = Marshal.AllocHGlobal(outBufferSize) - Dim bytesReturned As Integer = 0 - - Try - Dim result = WOFHelper.DeviceIoControl( - handle, - NtfsInterop.FSCTL_GET_RETRIEVAL_POINTERS, - inBufferPtr, - inBufferSize, - outBufferPtr, - outBufferSize, - bytesReturned, - IntPtr.Zero - ) - - - - 'Probably errors because the file is empty - If Not result Then Return Long.MaxValue - - - ' Marshal the output buffer to get the first LCN - Dim extentOffset As Integer = Marshal.OffsetOf(Of RETRIEVAL_POINTERS_BUFFER)("Extents").ToInt32() - Dim lcn As Long = Marshal.ReadInt64(outBufferPtr, extentOffset + 8) - Return lcn - - Finally - Marshal.FreeHGlobal(inBufferPtr) - Marshal.FreeHGlobal(outBufferPtr) - handle.Close() - End Try - End Function - -End Class diff --git a/CompactGUI.CoreVB/FodyWeavers.xml b/CompactGUI.CoreVB/FodyWeavers.xml deleted file mode 100644 index 003b8f77..00000000 --- a/CompactGUI.CoreVB/FodyWeavers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/CompactGUI.CoreVB/ICompressor.vb b/CompactGUI.CoreVB/ICompressor.vb deleted file mode 100644 index 0fadeabb..00000000 --- a/CompactGUI.CoreVB/ICompressor.vb +++ /dev/null @@ -1,7 +0,0 @@ -Public Interface ICompressor : Inherits IDisposable - Function RunAsync(filesList As List(Of String), Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing, Optional maxParallelism As Integer = 1) As Task(Of Boolean) - Sub Pause() - Sub [Resume]() - Sub Cancel() - -End Interface diff --git a/CompactGUI.CoreVB/NtfsInterop.vb b/CompactGUI.CoreVB/NtfsInterop.vb deleted file mode 100644 index 560566b0..00000000 --- a/CompactGUI.CoreVB/NtfsInterop.vb +++ /dev/null @@ -1,42 +0,0 @@ -Imports System.Runtime.InteropServices - -Imports Microsoft.Win32.SafeHandles - -Friend Module NtfsInterop - Public Const FSCTL_GET_RETRIEVAL_POINTERS As UInteger = &H90073 - Public Const OPEN_EXISTING As Integer = 3 - Public Const FILE_FLAG_BACKUP_SEMANTICS As Integer = &H2000000 - Public Const FILE_SHARE_READ As Integer = 1 - Public Const FILE_SHARE_WRITE As Integer = 2 - Public Const GENERIC_READ As Integer = &H80000000 - - - Public Function CreateFile( - lpFileName As String, - dwDesiredAccess As Integer, - dwShareMode As Integer, - lpSecurityAttributes As IntPtr, - dwCreationDisposition As Integer, - dwFlagsAndAttributes As Integer, - hTemplateFile As IntPtr - ) As SafeFileHandle - End Function - - - Public Structure STARTING_VCN_INPUT_BUFFER - Public StartingVcn As Long - End Structure - - - Public Structure RETRIEVAL_POINTERS_BUFFER - Public ExtentCount As Integer - Public StartingVcn As Long - Public Extents As LCN_EXTENT ' This is actually an array, but we only need the first. ?Perhaps I should get the LCN of the middle cluster in future? - End Structure - - - Public Structure LCN_EXTENT - Public NextVcn As Long - Public Lcn As Long - End Structure -End Module \ No newline at end of file diff --git a/CompactGUI.CoreVB/SharedMethods.vb b/CompactGUI.CoreVB/SharedMethods.vb deleted file mode 100644 index 85214b29..00000000 --- a/CompactGUI.CoreVB/SharedMethods.vb +++ /dev/null @@ -1,199 +0,0 @@ -Imports System.IO -Imports System.Runtime.CompilerServices -Imports System.Runtime.InteropServices -Imports System.Text -Public Module SharedMethods - Public Enum FolderVerificationResult - Valid = 0 - DirectoryDoesNotExist - SystemDirectory - RootDirectory - DirectoryEmptyOrUnauthorized - OneDriveFolder - NonNTFSDrive - InsufficientPermission - End Enum - - Function verifyFolder(folder As String) As FolderVerificationResult - - If Not Directory.Exists(folder) Then : Return FolderVerificationResult.DirectoryDoesNotExist - ElseIf folder.ToLowerInvariant.Contains((Environment.GetFolderPath(Environment.SpecialFolder.Windows)).ToLowerInvariant) Then : Return FolderVerificationResult.SystemDirectory - ElseIf folder.EndsWith(":\") Then : Return FolderVerificationResult.RootDirectory - ElseIf IsDirectoryEmptySafe(folder) Then : Return FolderVerificationResult.DirectoryEmptyOrUnauthorized - ElseIf IsOneDriveFolder(folder) Then : Return FolderVerificationResult.OneDriveFolder - ElseIf DriveInfo.GetDrives().First(Function(f) folder.StartsWith(f.Name)).DriveFormat <> "NTFS" Then : Return FolderVerificationResult.NonNTFSDrive - ElseIf Not Analyser.HasDirectoryWritePermission(folder) Then : Return FolderVerificationResult.InsufficientPermission - End If - - Return FolderVerificationResult.Valid - - End Function - - - Function GetFolderVerificationMessage(result As FolderVerificationResult) As String - Select Case result - Case FolderVerificationResult.Valid - Return "" - Case FolderVerificationResult.DirectoryDoesNotExist - Return "Directory does not exist" - Case FolderVerificationResult.SystemDirectory - Return "Cannot compress system directory" - Case FolderVerificationResult.RootDirectory - Return "Cannot compress root directory" - Case FolderVerificationResult.DirectoryEmptyOrUnauthorized - Return "This directory is either empty or you are not authorized to access its files." - Case FolderVerificationResult.OneDriveFolder - Return "Files synced with OneDrive cannot be compressed as they use a different storage structure" - Case FolderVerificationResult.NonNTFSDrive - Return "Cannot compress a directory on a non-NTFS drive" - Case FolderVerificationResult.InsufficientPermission - Return "Insufficient permission to access this folder." - Case Else - Return "Unknown error" - End Select - End Function - - - - Function IsDirectoryEmptySafe(folder As String) - - Try - Return Not Directory.EnumerateFileSystemEntries(folder).Any() - - - Catch ex As UnauthorizedAccessException - Return False - - Catch ex As Exception - Return False - End Try - - End Function - - Function GetFileSizeOnDisk(file As String) As Long - Dim hosize As UInteger - Dim losize As UInteger = GetCompressedFileSizeW(file, hosize) - 'INVALID_FILE_SIZE (0xFFFFFFFF) - If losize = 4294967295 Then - Dim errCode As Integer - errCode = Marshal.GetLastPInvokeError() - If errCode <> 0 Then - Return -1 - End If - End If - Return CLng(hosize) << 32 Or losize - End Function - - - - Function AsShortPathNames(filesList As IEnumerable(Of String)) As List(Of String) - - Return filesList.Select(Of String) _ - (Function(fl) - If fl.Length >= 255 Then - Dim sfp = GetShortPath(fl) - If sfp IsNot Nothing Then Return sfp - End If - Return fl - End Function).ToList - - End Function - - - Function GetShortPath(filePath As String) As String - - If String.IsNullOrWhiteSpace(filePath) Then Return Nothing - Dim hasPrefix As Boolean = False - - If filePath.Length >= 255 AndAlso Not filePath.StartsWith("\\?\") Then - filePath = "\\?\" & filePath - hasPrefix = True - End If - - Dim shortPath = New StringBuilder(1024) - Dim res As Integer = GetShortPathName(filePath, shortPath, 1024) - If res = 0 Then Return Nothing - filePath = shortPath.ToString() - If hasPrefix Then filePath = filePath.Substring(4) - Return filePath - - End Function - - - Function GetClusterSize(folderPath As String) - - Dim lpSectorsPerCluster As UInteger - Dim lpBytesPerSector As UInteger - Dim res As Integer = GetDiskFreeSpace(New DirectoryInfo(folderPath).Root.ToString, lpSectorsPerCluster, lpBytesPerSector, Nothing, Nothing) - Return lpSectorsPerCluster * lpBytesPerSector - - End Function - - Function IsOneDriveFolder(folderPath As String) As Boolean - Dim userProfile As String = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) - Dim oneDrivePaths As New List(Of String) From { - Path.Combine(userProfile, "OneDrive"), ' Personal OneDrive - Path.Combine(userProfile, "OneDrive - Personal"), ' Alternative Personal OneDrive - Path.Combine(userProfile, "OneDrive for Business"), ' OneDrive for Business - Path.Combine(userProfile, "OneDrive - Business") ' Alternative OneDrive for Business - } - - ' Normalize the folder path to compare - Dim normalizedFolderPath As String = Path.GetFullPath(folderPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant() - - ' Check if the folder path starts with any of the known OneDrive paths - Return oneDrivePaths.Any(Function(odPath) normalizedFolderPath.StartsWith(Path.GetFullPath(odPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant())) - End Function - - Public Sub PreventSleep() - SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS Or EXECUTION_STATE.ES_SYSTEM_REQUIRED Or EXECUTION_STATE.ES_DISPLAY_REQUIRED) - End Sub - Public Sub RestoreSleep() - SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS) - End Sub - - -#Region "DLL Imports" - - - Private Function GetCompressedFileSizeW( - <[In](), MarshalAs(UnmanagedType.LPWStr)> lpFileName As String, - ByRef lpFileSizeHigh As UInteger) _ - As UInteger - End Function - - - Private Function GetShortPathName( - path As String, - shortPath As StringBuilder, shortPathLength As Integer) As Integer - - End Function - - - - - Private Function GetDiskFreeSpace( - lpRootPathName As String, - ByRef lpSectorsPerCluster As UInteger, - ByRef lpBytesPerSector As UInteger, - ByRef lpNumberOfFreeClusters As UInteger, - ByRef lpTotalNumberOfClusters As UInteger) As Boolean - End Function - - - - Private Function SetThreadExecutionState(esFlags As EXECUTION_STATE) As EXECUTION_STATE - End Function - - - Private Enum EXECUTION_STATE As UInteger - ES_AWAYMODE_REQUIRED = &H40 - ES_CONTINUOUS = &H80000000UI - ES_DISPLAY_REQUIRED = &H2 - ES_SYSTEM_REQUIRED = &H1 - End Enum - -#End Region - - -End Module diff --git a/CompactGUI.CoreVB/SharedObjects.vb b/CompactGUI.CoreVB/SharedObjects.vb deleted file mode 100644 index b0ddb85b..00000000 --- a/CompactGUI.CoreVB/SharedObjects.vb +++ /dev/null @@ -1,54 +0,0 @@ -Public Class AnalysedFileDetails - - Public Property FileName As String - Public Property UncompressedSize As Long - Public Property CompressedSize As Long - Public Property CompressionMode As WOFCompressionAlgorithm - Public Property FileInfo As IO.FileInfo -End Class - - -'Used to track efficiency of compression and built results for submission to wiki -Public Class ExtensionResult - - Public Property extension As String - Public Property uncompressedBytes As Long - Public Property compressedBytes As Long - Public Property totalFiles As Integer - ReadOnly Property cRatio As Decimal - Get - Return Math.Round(compressedBytes / uncompressedBytes, 2) - End Get - End Property - -End Class - -Public Structure CompressionProgress - Public ProgressPercent As Integer - Public FileName As String - - Public Sub New(_progressPercent As Integer, _fileName As String) - ProgressPercent = _progressPercent - FileName = _fileName - End Sub - -End Structure - - - -Public Enum CompressionMode - XPRESS4K - XPRESS8K - XPRESS16K - LZX - None -End Enum - -Public Enum WOFCompressionAlgorithm - NO_COMPRESSION = -2 - LZNT1 = -1 - XPRESS4K = 0 - LZX = 1 - XPRESS8K = 2 - XPRESS16K = 3 -End Enum \ No newline at end of file diff --git a/CompactGUI.CoreVB/Uncompactor.vb b/CompactGUI.CoreVB/Uncompactor.vb deleted file mode 100644 index 6d6aae15..00000000 --- a/CompactGUI.CoreVB/Uncompactor.vb +++ /dev/null @@ -1,85 +0,0 @@ -Imports System.IO -Imports System.Threading - -Imports Microsoft.Win32.SafeHandles - -Public Class Uncompactor : Implements ICompressor, IDisposable - - - Private _pauseSemaphore As New SemaphoreSlim(1, 2) - Private _processedFileCount As New Concurrent.ConcurrentDictionary(Of String, Integer) - Private _cancellationTokenSource As New CancellationTokenSource - - - Public Async Function RunAsync(filesList As List(Of String), Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing, Optional MaxParallelism As Integer = 1) As Task(Of Boolean) Implements ICompressor.RunAsync - - Dim totalFiles As Integer = filesList.Count - If MaxParallelism <= 0 Then MaxParallelism = Environment.ProcessorCount - Dim paraOptions As New ParallelOptions With {.MaxDegreeOfParallelism = MaxParallelism} - - _processedFileCount.Clear() - Try - Await Parallel.ForEachAsync(filesList, paraOptions, - Function(file, _ctx) As ValueTask - _ctx.ThrowIfCancellationRequested() - Return New ValueTask(PauseAndProcessFile(file, totalFiles, _cancellationTokenSource.Token, progressMonitor)) - End Function).ConfigureAwait(False) - Catch ex As OperationCanceledException - Return False - End Try - - Return True - - End Function - - Private Async Function PauseAndProcessFile(file As String, totalFiles As Integer, _ctx As CancellationToken, progressMonitor As IProgress(Of CompressionProgress)) As Task - - Try - Await _pauseSemaphore.WaitAsync(_ctx).ConfigureAwait(False) - _pauseSemaphore.Release() - Catch ex As OperationCanceledException - Throw - Return - End Try - - _ctx.ThrowIfCancellationRequested() - - Dim res = WOFDecompressFile(file) - _processedFileCount.TryAdd(file, 1) - - progressMonitor?.Report(New CompressionProgress((CInt(((_processedFileCount.Count / totalFiles) * 100))), file)) - - End Function - - Private Function WOFDecompressFile(path As String) - - Try - Using fs As SafeFileHandle = File.OpenHandle(path) - Dim res = WOFHelper.DeviceIoControl(fs, FSCTL_DELETE_EXTERNAL_BACKING, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, IntPtr.Zero) - Return res - End Using - Catch ex As Exception - Debug.WriteLine(ex.Message) - Return Nothing - End Try - - End Function - - - Public Sub Pause() Implements ICompressor.Pause - _pauseSemaphore.Wait() - End Sub - - Public Sub [Resume]() Implements ICompressor.Resume - If _pauseSemaphore.CurrentCount = 0 Then _pauseSemaphore.Release() - End Sub - Public Sub Cancel() Implements ICompressor.Cancel - [Resume]() - _cancellationTokenSource.Cancel() - End Sub - - Public Sub Dispose() Implements IDisposable.Dispose - _pauseSemaphore?.Dispose() - _cancellationTokenSource?.Dispose() - End Sub -End Class diff --git a/CompactGUI.CoreVB/WOFHelper.vb b/CompactGUI.CoreVB/WOFHelper.vb deleted file mode 100644 index 36ff9f8b..00000000 --- a/CompactGUI.CoreVB/WOFHelper.vb +++ /dev/null @@ -1,83 +0,0 @@ -Imports System.Runtime.InteropServices - -Imports Microsoft.Win32.SafeHandles - -Public Module WOFHelper - - Public Const WOF_PROVIDER_FILE As ULong = 2 - Public Const FSCTL_DELETE_EXTERNAL_BACKING As UInteger = &H90314 - - Public Function WOFConvertCompressionLevel(compressionlevel As Integer) As Integer - - Select Case compressionlevel - Case 0 : Return WOFCompressionAlgorithm.XPRESS4K - Case 1 : Return WOFCompressionAlgorithm.XPRESS8K - Case 2 : Return WOFCompressionAlgorithm.XPRESS16K - Case 3 : Return WOFCompressionAlgorithm.LZX - Case Else : Return WOFCompressionAlgorithm.XPRESS4K - End Select - - End Function - - Public Function WOFConvertCompressionLevel(compressionlevel As CompressionMode) As Integer - Select Case compressionlevel - Case CompressionMode.XPRESS4K : Return WOFCompressionAlgorithm.XPRESS4K - Case CompressionMode.XPRESS8K : Return WOFCompressionAlgorithm.XPRESS8K - Case CompressionMode.XPRESS16K : Return WOFCompressionAlgorithm.XPRESS16K - Case CompressionMode.LZX : Return WOFCompressionAlgorithm.LZX - Case Else : Return WOFCompressionAlgorithm.XPRESS4K - End Select - End Function - - - Public Function WOFConvertBackCompressionLevel(WOFCompressionLevel As WOFCompressionAlgorithm) As Integer - - Select Case WOFCompressionLevel - Case WOFCompressionAlgorithm.XPRESS4K : Return 0 - Case WOFCompressionAlgorithm.XPRESS8K : Return 1 - Case WOFCompressionAlgorithm.XPRESS16K : Return 2 - Case WOFCompressionAlgorithm.LZX : Return 3 - Case Else : Return 0 - End Select - - End Function - - - Public Structure WOF_FILE_COMPRESSION_INFO_V1 - Public Algorithm As UInteger - Public Flags As UInteger - End Structure - - - - Friend Function WofIsExternalFile( - Filepath As String, - ByRef IsExternalFile As Integer, - ByRef Provider As UInteger, - ByRef Info As WOF_FILE_COMPRESSION_INFO_V1, - ByRef BufferLength As UInteger) As Integer - End Function - - - Friend Function WofSetFileDataLocation( - FileHandle As SafeFileHandle, - Provider As ULong, - ExternalFileInfo As IntPtr, - Length As ULong) As Integer - End Function - - 'Most of these should be optional if MS Docs are to be believed -.- - - Friend Function DeviceIoControl( - hDevice As SafeFileHandle, - dwIoControlCode As UInteger, - lpInBuffer As IntPtr, - nInBufferSize As UInteger, - lpOutBuffer As IntPtr, - nOutBufferSize As UInteger, - ByRef lpBytesReturned As IntPtr, - lpOverlapped As IntPtr) As Boolean - - End Function - -End Module diff --git a/CompactGUI.Watcher/BackgroundCompactor.vb b/CompactGUI.Watcher/BackgroundCompactor.vb index 107e1ce9..312faf80 100644 --- a/CompactGUI.Watcher/BackgroundCompactor.vb +++ b/CompactGUI.Watcher/BackgroundCompactor.vb @@ -49,11 +49,14 @@ Public Class BackgroundCompactor End Sub - Private Function CreateCompactor(folder As String, compressionLevel As Core.WOFCompressionAlgorithm) As Core.Compactor + Private Function CreateCompactor(folder As String, + compressionLevel As Core.WOFCompressionAlgorithm, + Optional excludedFileTypes As String() = Nothing) As Core.Compactor If compressionLevel = Core.WOFCompressionAlgorithm.NO_COMPRESSION Then Return Nothing - Return New Core.Compactor(folder, compressionLevel, _excludedFileTypes, New Core.Analyser(folder, NullLogger(Of Core.Analyser).Instance)) + Dim effectiveExclusions = If(excludedFileTypes Is Nothing, _excludedFileTypes, excludedFileTypes) + Return New Core.Compactor(folder, compressionLevel, effectiveExclusions, New Core.Analyser(folder, NullLogger(Of Core.Analyser).Instance)) End Function @@ -89,7 +92,8 @@ Public Class BackgroundCompactor Try WatcherLog.CompactingFolder(_logger, folder.DisplayName) - compactor = CreateCompactor(folder.Folder, folder.CompressionLevel) + Dim folderSkipList As String() = If(folder.SkipList Is Nothing, Nothing, folder.SkipList.ToArray()) + compactor = CreateCompactor(folder.Folder, folder.CompressionLevel, folderSkipList) If compactor Is Nothing Then Return False SyncLock _compactorLock diff --git a/CompactGUI.Watcher/WatchedFolder.vb b/CompactGUI.Watcher/WatchedFolder.vb index eeb3025b..b83171d4 100644 --- a/CompactGUI.Watcher/WatchedFolder.vb +++ b/CompactGUI.Watcher/WatchedFolder.vb @@ -20,6 +20,8 @@ Public Class WatchedFolder Private _LastCheckedDate As DateTime Private _LastCheckedSize As Long Private _CompressionLevel As Core.WOFCompressionAlgorithm + Private _SkipList As List(Of String) + Private _SkipListEnabled As Boolean = True Private _IsWorking As Boolean diff --git a/CompactGUI.slnx b/CompactGUI.slnx index bc4e5586..036be4eb 100644 --- a/CompactGUI.slnx +++ b/CompactGUI.slnx @@ -10,12 +10,7 @@ - - - - - - + diff --git a/CompactGUI/Components/Custom/TokenizedTextBox.vb b/CompactGUI/Components/Custom/TokenizedTextBox.vb deleted file mode 100644 index 68a26f00..00000000 --- a/CompactGUI/Components/Custom/TokenizedTextBox.vb +++ /dev/null @@ -1,84 +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 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 &= " " - 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..5e3eb144 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,99 @@ - - + + + + + + + - - - - - - - + d:Text="edit skipped filetypes" FontSize="22" FontWeight="SemiBold" + Text="{local:Localize SetFiletypeManagement_LocalSkipFiletypesEdit}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +