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

View File

@ -13,11 +13,12 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class LocateAudiobooksDialog : DialogWindow
{
private event EventHandler<FilePathCache.CacheEntry> FileFound;
private event EventHandler<FilePathCache.CacheEntry>? FileFound;
private readonly CancellationTokenSource tokenSource = new();
private readonly List<string> foundAsins = new();
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();
//If this dialog is closed before it's completed, Closing is fired
@ -50,7 +51,7 @@ namespace LibationAvalonia.Dialogs
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));
_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
{
Title = "Select the folder to search for audiobooks",
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();
@ -89,11 +90,13 @@ namespace LibationAvalonia.Dialogs
FilePathCache.Insert(book);
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));
tokenSource.Token.ThrowIfCancellationRequested();
FileFound?.Invoke(this, book);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
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.ProcessQueue;
#nullable enable
namespace LibationAvalonia.Views
{
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel item, QueuePosition queueButton);
public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel item);
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton);
public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel? item);
public partial class ProcessBookControl : UserControl
{
public static event QueueItemPositionButtonClicked PositionButtonClicked;
public static event QueueItemCancelButtonClicked CancelButtonClicked;
public static event QueueItemPositionButtonClicked? PositionButtonClicked;
public static event QueueItemCancelButtonClicked? CancelButtonClicked;
public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty =
AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true);
@ -31,12 +32,13 @@ namespace LibationAvalonia.Views
{
using var context = DbContexts.GetContext();
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;
}
}
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)
=> CancelButtonClicked?.Invoke(DataItem);

View File

@ -34,44 +34,51 @@ namespace LibationAvalonia.Views
var vm = new ProcessQueueViewModel();
DataContext = vm;
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()
{
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.FailedAbort,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.FailedSkip,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.FailedRetry,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.ValidationFail,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.Cancelled,
Status = ProcessBookStatus.Cancelled,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.Success,
Status = ProcessBookStatus.Completed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.None,
Status = ProcessBookStatus.Working,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
new ProcessBookViewModel(trialBook)
{
Result = ProcessBookResult.None,
Status = ProcessBookStatus.Queued,
@ -99,7 +106,7 @@ namespace LibationAvalonia.Views
#region Control event handlers
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel? item)
{
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();
if (Queue?.Current is not null)
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();
@ -128,12 +136,12 @@ namespace LibationAvalonia.Views
_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();
}
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)
{
@ -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();
if (Queue?.Current is not null)
await Queue.Current.CancelAsync();
}
private void btnClearFinished_Click(object sender, EventArgs e)
private void btnClearFinished_Click(object? sender, EventArgs e)
{
Queue?.ClearCompleted();

View File

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

View File

@ -84,7 +84,7 @@ namespace LibationFileManager
ProcessDirectory,
LocalAppData,
UserProfile,
Path.Combine(Path.GetTempPath(), "Libation")
WinTemp,
};
//Try to find and validate appsettings.json in each folder
@ -181,7 +181,7 @@ namespace LibationFileManager
}
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;
}
}

View File

@ -83,9 +83,11 @@ namespace LibationWinForms.Dialogs
if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
tokenSource.Token.ThrowIfCancellationRequested();
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);
}