Files
Libation/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs
T
Cursor Agentandrmcrackan cb01b9b3b1 Keep pointing at the trash when the library itself is empty
Two states were wrong, both when the live library is empty but the trash is not.
GetLibrary() filters IsDeleted, so an all-trashed library reads as empty and the
getting-started panel took over.

Without a filter it said 'Libation is empty' while 29 books sat in the trash -
the books are not gone, they are one click away. The panel now carries a trash
line when there is anything in there.

With a filter it was worse: searching for a book that IS in the trash showed
'Libation is empty. Add your Audible account' and suppressed the trash hint
entirely, which is the exact failure #1925 was about, reintroduced in the one
case where the library is empty.

The rule is now that someone searching is not someone getting started, so a
filter always answers for itself. That leaves one merely-suboptimal case, an
empty library filtered to something in neither place, which says 'no books
match' - true, and better than claiming the library is empty when it is being
filtered.

Also fixes what made this invisible while testing: GettingStartedVisible depends
on the filter but nothing raised it when the filter changed, so the panel never
re-evaluated.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-18 20:17:32 +00:00

138 lines
4.6 KiB
C#

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();
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
/// <summary> The "Begin PDF Only Backup" menu item header text </summary>
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
/// <summary> How many books are in the trash, where nothing else on screen would reveal them </summary>
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;
/// <summary>
/// Re-read the trash count. Kept off <see cref="LibraryCommands.GetCounts"/> on purpose: that also runs
/// against the visible subset on every filter change, where a database round trip would be wasted work.
/// </summary>
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");
}
}
/// <summary> The user's library statistics </summary>
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<LibraryBook>? 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<LibraryBook>);
}
}
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));
}
}
}