Null safety and minor UI bugfix

Properly cancel the Locate Audiobooks when the dialog window closes before scanning is finished.
This commit is contained in:
Michael Bucari-Tovo 2025-08-04 17:15:37 -06:00
parent 29be091a4b
commit db588629c0
7 changed files with 55 additions and 42 deletions

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
#nullable enable
namespace DataLayer namespace DataLayer
{ {
// only library importing should use tracking. All else should be NoTracking. // only library importing should use tracking. All else should be NoTracking.
@ -24,13 +25,13 @@ namespace DataLayer
.Where(c => !c.Book.IsEpisodeParent() || includeParents) .Where(c => !c.Book.IsEpisodeParent() || includeParents)
.ToList(); .ToList();
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId) public static LibraryBook? GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
=> context => context
.LibraryBooks .LibraryBooks
.AsNoTrackingWithIdentityResolution() .AsNoTrackingWithIdentityResolution()
.GetLibraryBook(productId); .GetLibraryBook(productId);
public static LibraryBook GetLibraryBook(this IQueryable<LibraryBook> library, string productId) public static LibraryBook? GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
=> library => library
.GetLibrary() .GetLibrary()
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId); .SingleOrDefault(lb => lb.Book.AudibleProductId == productId);

View File

@ -13,11 +13,12 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
public partial class LocateAudiobooksDialog : DialogWindow public partial class LocateAudiobooksDialog : DialogWindow
{ {
private event EventHandler<FilePathCache.CacheEntry> FileFound; private event EventHandler<FilePathCache.CacheEntry>? FileFound;
private readonly CancellationTokenSource tokenSource = new(); private readonly CancellationTokenSource tokenSource = new();
private readonly List<string> foundAsins = new(); private readonly List<string> foundAsins = new();
private readonly LocatedAudiobooksViewModel _viewModel; private readonly LocatedAudiobooksViewModel _viewModel;
@ -41,7 +42,7 @@ namespace LibationAvalonia.Dialogs
} }
} }
private void LocateAudiobooksDialog_Closing(object sender, System.ComponentModel.CancelEventArgs e) private void LocateAudiobooksDialog_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{ {
tokenSource.Cancel(); tokenSource.Cancel();
//If this dialog is closed before it's completed, Closing is fired //If this dialog is closed before it's completed, Closing is fired
@ -50,7 +51,7 @@ namespace LibationAvalonia.Dialogs
this.SaveSizeAndLocation(Configuration.Instance); this.SaveSizeAndLocation(Configuration.Instance);
} }
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e) private void LocateAudiobooks_FileFound(object? sender, FilePathCache.CacheEntry e)
{ {
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path)); var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
_viewModel.FoundFiles.Add(newItem); _viewModel.FoundFiles.Add(newItem);
@ -63,13 +64,13 @@ namespace LibationAvalonia.Dialogs
} }
} }
private async void LocateAudiobooksDialog_Opened(object sender, EventArgs e) private async void LocateAudiobooksDialog_Opened(object? sender, EventArgs e)
{ {
var folderPicker = new FolderPickerOpenOptions var folderPicker = new FolderPickerOpenOptions
{ {
Title = "Select the folder to search for audiobooks", Title = "Select the folder to search for audiobooks",
AllowMultiple = false, AllowMultiple = false,
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix) SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? "")
}; };
var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath(); var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath();
@ -89,11 +90,13 @@ namespace LibationAvalonia.Dialogs
FilePathCache.Insert(book); FilePathCache.Insert(book);
var lb = context.GetLibraryBook_Flat_NoTracking(book.Id); var lb = context.GetLibraryBook_Flat_NoTracking(book.Id);
if (lb?.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated) if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated)); await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
tokenSource.Token.ThrowIfCancellationRequested();
FileFound?.Invoke(this, book); FileFound?.Invoke(this, book);
} }
catch (OperationCanceledException) { }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book); Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);

View File

@ -5,14 +5,15 @@ using DataLayer;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.ProcessQueue; using LibationUiBase.ProcessQueue;
#nullable enable
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
{ {
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel item, QueuePosition queueButton); public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton);
public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel item); public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel? item);
public partial class ProcessBookControl : UserControl public partial class ProcessBookControl : UserControl
{ {
public static event QueueItemPositionButtonClicked PositionButtonClicked; public static event QueueItemPositionButtonClicked? PositionButtonClicked;
public static event QueueItemCancelButtonClicked CancelButtonClicked; public static event QueueItemCancelButtonClicked? CancelButtonClicked;
public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty = public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty =
AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true); AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true);
@ -31,12 +32,13 @@ namespace LibationAvalonia.Views
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
ViewModels.MainVM.Configure_NonUI(); ViewModels.MainVM.Configure_NonUI();
DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")); if (context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") is LibraryBook book)
DataContext = new ProcessBookViewModel(book);
return; return;
} }
} }
private ProcessBookViewModel DataItem => DataContext is null ? null : DataContext as ProcessBookViewModel; private ProcessBookViewModel? DataItem => DataContext as ProcessBookViewModel;
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> CancelButtonClicked?.Invoke(DataItem); => CancelButtonClicked?.Invoke(DataItem);

