diff --git a/CompactGUI/Services/WindowService.vb b/CompactGUI/Services/WindowService.vb index 11639797..854c9953 100644 --- a/CompactGUI/Services/WindowService.vb +++ b/CompactGUI/Services/WindowService.vb @@ -74,7 +74,6 @@ Public Class WindowService .Content = String.Format(LanguageHelper.GetString("CompressionStop_Message"), folderName), .DialogWidth = 640, .PrimaryButtonText = LanguageHelper.GetString("CompressionStop_SaveProgress"), - .SecondaryButtonText = LanguageHelper.GetString("CompressionStop_UndoProgress"), .CloseButtonText = LanguageHelper.GetString("CompressionStop_LeaveAsIs") } @@ -87,8 +86,6 @@ Public Class WindowService Select Case Await dialog.ShowAsync() Case Wpf.Ui.Controls.ContentDialogResult.Primary Return CompressionStopChoice.SaveProgress - Case Wpf.Ui.Controls.ContentDialogResult.Secondary - Return CompressionStopChoice.UndoProgress Case Else Return If(cancelRequested, CompressionStopChoice.Cancel, CompressionStopChoice.LeaveAsIs) End Select diff --git a/CompactGUI/ViewModels/HomeViewModel.vb b/CompactGUI/ViewModels/HomeViewModel.vb index 80fa84b0..05fcdafb 100644 --- a/CompactGUI/ViewModels/HomeViewModel.vb +++ b/CompactGUI/ViewModels/HomeViewModel.vb @@ -62,6 +62,8 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient Private ReadOnly _settingsService As ISettingsService Private ReadOnly _compressableFolderService As CompressableFolderService Private ReadOnly _folderValidationService As FolderValidationService + Private _stopBatchAfterCurrent As Boolean = False + Private _compressionBatchCompletion As TaskCompletionSource(Of Boolean) Sub New(watcher As Watcher.Watcher, snackbarService As CustomSnackBarService, logger As ILogger(Of HomeViewModel), settingsService As ISettingsService, compressableFolderService As CompressableFolderService, folderValidationService As FolderValidationService) WeakReferenceMessenger.Default.Register(Of WatcherAddedFolderToQueueMessage)(Me) @@ -125,7 +127,6 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient Next Dim validFolders = requestedFolders.Except(invalidFolderPaths, StringComparer.OrdinalIgnoreCase) - Dim foldersToResume As New Dictionary(Of CompressableFolder, SavedCompressionSession)() Dim resumeService = Application.GetService(Of CompressionResumeService)() Dim windowService = Application.GetService(Of IWindowService)() @@ -158,10 +159,7 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient newFolder.CompressionOptions.SkipUserSubmittedFiletypes = _settingsService.AppSettings.SkipUserNonCompressable If resumeChoice.HasValue AndAlso resumeChoice.Value = CompressionResumeChoice.ResumeProgress AndAlso savedSession IsNot Nothing Then - newFolder.CompressionOptions.SelectedCompressionMode = savedSession.SelectedCompressionMode - newFolder.CompressionOptions.SkipPoorlyCompressedFileTypes = savedSession.SkipPoorlyCompressedFileTypes - newFolder.CompressionOptions.SkipUserSubmittedFiletypes = savedSession.SkipUserSubmittedFiletypes - newFolder.CompressionOptions.WatchFolderForChanges = savedSession.WatchFolderForChanges + ApplyResumeSessionOptions(newFolder, savedSession) End If Folders.Add(newFolder) @@ -186,14 +184,22 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient End If If resumeChoice.HasValue AndAlso resumeChoice.Value = CompressionResumeChoice.ResumeProgress AndAlso savedSession IsNot Nothing Then - foldersToResume(newFolder) = savedSession + 'Resume only prepares this folder to continue from its saved checkpoint. + 'Actual compression starts when the user clicks Compress Selected. + newFolder.FolderActionState = ActionState.Idle End If Next - - If foldersToResume.Count > 0 Then Await CompressFoldersAsync(foldersToResume.Keys.ToList(), foldersToResume) End Function + Private Shared Sub ApplyResumeSessionOptions(folder As CompressableFolder, session As SavedCompressionSession) + folder.CompressionOptions.SelectedCompressionMode = session.SelectedCompressionMode + folder.CompressionOptions.SkipPoorlyCompressedFileTypes = session.SkipPoorlyCompressedFileTypes + folder.CompressionOptions.SkipUserSubmittedFiletypes = session.SkipUserSubmittedFiletypes + folder.CompressionOptions.WatchFolderForChanges = session.WatchFolderForChanges + End Sub + + Public Sub RemoveFolder(folder As CompressableFolder) If Not CanRemoveFolder() Then @@ -255,6 +261,25 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient Private _Compressing As Boolean = False + Public Function GetActiveManualCompressionFolder() As CompressableFolder + Return Folders.FirstOrDefault( + Function(folder) + Return TypeOf folder.Compressor Is Core.Compactor AndAlso + (folder.FolderActionState = ActionState.Working OrElse folder.FolderActionState = ActionState.Paused) + End Function) + End Function + + + Public Async Function StopManualCompressionForExitAsync(folder As CompressableFolder, choice As CompressionStopChoice?) As Task + _stopBatchAfterCurrent = True + Dim completion = _compressionBatchCompletion + + If folder IsNot Nothing AndAlso choice.HasValue Then + _compressableFolderService.RequestCompressionStop(folder, choice.Value) + End If + + If completion IsNot Nothing Then Await completion.Task + End Function @@ -263,16 +288,20 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient Await CompressFoldersAsync(foldersToCompress) End Function - Private Async Function CompressFoldersAsync( - foldersToCompress As IReadOnlyCollection(Of CompressableFolder), - Optional resumeSessions As IReadOnlyDictionary(Of CompressableFolder, SavedCompressionSession) = Nothing) As Task + Private Async Function CompressFoldersAsync(foldersToCompress As IReadOnlyCollection(Of CompressableFolder)) As Task If foldersToCompress Is Nothing OrElse foldersToCompress.Count = 0 Then Return Await _watcher.DisableBackgrounding() + + Dim completion = New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously) + _compressionBatchCompletion = completion + _stopBatchAfterCurrent = False + Compressing = True Core.SharedMethods.PreventSleep() HomeViewModelLog.StartingBatchCompression(_logger, foldersToCompress.Count) + Dim resumeService = Application.GetService(Of CompressionResumeService)() Dim watcherTargetOverrides As New Dictionary(Of CompressableFolder, Core.WOFCompressionAlgorithm)() Dim foldersWithCompressionWork As New HashSet(Of CompressableFolder)() Dim capturedException As Exception = Nothing @@ -287,11 +316,19 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient 'Keep the orchestration on the WPF synchronization context. The compressor performs 'its own asynchronous file work, while folder state and commands remain UI-thread-owned. HomeViewModelLog.CompressingFolder(_logger, folder.FolderName) + Dim resumeSession As SavedCompressionSession = Nothing - If resumeSessions IsNot Nothing Then resumeSessions.TryGetValue(folder, resumeSession) + If resumeService.TryGetSession(folder.FolderName, resumeSession) Then + 'A saved checkpoint belongs to the folder, not to the act of re-adding it. + 'This also covers Compress Again while the folder remains in the Home list. + ApplyResumeSessionOptions(folder, resumeSession) + End If Dim runResult = Await _compressableFolderService.CompressFolder(folder, resumeSession) - If Not runResult.HadWork Then Continue For + If Not runResult.HadWork Then + If _stopBatchAfterCurrent Then Exit For + Continue For + End If foldersWithCompressionWork.Add(folder) Await _compressableFolderService.AnalyseFolderAsync(folder) @@ -307,6 +344,8 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient watcherTargetOverrides(folder) = watcherTarget Await _watcher.UpdateWatched(folder.FolderName, folder.Analyser, runResult.Completed, targetCompressionLevel:=watcherTarget) + + If _stopBatchAfterCurrent Then Exit For Next For Each folder In Folders.Where(Function(item) item.CompressionOptions.WatchFolderForChanges) @@ -326,7 +365,16 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient Core.SharedMethods.RestoreSleep() End Try - Await _watcher.EnableBackgrounding() + Try + Await _watcher.EnableBackgrounding() + Finally + If Object.ReferenceEquals(_compressionBatchCompletion, completion) Then + _compressionBatchCompletion = Nothing + End If + _stopBatchAfterCurrent = False + completion.TrySetResult(True) + End Try + If capturedException IsNot Nothing Then ExceptionDispatchInfo.Capture(capturedException).Throw() End If diff --git a/CompactGUI/ViewModels/MainWindowViewModel.vb b/CompactGUI/ViewModels/MainWindowViewModel.vb index ffcd00cd..ae24915f 100644 --- a/CompactGUI/ViewModels/MainWindowViewModel.vb +++ b/CompactGUI/ViewModels/MainWindowViewModel.vb @@ -15,6 +15,8 @@ Partial Public Class MainWindowViewModel : Inherits ObservableRecipient : Implem Private ReadOnly _watcher As Watcher.Watcher Private ReadOnly _windowService As IWindowService Private ReadOnly _settingsService As ISettingsService + Private _allowClose As Boolean = False + Private _isExitInProgress As Boolean = False Public Sub New(windowService As IWindowService, watcher As Watcher.Watcher, settingsService As ISettingsService) _watcher = watcher @@ -38,35 +40,114 @@ Partial Public Class MainWindowViewModel : Inherits ObservableRecipient : Implem Private Async Function NotifyIconExit() As Task - If _watcher.WatchedFolders.Count = 0 Then Application.Current.Shutdown() - Dim message As String = String.Format(LanguageHelper.GetString("MessageBox_ExitText"), _watcher.WatchedFolders.Count) - Dim confirmed = Await _windowService.ShowMessageBox(LanguageHelper.GetString("Title_CompactGUI"), message) - If Not confirmed Then Return - _watcher.WriteToFile() - Application.Current.Shutdown() - End Function + If _isExitInProgress Then Return + _isExitInProgress = True + Try + If _watcher.WatchedFolders.Count <> 0 Then + Dim message As String = String.Format(LanguageHelper.GetString("MessageBox_ExitText"), _watcher.WatchedFolders.Count) + Dim confirmed = Await _windowService.ShowMessageBox(LanguageHelper.GetString("Title_CompactGUI"), message) + If Not confirmed Then Return + End If - - Private Sub Closing(e As ComponentModel.CancelEventArgs) - If e Is Nothing Then Return + If Not Await PrepareManualCompressionForExitAsync() Then Return - If Keyboard.Modifiers = ModifierKeys.Shift Then - e.Cancel = False If _watcher.WatchedFolders.Count <> 0 Then _watcher.WriteToFile() _settingsService.SaveSettings() + _allowClose = True Application.Current.Shutdown() + Finally + If Not _allowClose Then _isExitInProgress = False + End Try + End Function + + + Private Async Function PrepareManualCompressionForExitAsync() As Task(Of Boolean) + Dim homeViewModel = Application.GetService(Of HomeViewModel)() + If homeViewModel Is Nothing OrElse Not homeViewModel.Compressing Then Return True + + Dim activeFolder = homeViewModel.GetActiveManualCompressionFolder() + If activeFolder Is Nothing Then + Await homeViewModel.StopManualCompressionForExitAsync(Nothing, Nothing) + Return True + End If + + _windowService.ShowMainWindow() + + Dim pausedForStopDialog = False + Dim compressionFinishedWhilePausing = False + If activeFolder.FolderActionState = ActionState.Working Then + Try + activeFolder.Compressor?.Pause() + activeFolder.FolderActionState = ActionState.Paused + pausedForStopDialog = True + Catch ex As OperationCanceledException + compressionFinishedWhilePausing = True + Catch ex As ObjectDisposedException + compressionFinishedWhilePausing = True + End Try + End If + + If compressionFinishedWhilePausing Then + Await homeViewModel.StopManualCompressionForExitAsync(Nothing, Nothing) + Return True + End If + + Dim choice = Await _windowService.ShowCompressionStopDialog(activeFolder.DisplayName) + If choice = CompressionStopChoice.Cancel Then + If pausedForStopDialog AndAlso activeFolder.FolderActionState = ActionState.Paused Then + Try + activeFolder.Compressor?.Resume() + activeFolder.FolderActionState = ActionState.Working + Catch ex As OperationCanceledException + 'The compression finished while the stop dialog was open. + Catch ex As ObjectDisposedException + 'The compression finished while the stop dialog was open. + End Try + End If + Return False + End If + + Await homeViewModel.StopManualCompressionForExitAsync(activeFolder, choice) + Return True + End Function + + + + Private Async Function Closing(e As ComponentModel.CancelEventArgs) As Task + If e Is Nothing Then Return + If _allowClose Then + e.Cancel = False Return End If - If _watcher.WatchedFolders.Count <> 0 Then + Dim forceExit = Keyboard.Modifiers = ModifierKeys.Shift + + If Not forceExit AndAlso _watcher.WatchedFolders.Count <> 0 Then e.Cancel = True _windowService.MinimizeMainWindow() _watcher.WriteToFile() _windowService.HideMainWindow() + Return End If - End Sub + 'This close would actually exit the application. Cancel it synchronously so an + 'active manual compression run can use the normal stop/save/undo prompt first. + e.Cancel = True + If _isExitInProgress Then Return + _isExitInProgress = True + + Try + If Not Await PrepareManualCompressionForExitAsync() Then Return + + If _watcher.WatchedFolders.Count <> 0 Then _watcher.WriteToFile() + _settingsService.SaveSettings() + _allowClose = True + Application.Current.Shutdown() + Finally + If Not _allowClose Then _isExitInProgress = False + End Try + End Function Public Sub Receive(message As PropertyChangedMessage(Of CompressableFolder)) Implements IRecipient(Of PropertyChangedMessage(Of CompressableFolder)).Receive