diff --git a/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs b/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs index ea34788b..c2a76350 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs @@ -1,4 +1,5 @@ using ApplicationServices; +using Avalonia.Threading; using DataLayer; using LibationFileManager; using ReactiveUI; @@ -77,6 +78,9 @@ partial class MainVM if (Configuration.Instance.AutoDownloadEpisodes && stats.PendingBooks + stats.pdfsNotDownloaded > 0) - await BackupAllBooksAsync(stats.LibraryBooks); + { + // RunWorkerCompleted has no SynchronizationContext; queue items require the UI thread. + await Dispatcher.UIThread.InvokeAsync(async () => await BackupAllBooksAsync(stats.LibraryBooks)); + } } } diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index b727bae8..9fc2ac83 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -255,7 +255,16 @@ public class ProcessQueueViewModel : ReactiveObject && entry.Status is ProcessBookStatus.Completed && Queue.RemoveCompleted(entry); + /// + /// ProcessBookViewModel requires a captured UI SynchronizationContext. Callers may resume on a + /// thread-pool thread after await (e.g. auto-download after BackgroundWorker). + /// + private void RunOnQueueUiThread(Action action) => Invoke(action); + private void AddDownloadPdf(IList entries, Configuration config) + => RunOnQueueUiThread(() => addDownloadPdfCore(entries, config)); + + private void addDownloadPdfCore(IList entries, Configuration config) { var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length); @@ -266,6 +275,9 @@ public class ProcessQueueViewModel : ReactiveObject } private void AddDownloadDecrypt(IList entries, Configuration config) + => RunOnQueueUiThread(() => addDownloadDecryptCore(entries, config)); + + private void addDownloadDecryptCore(IList entries, Configuration config) { var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length); @@ -276,6 +288,9 @@ public class ProcessQueueViewModel : ReactiveObject } private void AddConvertMp3(IList entries, Configuration config) + => RunOnQueueUiThread(() => addConvertMp3Core(entries, config)); + + private void addConvertMp3Core(IList entries, Configuration config) { var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length); diff --git a/Source/LibationWinForms/Form1._NonUI.cs b/Source/LibationWinForms/Form1._NonUI.cs index e8e623c5..a5708735 100644 --- a/Source/LibationWinForms/Form1._NonUI.cs +++ b/Source/LibationWinForms/Form1._NonUI.cs @@ -1,5 +1,6 @@ using ApplicationServices; using AudibleUtilities; +using Dinah.Core.Threading; using Dinah.Core.WindowsDesktop.Drawing; using FileManager; using LibationFileManager; @@ -26,14 +27,19 @@ public partial class Form1 // wire-up event to automatically download after scan. // winforms only. this should NOT be allowed in cli - updateCountsBw.RunWorkerCompleted += async (object? sender, System.ComponentModel.RunWorkerCompletedEventArgs e) => - { - if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats) - return; + updateCountsBw.RunWorkerCompleted += (_, e) => tryAutoDownloadAfterCounts(e); + } - if ((libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded) > 0) - await BackupAllBooksAsync(libraryStats.LibraryBooks); - }; + private void tryAutoDownloadAfterCounts(System.ComponentModel.RunWorkerCompletedEventArgs e) + { + if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats) + return; + + if (libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded <= 0) + return; + + // RunWorkerCompleted has no SynchronizationContext; queue items require the UI thread. + this.UIThreadAsync(() => _ = BackupAllBooksAsync(libraryStats.LibraryBooks)); } private static object? LoadResourceImage(string resourceName)