diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 0f5008ec..7f64645e 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -37,10 +37,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: 9.0.x @@ -62,7 +62,8 @@ jobs: -p:PublishSingleFile=true ` -p:IncludeNativeLibrariesForSelfExtract=true ` -p:DebugType=None ` - -p:DebugSymbols=false + -p:DebugSymbols=false ` + -p:TreatWarningsAsErrors=true if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE @@ -105,7 +106,7 @@ jobs: } | Set-Content (Join-Path $artifactDir "SHA256SUMS.txt") -Encoding ascii - name: Upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: CompactGUI-win-x64 path: ${{ runner.temp }}/compactgui-artifacts/* @@ -122,12 +123,12 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Download release assets - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: CompactGUI-win-x64 path: release-assets @@ -188,4 +189,4 @@ jobs: release_args+=(--prerelease) fi - gh "${release_args[@]}" + gh "${release_args[@]}" \ No newline at end of file diff --git a/CompactGUI.Core/Analyser.cs b/CompactGUI.Core/Analyser.cs index 68fe9cec..f8118ddf 100644 --- a/CompactGUI.Core/Analyser.cs +++ b/CompactGUI.Core/Analyser.cs @@ -125,10 +125,12 @@ private static bool GetContainsCompressedFiles(List fileCom public List GetPoorlyCompressedExtensions() { + List analysedFileDetails = _analysedFileDetails ?? []; + // Only use PLINQ if the list is large enough to benefit from parallel processing - IEnumerable query = _analysedFileDetails?.Count <= 10000 - ? _analysedFileDetails - : _analysedFileDetails.AsParallel(); + IEnumerable query = analysedFileDetails.Count <= 10000 + ? analysedFileDetails + : analysedFileDetails.AsParallel(); return query .Where(fl => fl.UncompressedSize > 0) @@ -150,6 +152,4 @@ public void Dispose() _folderMonitor.Dispose(); _analysedFileDetails?.Clear(); } -} - - +} \ No newline at end of file diff --git a/CompactGUI.Core/Compactor.cs b/CompactGUI.Core/Compactor.cs index 0c6071f9..f927d378 100644 --- a/CompactGUI.Core/Compactor.cs +++ b/CompactGUI.Core/Compactor.cs @@ -50,7 +50,8 @@ private void InitializeCompressionInfoPointer() } - public async Task RunAsync(List filesList, IProgress progressMonitor = null, int maxParallelism = 1) + + public async Task RunAsync(List filesList, IProgress? progressMonitor = null, int maxParallelism = 1) { if(cancellationTokenSource.IsCancellationRequested) { return false; } @@ -92,7 +93,7 @@ await Parallel.ForEachAsync(workingFiles, parallelOptions, return true; } - private async Task PauseAndProcessFile(FileDetails file, long totalFilesSize, CancellationToken token, IProgress progressMonitor) + private async Task PauseAndProcessFile(FileDetails file, long totalFilesSize, CancellationToken token, IProgress? progressMonitor) { CompactorLog.ProcessingFile(_logger, file.FileName, file.UncompressedSize); @@ -126,9 +127,9 @@ public async Task> BuildWorkingFilesList() uint clusterSize = SharedMethods.GetClusterSize(workingDirectory); - var analysedFiles = await _analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token); + var analysedFiles = await _analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token) ?? []; - var filesList = analysedFiles? + return analysedFiles .Where(fl => fl.CompressionMode != wofCompressionAlgorithm && fl.UncompressedSize > clusterSize @@ -136,13 +137,12 @@ public async Task> BuildWorkingFilesList() ) .Select(fl => new FileDetails(fl.FileName, fl.UncompressedSize)) .ToList(); - - return filesList; } + public void Pause() { CompactorLog.CompressionPaused(_logger); @@ -178,5 +178,4 @@ public void Dispose() public readonly record struct FileDetails(string FileName, long UncompressedSize); - -} +} \ No newline at end of file diff --git a/CompactGUI.Core/ICompressor.cs b/CompactGUI.Core/ICompressor.cs index e9c3c8fc..04054c96 100644 --- a/CompactGUI.Core/ICompressor.cs +++ b/CompactGUI.Core/ICompressor.cs @@ -3,7 +3,7 @@ public interface ICompressor : IDisposable { Task RunAsync(List filesList, - IProgress progressMonitor = null, + IProgress? progressMonitor = null, int maxParallelism = 1); void Pause(); diff --git a/CompactGUI.Core/Settings/Settings.cs b/CompactGUI.Core/Settings/Settings.cs index 2716e23c..1b4ed262 100644 --- a/CompactGUI.Core/Settings/Settings.cs +++ b/CompactGUI.Core/Settings/Settings.cs @@ -58,14 +58,14 @@ public partial class Settings : ObservableRecipient private void UpdateNextScheduledBackgroundRun() { - DateTime nextRun = scheduledBackgroundLastRan.Date - .AddDays(scheduledBackgroundInterval) - .AddHours(scheduledBackgroundHour) - .AddMinutes(scheduledBackgroundMinute); + DateTime nextRun = ScheduledBackgroundLastRan.Date + .AddDays(ScheduledBackgroundInterval) + .AddHours(ScheduledBackgroundHour) + .AddMinutes(ScheduledBackgroundMinute); DateTime now = DateTime.Now; while (nextRun <= now) { - nextRun = nextRun.AddDays(scheduledBackgroundInterval == 0 ? 1 : scheduledBackgroundInterval); + nextRun = nextRun.AddDays(ScheduledBackgroundInterval == 0 ? 1 : ScheduledBackgroundInterval); } NextScheduledBackgroundRun = nextRun; @@ -89,4 +89,4 @@ public enum BackgroundMode IdleOnly, Scheduled, ScheduledAndIdle -} +} \ No newline at end of file diff --git a/CompactGUI.Core/SharedMethods.cs b/CompactGUI.Core/SharedMethods.cs index 910e39c6..900ab37a 100644 --- a/CompactGUI.Core/SharedMethods.cs +++ b/CompactGUI.Core/SharedMethods.cs @@ -183,10 +183,11 @@ public static bool HasDirectoryWritePermission(string folderName) DirectoryInfo dirInfo = new DirectoryInfo(folderName); DirectorySecurity dirSecurity = dirInfo.GetAccessControl(); - var user = WindowsIdentity.GetCurrent(); - var userSID = user.User; - var userGroupSIDs = user.Groups; + using WindowsIdentity user = WindowsIdentity.GetCurrent(); + SecurityIdentifier? userSID = user.User; + if (userSID is null) return false; + IdentityReferenceCollection? userGroupSIDs = user.Groups; var rules = dirSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier)); bool writeAllowed = false; @@ -194,11 +195,12 @@ public static bool HasDirectoryWritePermission(string folderName) foreach (FileSystemAccessRule rule in rules) { - var fileSystemRights = rule.FileSystemRights; - if (!rule.FileSystemRights.HasFlag(FileSystemRights.Write)) continue; if (rule.IdentityReference is not SecurityIdentifier ruleSID) continue; - if (!ruleSID.Equals(userSID) && !userGroupSIDs.Contains(ruleSID)) continue; + + bool belongsToUser = ruleSID.Equals(userSID); + bool belongsToGroup = userGroupSIDs?.Contains(ruleSID) == true; + if (!belongsToUser && !belongsToGroup) continue; if (rule.AccessControlType == AccessControlType.Deny) { @@ -243,4 +245,4 @@ public static bool IsDirectStorageGameFolder(string folderPath) } -} +} \ No newline at end of file diff --git a/CompactGUI.Watcher/WatchedFolder.vb b/CompactGUI.Watcher/WatchedFolder.vb index 23d55821..eeb3025b 100644 --- a/CompactGUI.Watcher/WatchedFolder.vb +++ b/CompactGUI.Watcher/WatchedFolder.vb @@ -27,6 +27,9 @@ Public Class WatchedFolder Private _IsEditing As Boolean = False + + Private _IsAvailable As Boolean = True + ' --- Monitoring State --- Private _HasTargetChanged As Boolean = False @@ -39,6 +42,7 @@ Public Class WatchedFolder Private WithEvents FSWatcher As FileSystemWatcher Private debounceTimer As Timer + Private isMonitoringPaused As Boolean Private disposedValue As Boolean @@ -52,40 +56,96 @@ Public Class WatchedFolder Public Sub New() End Sub - Public Sub InitializeMonitoring() - If FSWatcher Is Nothing Then - FSWatcher = New FileSystemWatcher(Folder) With { - .NotifyFilter = NotifyFilters.Size Or NotifyFilters.CreationTime Or NotifyFilters.LastWrite Or NotifyFilters.FileName, - .IncludeSubdirectories = True, - .Filter = "", - .EnableRaisingEvents = True - } - debounceTimer = New Timer(AddressOf DebounceTimerCallback, Nothing, Timeout.Infinite, Timeout.Infinite) + Public Function InitializeMonitoring() As Boolean + If disposedValue Then Return False + + If String.IsNullOrWhiteSpace(Folder) OrElse Not Directory.Exists(Folder) Then + MarkUnavailable() + Return False End If - End Sub + + Try + If FSWatcher Is Nothing Then + FSWatcher = New FileSystemWatcher(Folder) With { + .NotifyFilter = NotifyFilters.Size Or NotifyFilters.CreationTime Or NotifyFilters.LastWrite Or NotifyFilters.FileName, + .IncludeSubdirectories = True, + .Filter = "", + .EnableRaisingEvents = Not isMonitoringPaused + } + debounceTimer = New Timer(AddressOf DebounceTimerCallback, Nothing, Timeout.Infinite, Timeout.Infinite) + Else + FSWatcher.EnableRaisingEvents = Not isMonitoringPaused + End If + + IsAvailable = True + Return True + Catch ex As Exception When TypeOf ex Is IOException OrElse TypeOf ex Is UnauthorizedAccessException OrElse TypeOf ex Is ArgumentException + Debug.WriteLine($"Unable to monitor folder {Folder}: {ex.Message}") + MarkUnavailable() + Return False + End Try + End Function + + Public Function RefreshAvailability() As Boolean + Dim wasAvailable = IsAvailable + + If Not InitializeMonitoring() Then Return False + + If Not wasAvailable Then + HasTargetChanged = True + LastChangedDate = DateTime.Now + End If + + Return True + End Function Public Sub PauseMonitoring() - If FSWatcher IsNot Nothing Then - FSWatcher.EnableRaisingEvents = False + isMonitoringPaused = True + + If Not IsAvailable OrElse FSWatcher Is Nothing Then Return + + If Not Directory.Exists(Folder) Then + MarkUnavailable() + Return End If + + Try + FSWatcher.EnableRaisingEvents = False + Catch ex As Exception When TypeOf ex Is IOException OrElse TypeOf ex Is ObjectDisposedException + MarkUnavailable() + End Try End Sub Public Sub ResumeMonitoring() - If FSWatcher IsNot Nothing Then + isMonitoringPaused = False + If Not RefreshAvailability() Then Return + + Try FSWatcher.EnableRaisingEvents = True - End If + Catch ex As Exception When TypeOf ex Is IOException OrElse TypeOf ex Is ObjectDisposedException + MarkUnavailable() + End Try End Sub ' --- Monitoring Events --- Private Sub WatcherErrorEvent(sender As Object, e As ErrorEventArgs) Handles FSWatcher.Error Debug.WriteLine(e.GetException.Message) + + If Not Directory.Exists(Folder) Then + MarkUnavailable() + End If End Sub Private Sub WatcherModifiedEvent(sender As Object, e As FileSystemEventArgs) Handles FSWatcher.Created, FSWatcher.Changed, FSWatcher.Renamed, FSWatcher.Deleted - debounceTimer.Change(1000, Timeout.Infinite) + debounceTimer?.Change(1000, Timeout.Infinite) End Sub Private Sub DebounceTimerCallback(state As Object) + If Not Directory.Exists(Folder) Then + MarkUnavailable() + Return + End If + HasTargetChanged = True LastChangedDate = DateTime.Now OnPropertyChanged(NameOf(HasTargetChanged)) @@ -94,6 +154,28 @@ Public Class WatchedFolder Debug.WriteLine("Folder " & Folder & " has changed!") End Sub + Private Sub MarkUnavailable() + IsAvailable = False + IsWorking = False + StopMonitoring() + End Sub + + Private Sub StopMonitoring() + Try + FSWatcher?.Dispose() + Catch ex As ObjectDisposedException + Finally + FSWatcher = Nothing + End Try + + Try + debounceTimer?.Dispose() + Catch ex As ObjectDisposedException + Finally + debounceTimer = Nothing + End Try + End Sub + ' --- Calculated Properties --- Public ReadOnly Property DecayPercentage As Decimal Get @@ -119,8 +201,7 @@ Public Class WatchedFolder Protected Overridable Sub Dispose(disposing As Boolean) If Not disposedValue Then If disposing Then - FSWatcher?.Dispose() - debounceTimer?.Dispose() + StopMonitoring() End If disposedValue = True End If @@ -130,4 +211,4 @@ Public Class WatchedFolder Dispose(disposing:=True) GC.SuppressFinalize(Me) End Sub -End Class \ No newline at end of file +End Class diff --git a/CompactGUI.Watcher/Watcher.vb b/CompactGUI.Watcher/Watcher.vb index 8986a8ff..1d1a1c8b 100644 --- a/CompactGUI.Watcher/Watcher.vb +++ b/CompactGUI.Watcher/Watcher.vb @@ -69,7 +69,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi BGCompactor = New BackgroundCompactor(Array.Empty(Of String), _logger) - InitializeWatchedFoldersAsync() + BeginInitializeWatchedFolders() End Sub @@ -118,7 +118,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi _settingsService.AppSettings.ScheduledBackgroundLastRan = DateTime.Now If Not IsWatchingEnabled Then Return False Dim recentThresholdDate As DateTime = DateTime.Now.AddSeconds(-IdleSettings.LastSystemModifiedTimeThresholdSeconds) - If Not runAll AndAlso WatchedFolders.Any(Function(x) x.LastChangedDate > recentThresholdDate) Then Return False + If Not runAll AndAlso WatchedFolders.Any(Function(x) x.IsAvailable AndAlso x.LastChangedDate > recentThresholdDate) Then Return False If _parseWatchersSemaphore.CurrentCount <> 0 Then Await ParseWatchers(runAll, runToken) @@ -227,6 +227,14 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi OnPropertyChanged(NameOf(TotalSaved)) End Sub + Private Async Sub BeginInitializeWatchedFolders() + Try + Await InitializeWatchedFoldersAsync() + Catch ex As Exception + _logger.LogError(ex, "Unable to initialize watched folders.") + End Try + End Sub + Private Async Function InitializeWatchedFoldersAsync() As Task Dim initialWatchedFolders = Await GetWatchedFoldersFromJson() @@ -234,7 +242,8 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi WatchedFolders.Clear() - For Each folder In initialWatchedFolders.Where(Function(f) IO.Directory.Exists(f.Folder)) + For Each folder In initialWatchedFolders + folder.RefreshAvailability() WatchedFolders.Add(folder) folder.LastChangedDate = folder.LastSystemModifiedDate Next @@ -311,6 +320,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi .CompressionLevel = If(newItem.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION, newItem.CompressionLevel, existingItem.CompressionLevel) End With existingItem.HasTargetChanged = False + existingItem.RefreshAvailability() End Sub Public Async Function RemoveWatched(item As WatchedFolder, Optional writeToFile As Boolean = True) As Task @@ -322,18 +332,11 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi End Function - Public Async Function DeleteWatchersWithNonExistentFolders() As Task - - For i As Integer = WatchedFolders.Count - 1 To 0 Step -1 - If Not IO.Directory.Exists(WatchedFolders(i).Folder) Then - WatcherLog.RemovingNonexistentFolders(_logger, 1) - Await RemoveWatched(WatchedFolders(i), False) - End If + Public Sub RefreshWatchedFolderAvailability() + For Each watchedFolder In WatchedFolders + watchedFolder.RefreshAvailability() Next - - Await WriteToFileAsync() - - End Function + End Sub Private Async Function GetWatchedFoldersFromJson() As Task(Of ObservableCollection(Of WatchedFolder)) @@ -362,8 +365,8 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi validatedResult = JsonSerializer.Deserialize(Of (DateTime, ObservableCollection(Of WatchedFolder)))(WatcherJSON, DeserializeOptions) If validatedResult.Item2 IsNot Nothing Then - For Each folder In validatedResult.Item2.Where(Function(f) IO.Directory.Exists(f.Folder)) - folder.InitializeMonitoring() + For Each folder In validatedResult.Item2 + folder.RefreshAvailability() Next End If @@ -397,16 +400,19 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi Try WatcherLog.ParsingWatchers(_logger, ParseAll) - Await DeleteWatchersWithNonExistentFolders() + RefreshWatchedFolderAvailability() + Dim availableFolders = WatchedFolders.Where(Function(w) w.IsAvailable) Dim WatchersQuery = If(ParseAll, - WatchedFolders, - WatchedFolders.Where(Function(w) w.HasTargetChanged) + availableFolders, + availableFolders.Where(Function(w) w.HasTargetChanged) ).OrderBy(Function(f) f.DisplayName) If Not WatchersQuery.Any() Then Return For Each fsWatcher In WatchersQuery + If Not fsWatcher.RefreshAvailability() Then Continue For + WatcherLog.FolderChanged(_logger, fsWatcher.DisplayName) If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then Return Await Analyse(fsWatcher.Folder, ParseAll, cToken) @@ -429,11 +435,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi If Not acquired Then Return Try - If watchedFolder Is Nothing Then Return - If Not IO.Directory.Exists(watchedFolder.Folder) Then - Await RemoveWatched(watchedFolder) - Return - End If + If watchedFolder Is Nothing OrElse Not watchedFolder.RefreshAvailability() Then Return Await Analyse(watchedFolder.Folder, False) LastAnalysed = DateTime.Now @@ -454,11 +456,12 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi If BGCompactor.IsCompactorActive Then Return + RefreshWatchedFolderAvailability() Dim recentThresholdDate As DateTime = DateTime.Now.AddSeconds(-IdleSettings.LastSystemModifiedTimeThresholdSeconds) Dim foldersToCompress = WatchedFolders. Where(Function(folder) - Dim eligible = folder.DecayPercentage <> 0 AndAlso folder.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION + Dim eligible = folder.IsAvailable AndAlso folder.DecayPercentage <> 0 AndAlso folder.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION Dim recentlyModified = folder.LastSystemModifiedDate > recentThresholdDate AndAlso Not runAll If eligible AndAlso recentlyModified Then WatcherLog.SkippingRecentlyModifiedFolder(_logger, folder.DisplayName) @@ -480,11 +483,13 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi Public Async Function Analyse(folder As String, checkDiskModified As Boolean, Optional cToken As CancellationToken = Nothing) As Task(Of Boolean) + Dim watched = WatchedFolders.FirstOrDefault(Function(f) f.Folder = folder) + If watched Is Nothing OrElse Not watched.RefreshAvailability() Then Return False + + watched.IsWorking = True - Using analyser As New Analyser(folder, NullLogger(Of Analyser).Instance) - Dim watched = WatchedFolders.First(Function(f) f.Folder = folder) - watched.IsWorking = True - Try + Try + Using analyser As New Analyser(folder, NullLogger(Of Analyser).Instance) Dim analysedFiles = Await analyser.GetAnalysedFilesAsync(cToken) If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then Return False @@ -494,8 +499,8 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi watched.LastSystemModifiedDate = watched.LastChangedDate - If analysedFiles.Count <> 0 Then - Dim mainCompressionLVL = analysedFiles?.Select(Function(f) f.CompressionMode).Max + If analysedFiles?.Count <> 0 Then + Dim mainCompressionLVL = analysedFiles.Select(Function(f) f.CompressionMode).Max watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel) If checkDiskModified Then @@ -510,17 +515,18 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi End If watched.HasTargetChanged = False - Catch ex As OperationCanceledException - Return False - Finally - - watched.IsWorking = False - End Try - - Return True - - End Using + End Using + Catch ex As OperationCanceledException + Return False + Catch ex As Exception When TypeOf ex Is IO.IOException OrElse TypeOf ex Is UnauthorizedAccessException + watched.RefreshAvailability() + _logger.LogDebug(ex, "Unable to analyse watched folder {Folder}.", folder) + Return False + Finally + watched.IsWorking = False + End Try + Return True End Function Public Sub Receive(message As PropertyChangedMessage(Of Boolean)) Implements IRecipient(Of PropertyChangedMessage(Of Boolean)).Receive @@ -536,5 +542,4 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi - -End Class +End Class \ No newline at end of file diff --git a/CompactGUI/ViewModels/WatcherViewModel.vb b/CompactGUI/ViewModels/WatcherViewModel.vb index 596e415a..ff1b2773 100644 --- a/CompactGUI/ViewModels/WatcherViewModel.vb +++ b/CompactGUI/ViewModels/WatcherViewModel.vb @@ -41,7 +41,7 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject Private Async Function RefreshWatched() As Task - Await Watcher.DeleteWatchersWithNonExistentFolders() + Watcher.RefreshWatchedFolderAvailability() Await Task.Run(Function() Watcher.ParseWatchers(True)) End Function @@ -54,6 +54,7 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject Private Sub AddWatchedFolderToQueue(folder As Watcher.WatchedFolder) + If folder Is Nothing OrElse Not folder.RefreshAvailability() Then Return WeakReferenceMessenger.Default.Send(New WatcherAddedFolderToQueueMessage(folder.Folder)) End Sub @@ -91,6 +92,7 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject End Function + Public Async Function AddFolderAsync(folderPath As String) As Task(Of CompressableFolder) If GetInvalidFolders({folderPath}).InvalidFolders.Count > 0 Then @@ -100,7 +102,6 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject End If Return Await CompressableFolderFactory.CreateCompressableFolder(folderPath) - End Function diff --git a/CompactGUI/Views/Components/FolderWatcherCard.xaml b/CompactGUI/Views/Components/FolderWatcherCard.xaml index a408c107..15981b27 100644 --- a/CompactGUI/Views/Components/FolderWatcherCard.xaml +++ b/CompactGUI/Views/Components/FolderWatcherCard.xaml @@ -37,7 +37,6 @@ - - - - - @@ -126,12 +121,10 @@ Visibility="{Binding Watcher.IsRunning, Converter={StaticResource BooleanToInverseVisibilityConverter}}" /> - - - - - -