mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 06:07:30 -04:00
The blank grid said nothing, and the only pointer anywhere was the status bar's
'No books. Begin by importing your library' - which is the wrong advice for
someone who has not added an account yet, since Import has nothing to scan.
An empty library has two causes with different next steps, and AccountsCount
already knows which one applies, so say the one that fits and offer the action
that goes with it:
no accounts Libation is empty.
Add your Audible account, then scan your library ...
[Add Account] [Take a Guided Tour]
no books No books in your library yet.
Scan your Audible account to bring your books in.
[Scan Library] [Take a Guided Tour]
The buttons run the same commands the menu items do, so the two cannot drift.
The tour is there because it is offered once on first launch and never again
once declined; Settings > Take a Guided Tour is not somewhere a new user looks.
Held back until LibraryStats has been counted at least once, which is what keeps
'Add your Audible account' off the screen in the moment between the window
appearing and a full library loading, and held back mid-scan. An empty library
takes precedence over an empty filter result, since 'no books match' is true but
useless when there are no books at all.
Both grid empty states now share one set of strings in GridEmptyStateUi rather
than the no-matches pair living in TrashBinUi, which was already the wrong home
for them and would only get worse with a third.
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
296 lines
11 KiB
C#
296 lines
11 KiB
C#
using ApplicationServices;
|
|
using AudibleUtilities;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Threading;
|
|
using LibationFileManager;
|
|
using LibationUiBase;
|
|
using LibationUiBase.Forms;
|
|
using ReactiveUI;
|
|
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LibationAvalonia.ViewModels;
|
|
|
|
public partial class MainVM
|
|
{
|
|
private int _numAccountsScanning = 2;
|
|
public string LocateAudiobooksTip => Configuration.GetHelpText("LocateAudiobooks");
|
|
|
|
/// <summary> Auto scanning accounts is enables </summary>
|
|
public bool AutoScanChecked { get => field; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref field, value); } = Configuration.Instance.AutoScan;
|
|
/// <summary> Display text for the "Remove # Books from Libation" button </summary>
|
|
public string RemoveBooksButtonText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = "Remove # Books from Libation";
|
|
/// <summary> Indicates if the "Remove # Books from Libation" button is enabled </summary>
|
|
public bool RemoveBooksButtonEnabled { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } } = Design.IsDesignMode;
|
|
/// <summary> Indicates if the "Remove # Books from Libation" and "Done Removing" buttons should be visible </summary>
|
|
public bool RemoveButtonsVisible
|
|
{
|
|
get => field;
|
|
set
|
|
{
|
|
this.RaiseAndSetIfChanged(ref field, value);
|
|
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
|
|
}
|
|
} = Design.IsDesignMode;
|
|
/// <summary> Indicates if Libation is currently scanning account(s) </summary>
|
|
public bool ActivelyScanning => _numAccountsScanning > 0;
|
|
/// <summary> Indicates if the "Remove Books" menu items are enabled</summary>
|
|
public bool RemoveMenuItemsEnabled => !RemoveButtonsVisible && !ActivelyScanning;
|
|
/// <summary> The library scanning status text </summary>
|
|
public string ScanningText => _numAccountsScanning == 1 ? "Scanning..." : $"Scanning {_numAccountsScanning} accounts...";
|
|
/// <summary> There is at least one Audible account </summary>
|
|
public bool AnyAccounts => AccountsCount > 0;
|
|
/// <summary> There is exactly one Audible account </summary>
|
|
public bool OneAccount => AccountsCount == 1;
|
|
/// <summary> There are more than 1 Audible accounts </summary>
|
|
public bool MultipleAccounts => AccountsCount > 1;
|
|
/// <summary> The number of accounts added to Libation </summary>
|
|
public int AccountsCount
|
|
{
|
|
get => field;
|
|
set
|
|
{
|
|
this.RaiseAndSetIfChanged(ref field, value);
|
|
this.RaisePropertyChanged(nameof(AnyAccounts));
|
|
this.RaisePropertyChanged(nameof(OneAccount));
|
|
this.RaisePropertyChanged(nameof(MultipleAccounts));
|
|
RaiseGettingStartedChanged();
|
|
}
|
|
}
|
|
|
|
|
|
public void Configure_Import()
|
|
{
|
|
MainWindow.Loaded += (_, _) =>
|
|
{
|
|
refreshImportMenu();
|
|
AccountsSettingsPersister.Saved += (_, _)
|
|
=> Avalonia.Threading.Dispatcher.UIThread.Invoke(refreshImportMenu);
|
|
};
|
|
|
|
AutoScanChecked = Configuration.Instance.AutoScan;
|
|
|
|
setyNumScanningAccounts(0);
|
|
LibraryCommands.ScanBegin += (_, accountsLength) => Dispatcher.UIThread.Post(() => setyNumScanningAccounts(accountsLength));
|
|
LibraryCommands.ScanEnd += (_, _) => Dispatcher.UIThread.Post(() => setyNumScanningAccounts(0));
|
|
|
|
if (!Design.IsDesignMode)
|
|
RemoveButtonsVisible = false;
|
|
}
|
|
|
|
public void ToggleAutoScan() => AutoScanChecked = !AutoScanChecked;
|
|
|
|
public async Task AddAccountsAsync()
|
|
{
|
|
await MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
|
|
await new LibationAvalonia.Dialogs.AccountsDialog().ShowDialog(MainWindow);
|
|
}
|
|
|
|
public async Task ScanAccountAsync()
|
|
{
|
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
|
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
|
if (firstAccount != null)
|
|
await scanLibrariesAsync(firstAccount);
|
|
}
|
|
|
|
public async Task ScanAllAccountsAsync()
|
|
{
|
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
|
await scanLibrariesAsync(persister.AccountsSettings.GetAll().ToArray());
|
|
}
|
|
|
|
public async Task ScanSomeAccountsAsync()
|
|
{
|
|
var scanAccountsDialog = new LibationAvalonia.Dialogs.ScanAccountsDialog();
|
|
|
|
if (await scanAccountsDialog.ShowDialog<DialogResult>(MainWindow) != DialogResult.OK)
|
|
return;
|
|
|
|
if (!scanAccountsDialog.CheckedAccounts.Any())
|
|
return;
|
|
|
|
await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts.ToArray());
|
|
}
|
|
|
|
public async Task RemoveBooksAsync()
|
|
{
|
|
// if 0 accounts, this will not be visible
|
|
// if 1 account, run scanLibrariesRemovedBooks() on this account
|
|
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
|
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
|
var accounts = persister.AccountsSettings.GetAll();
|
|
|
|
if (accounts.Count != 1)
|
|
return;
|
|
|
|
var firstAccount = accounts.Single();
|
|
await scanLibrariesRemovedBooks(firstAccount);
|
|
}
|
|
|
|
// selectively remove books from all accounts
|
|
public async Task RemoveBooksAllAsync()
|
|
{
|
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
|
var allAccounts = persister.AccountsSettings.GetAll();
|
|
await scanLibrariesRemovedBooks(allAccounts.ToArray());
|
|
}
|
|
|
|
public async Task RemoveBooksBtn()
|
|
{
|
|
RemoveBooksButtonEnabled = false;
|
|
await ProductsDisplay.RemoveCheckedBooksAsync();
|
|
RemoveBooksButtonEnabled = true;
|
|
}
|
|
|
|
public async Task DoneRemovingBtn()
|
|
{
|
|
RemoveButtonsVisible = false;
|
|
|
|
ProductsDisplay.DoneRemovingBooks();
|
|
|
|
//Restore the filter
|
|
await PerformFilter(lastGoodFilter);
|
|
}
|
|
|
|
// selectively remove books from some accounts
|
|
public async Task RemoveBooksSomeAsync()
|
|
{
|
|
var scanAccountsDialog = new LibationAvalonia.Dialogs.ScanAccountsDialog();
|
|
|
|
if (await scanAccountsDialog.ShowDialog<DialogResult>(MainWindow) != DialogResult.OK)
|
|
return;
|
|
|
|
if (!scanAccountsDialog.CheckedAccounts.Any())
|
|
return;
|
|
|
|
await scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
|
|
}
|
|
|
|
public async Task LocateAudiobooksAsync()
|
|
{
|
|
var result = await MessageBox.Show(
|
|
MainWindow,
|
|
Configuration.GetHelpText(nameof(LibationAvalonia.Dialogs.LocateAudiobooksDialog)),
|
|
"Locate Previously-Liberated Audiobook Files",
|
|
MessageBoxButtons.OKCancel,
|
|
MessageBoxIcon.Information,
|
|
MessageBoxDefaultButton.Button1);
|
|
|
|
if (result is DialogResult.OK)
|
|
{
|
|
var locateDialog = new LibationAvalonia.Dialogs.LocateAudiobooksDialog();
|
|
await locateDialog.ShowDialog(MainWindow);
|
|
}
|
|
}
|
|
|
|
private void setyNumScanningAccounts(int numScanning)
|
|
{
|
|
_numAccountsScanning = numScanning;
|
|
this.RaisePropertyChanged(nameof(ActivelyScanning));
|
|
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
|
|
this.RaisePropertyChanged(nameof(ScanningText));
|
|
RaiseGettingStartedChanged();
|
|
}
|
|
|
|
private async Task scanLibrariesRemovedBooks(params Account[] accounts)
|
|
{
|
|
//This action is meant to operate on the entire library.
|
|
//For removing books within a filter set, use
|
|
//Visible Books > Remove from library
|
|
|
|
await ProductsDisplay.Filter(null);
|
|
|
|
RemoveBooksButtonEnabled = true;
|
|
RemoveButtonsVisible = true;
|
|
|
|
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
|
}
|
|
|
|
private async Task scanLibrariesAsync(params Account[]? accounts)
|
|
{
|
|
try
|
|
{
|
|
var (totalProcessed, newAdded) = await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts));
|
|
autoScanRunner?.OnManualScanSucceeded();
|
|
|
|
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
|
|
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
|
|
await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Serilog.Log.Information("Audible login attempt cancelled by user");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (WebView2LoginErrorMessage.TryFindInTree(ex, out var webViewEx) && webViewEx is not null)
|
|
{
|
|
await MessageBox.ShowAdminAlert(
|
|
MainWindow,
|
|
WebView2LoginErrorMessage.ExplainerBody,
|
|
WebView2LoginErrorMessage.Caption,
|
|
webViewEx);
|
|
}
|
|
else if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null)
|
|
{
|
|
await MessageBox.ShowAdminAlert(
|
|
MainWindow,
|
|
htmlEx.GetExplainerBody(),
|
|
NonJsonResponseExceptionExtensions.LibraryScanFailedCaption,
|
|
htmlEx);
|
|
}
|
|
else
|
|
{
|
|
await MessageBox.ShowAdminAlert(
|
|
MainWindow,
|
|
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
|
|
"Error importing library",
|
|
ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void refreshImportMenu()
|
|
{
|
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
|
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
|
|
|
|
|
if (NativeMenu.GetMenu(MainWindow)?.Items[0] is not NativeMenuItem ss ||
|
|
ss.Menu is not NativeMenu importMenuItem)
|
|
{
|
|
Serilog.Log.Logger.Error($"Unable to find {nameof(importMenuItem)}");
|
|
return;
|
|
}
|
|
|
|
|
|
for (int i = importMenuItem.Items.Count - 1; i >= 2; i--)
|
|
importMenuItem.Items.RemoveAt(i);
|
|
|
|
if (AccountsCount < 1)
|
|
{
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
|
}
|
|
else if (AccountsCount == 1)
|
|
{
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyGestureHelper.MenuModifier) });
|
|
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyGestureHelper.MenuModifier) });
|
|
}
|
|
else
|
|
{
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyGestureHelper.MenuModifier) });
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyGestureHelper.MenuModifier | KeyModifiers.Shift) });
|
|
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyGestureHelper.MenuModifier) });
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyGestureHelper.MenuModifier | KeyModifiers.Shift) });
|
|
}
|
|
|
|
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
|
}
|
|
}
|