View File

@ -34,44 +34,51 @@ namespace LibationAvalonia.Views
var vm = new ProcessQueueViewModel(); var vm = new ProcessQueueViewModel();
DataContext = vm; DataContext = vm;
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
var trialBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") ?? context.GetLibrary_Flat_NoTracking().FirstOrDefault();
if (trialBook is null)
return;
List<ProcessBookViewModel> testList = new() List<ProcessBookViewModel> testList = new()
{ {
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.FailedAbort, Result = ProcessBookResult.FailedAbort,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.FailedSkip, Result = ProcessBookResult.FailedSkip,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.FailedRetry, Result = ProcessBookResult.FailedRetry,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.ValidationFail, Result = ProcessBookResult.ValidationFail,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.Cancelled, Result = ProcessBookResult.Cancelled,
Status = ProcessBookStatus.Cancelled, Status = ProcessBookStatus.Cancelled,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.Success, Result = ProcessBookResult.Success,
Status = ProcessBookStatus.Completed, Status = ProcessBookStatus.Completed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.None, Result = ProcessBookResult.None,
Status = ProcessBookStatus.Working, Status = ProcessBookStatus.Working,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.None, Result = ProcessBookResult.None,
Status = ProcessBookStatus.Queued, Status = ProcessBookStatus.Queued,
@ -99,7 +106,7 @@ namespace LibationAvalonia.Views
#region Control event handlers #region Control event handlers
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item) private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel? item)
{ {
if (item is not null) if (item is not null)
{ {
@ -108,19 +115,20 @@ namespace LibationAvalonia.Views
} }
} }
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton) private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton)
{ {
Queue?.MoveQueuePosition(item, queueButton); if (item is not null)
Queue?.MoveQueuePosition(item, queueButton);
} }
public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void CancelAllBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
Queue?.ClearQueue(); Queue?.ClearQueue();
if (Queue?.Current is not null) if (Queue?.Current is not null)
await Queue.Current.CancelAsync(); await Queue.Current.CancelAsync();
} }
public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ClearFinishedBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
Queue?.ClearCompleted(); Queue?.ClearCompleted();
@ -128,12 +136,12 @@ namespace LibationAvalonia.Views
_viewModel.RunningTime = string.Empty; _viewModel.RunningTime = string.Empty;
} }
public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ClearLogBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
_viewModel?.LogEntries.Clear(); _viewModel?.LogEntries.Clear();
} }
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) private async void LogCopyBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
if (_viewModel is ProcessQueueViewModel vm) if (_viewModel is ProcessQueueViewModel vm)
{ {
@ -143,14 +151,14 @@ namespace LibationAvalonia.Views
} }
} }
private async void cancelAllBtn_Click(object sender, EventArgs e) private async void cancelAllBtn_Click(object? sender, EventArgs e)
{ {
Queue?.ClearQueue(); Queue?.ClearQueue();
if (Queue?.Current is not null) if (Queue?.Current is not null)
await Queue.Current.CancelAsync(); await Queue.Current.CancelAsync();
} }
private void btnClearFinished_Click(object sender, EventArgs e) private void btnClearFinished_Click(object? sender, EventArgs e)
{ {
Queue?.ClearCompleted(); Queue?.ClearCompleted();

View File

@ -62,25 +62,22 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
List<LibraryBook> sampleEntries; LibraryBook?[] sampleEntries;
try try
{ {
sampleEntries = new() sampleEntries = [
{
//context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),try{
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"),
context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6") context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")];
};
} }
catch { sampleEntries = new(); } catch { sampleEntries = []; }
var pdvm = new ProductsDisplayViewModel(); var pdvm = new ProductsDisplayViewModel();
_ = pdvm.BindToGridAsync(sampleEntries); _ = pdvm.BindToGridAsync(sampleEntries.OfType<LibraryBook>().ToList());
DataContext = pdvm; DataContext = pdvm;
setGridScale(1); setGridScale(1);

View File

@ -84,7 +84,7 @@ namespace LibationFileManager
ProcessDirectory, ProcessDirectory,
LocalAppData, LocalAppData,
UserProfile, UserProfile,
Path.Combine(Path.GetTempPath(), "Libation") WinTemp,
}; };
//Try to find and validate appsettings.json in each folder //Try to find and validate appsettings.json in each folder
@ -181,7 +181,7 @@ namespace LibationFileManager
} }
catch (Exception e) catch (Exception e)
{ {
Serilog.Log.Error(e, "Failed to run shell command. {Arguments}", psi.ArgumentList); Serilog.Log.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
return null; return null;
} }
} }

View File

@ -83,9 +83,11 @@ namespace LibationWinForms.Dialogs
if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated) if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated)); await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
tokenSource.Token.ThrowIfCancellationRequested();
this.Invoke(FileFound, this, book); this.Invoke(FileFound, this, book);
} }
catch(Exception ex) catch (OperationCanceledException) { }
catch (Exception ex)
{ {
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book); Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
} }