using ApplicationServices; using Avalonia.Threading; using DataLayer; using LibationFileManager; using LibationUiBase; using ReactiveUI; using System.Collections.Generic; using System.Threading.Tasks; namespace LibationAvalonia.ViewModels; partial class MainVM { private readonly System.ComponentModel.BackgroundWorker updateCountsBw = new(); /// The "Begin Book and PDF Backup" menu item header text public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0"; /// The "Begin PDF Only Backup" menu item header text public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0"; /// How many books are in the trash, where nothing else on screen would reveal them public int BooksInTrash { get => field; private set { this.RaiseAndSetIfChanged(ref field, value); this.RaisePropertyChanged(nameof(TrashBinMenuText)); this.RaisePropertyChanged(nameof(TrashBinStatusText)); this.RaisePropertyChanged(nameof(TrashBinStatusVisible)); RaiseGettingStartedChanged(); } } public string TrashBinMenuText => TrashBinUi.MenuText(BooksInTrash); public string TrashBinStatusText => TrashBinUi.StatusText(BooksInTrash); public bool TrashBinStatusVisible => TrashBinUi.ShowStatus(BooksInTrash); public string TrashBinStatusToolTip => TrashBinUi.StatusToolTip; /// /// Re-read the trash count. Kept off on purpose: that also runs /// against the visible subset on every filter change, where a database round trip would be wasted work. /// public async Task RefreshBooksInTrashAsync() { try { var count = await Task.Run(DbContexts.GetTrashedBookCount); await Dispatcher.UIThread.InvokeAsync(() => BooksInTrash = count); } catch (System.Exception ex) { //A stale count must not take down the window that displays it. Serilog.Log.Logger.Error(ex, "Failed to count books in the trash"); } } /// The user's library statistics public LibraryCommands.LibraryStats? LibraryStats { get => field; set { this.RaiseAndSetIfChanged(ref field, value); BookBackupsToolStripText = LibraryStats?.HasPendingBooks ?? false ? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining") : "All books have been liberated"; PdfBackupsToolStripText = LibraryStats?.pdfsNotDownloaded > 0 ? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining") : "All PDFs have been downloaded"; this.RaisePropertyChanged(nameof(BookBackupsToolStripText)); this.RaisePropertyChanged(nameof(PdfBackupsToolStripText)); //Whether the library is empty is only known once these counts have been taken. RaiseGettingStartedChanged(); } } private void Configure_BackupCounts() { //Pass null to the setup count to get the whole library. LibraryCommands.BookUserDefinedItemCommitted += async (_, _) => await SetBackupCountsAsync(null); updateCountsBw.DoWork += UpdateCountsBw_DoWork; updateCountsBw.RunWorkerCompleted += UpdateCountsBw_CompletedAsync; } private bool runBackupCountsAgain; public async Task SetBackupCountsAsync(IEnumerable? libraryBooks) { runBackupCountsAgain = true; if (!updateCountsBw.IsBusy) updateCountsBw.RunWorkerAsync(libraryBooks); } private void UpdateCountsBw_DoWork(object? sender, System.ComponentModel.DoWorkEventArgs e) { while (runBackupCountsAgain) { runBackupCountsAgain = false; e.Result = LibraryCommands.GetCounts(e.Argument as IEnumerable); } } private async void UpdateCountsBw_CompletedAsync(object? sender, System.ComponentModel.RunWorkerCompletedEventArgs e) { // Reading e.Result rethrows any exception from UpdateCountsBw_DoWork. Because this is an // async void handler with no SynchronizationContext, that would become an unhandled // exception and crash the app. Check e.Error/e.Cancelled first and degrade gracefully. if (e.Cancelled) return; if (e.Error is not null) { Serilog.Log.Logger.Error(e.Error, "Failed to update backup counts"); return; } if (e.Result is not LibraryCommands.LibraryStats stats) return; LibraryStats = stats; if (Configuration.Instance.AutoDownloadEpisodes && stats.PendingBooks + stats.pdfsNotDownloaded > 0) { // RunWorkerCompleted has no SynchronizationContext; queue items require the UI thread. await Dispatcher.UIThread.InvokeAsync(async () => await BackupAllBooksAsync(stats.LibraryBooks, notifyIfNothingQueued: false)); } } }