diff --git a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs index 8f350cb2..fb33474f 100644 --- a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs +++ b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs @@ -25,9 +25,8 @@ namespace AaxDecrypter public override async Task CancelAsync() { - IsCanceled = true; + await base.CancelAsync(); await (AaxConversion?.CancelAsync() ?? Task.CompletedTask); - FinalizeDownload(); } private Mp4File Open() diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 355b3eca..c5389d29 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -120,7 +120,12 @@ namespace AaxDecrypter } } - public abstract Task CancelAsync(); + public virtual Task CancelAsync() + { + IsCanceled = true; + FinalizeDownload(); + return Task.CompletedTask; + } protected abstract Task Step_DownloadAndDecryptAudiobookAsync(); public virtual void SetCoverArt(byte[] coverArt) { } diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index 200a4b5f..18c57054 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -17,13 +17,6 @@ namespace AaxDecrypter AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync; } - public override Task CancelAsync() - { - IsCanceled = true; - FinalizeDownload(); - return Task.CompletedTask; - } - protected override async Task Step_DownloadAndDecryptAudiobookAsync() { await InputFileStream.DownloadTask; diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj index 5d577965..e402c4c4 100644 --- a/Source/AppScaffolding/AppScaffolding.csproj +++ b/Source/AppScaffolding/AppScaffolding.csproj @@ -7,10 +7,9 @@ - + - diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 41d25595..2653ab57 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -115,11 +115,22 @@ namespace AppScaffolding { if (config.GetObject("Serilog") is JObject serilog) { - if (serilog["WriteTo"] is JArray sinks && sinks.FirstOrDefault(s => s["Name"].Value() is "File") is JToken fileSink) + bool fileChanged = false; + if (serilog.SelectToken("$.WriteTo[?(@.Name == 'ZipFile')]", false) is JObject zipFileSink) { - fileSink["Name"] = "ZipFile"; - config.SetNonString(serilog.DeepClone(), "Serilog"); + zipFileSink["Name"] = "File"; + fileChanged = true; } + var hooks = $"{nameof(LibationFileManager)}.{nameof(FileSinkHook)}, {nameof(LibationFileManager)}"; + if (serilog.SelectToken("$.WriteTo[?(@.Name == 'File')].Args", false) is JObject fileSinkArgs + && fileSinkArgs["hooks"]?.Value() != hooks) + { + fileSinkArgs["hooks"] = hooks; + fileChanged = true; + } + + if (fileChanged) + config.SetNonString(serilog.DeepClone(), "Serilog"); return; } @@ -129,17 +140,17 @@ namespace AppScaffolding { "WriteTo", new JArray { // ABOUT SINKS - // Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use. + // Only File sink is currently used. By user request (June 2024) others packages are included for experimental use. // new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved new JObject { - { "Name", "ZipFile" }, + { "Name", "File" }, { "Args", new JObject { // for this sink to work, a path must be provided. we override this below - { "path", Path.Combine(config.LibationFiles, "_Log.log") }, + { "path", Path.Combine(config.LibationFiles, "Log.log") }, { "rollingInterval", "Month" }, // Serilog template formatting examples // - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}" @@ -274,7 +285,7 @@ namespace AppScaffolding disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value), }); - if (InteropFactory.InteropFunctionsType is null) + if (InteropFactory.InteropFunctionsType is null) Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null"); } diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 3ead2a9f..a349b497 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AaxDecrypter; using ApplicationServices; @@ -18,10 +19,15 @@ namespace FileLiberator { public override string Name => "Download & Decrypt"; private AudiobookDownloadBase abDownloader; + private readonly CancellationTokenSource cancellationTokenSource = new(); - public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists(); - - public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask; + public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists(); + public override async Task CancelAsync() + { + cancellationTokenSource.Cancel(); + if (abDownloader is not null) + await abDownloader.CancelAsync(); + } public override async Task ProcessAsync(LibraryBook libraryBook) { @@ -41,8 +47,9 @@ namespace FileLiberator } OnBegin(libraryBook); + var cancellationToken = cancellationTokenSource.Token; - try + try { if (libraryBook.Book.Audio_Exists()) return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; @@ -50,7 +57,7 @@ namespace FileLiberator downloadValidation(libraryBook); var api = await libraryBook.GetApiAsync(); var config = Configuration.Instance; - using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook); + using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook, cancellationToken); bool success = false; try @@ -74,38 +81,32 @@ namespace FileLiberator .Where(f => f.FileType != FileType.AAXC) .Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path)))); - return - abDownloader?.IsCanceled is true - ? new StatusHandler { "Cancelled" } - : new StatusHandler { "Decrypt failed" }; + cancellationToken.ThrowIfCancellationRequested(); + return new StatusHandler { "Decrypt failed" }; } var finalStorageDir = getDestinationDirectory(libraryBook); - var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries)); + var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries, cancellationToken)); Task[] finalTasks = [ - Task.Run(() => downloadCoverArt(downloadOptions)), + Task.Run(() => downloadCoverArt(downloadOptions, cancellationToken)), moveFilesTask, - Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir)) + Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir, cancellationToken)) ]; try { - await Task.WhenAll(finalTasks); - } - catch - { + await Task.WhenAll(finalTasks); + } + catch when (!moveFilesTask.IsFaulted) + { //Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions. - //Only fail if the downloaded audio files failed to move to Books directory - if (moveFilesTask.IsFaulted) - { - throw; - } - } - finally + //Only fail if the downloaded audio files failed to move to Books directory + } + finally { - if (moveFilesTask.IsCompletedSuccessfully) + if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested) { await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)); @@ -114,8 +115,12 @@ namespace FileLiberator } return new StatusHandler(); - } - finally + } + catch when (cancellationToken.IsCancellationRequested) + { + return new StatusHandler { "Cancelled" }; + } + finally { OnCompleted(libraryBook); } @@ -257,16 +262,17 @@ namespace FileLiberator /// Move new files to 'Books' directory /// Return directory if audiobook file(s) were successfully created and can be located on disk. Else null. - private static void moveFilesToBooksDir(LibraryBook libraryBook, List entries) + private static void moveFilesToBooksDir(LibraryBook libraryBook, List entries, CancellationToken cancellationToken) { // create final directory. move each file into it var destinationDir = getDestinationDirectory(libraryBook); + cancellationToken.ThrowIfCancellationRequested(); for (var i = 0; i < entries.Count; i++) { var entry = entries[i]; - var realDest + var realDest = FileUtility.SaferMoveToValidPath( entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), @@ -278,7 +284,8 @@ namespace FileLiberator // propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) entries[i] = entry with { Path = realDest }; - } + cancellationToken.ThrowIfCancellationRequested(); + } var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue); if (cue != default) @@ -287,7 +294,8 @@ namespace FileLiberator SetFileTime(libraryBook, cue.Path); } - AudibleFileStorage.Audio.Refresh(); + cancellationToken.ThrowIfCancellationRequested(); + AudibleFileStorage.Audio.Refresh(); } private static string getDestinationDirectory(LibraryBook libraryBook) @@ -301,7 +309,7 @@ namespace FileLiberator private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable entries) => entries.FirstOrDefault(f => f.FileType == FileType.Audio); - private static void downloadCoverArt(DownloadOptions options) + private static void downloadCoverArt(DownloadOptions options, CancellationToken cancellationToken) { if (!Configuration.Instance.DownloadCoverArt) return; @@ -316,7 +324,7 @@ namespace FileLiberator if (File.Exists(coverPath)) FileUtility.SaferDelete(coverPath); - var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native)); + var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native), cancellationToken); if (picBytes.Length > 0) { File.WriteAllBytes(coverPath, picBytes); @@ -327,6 +335,7 @@ namespace FileLiberator { //Failure to download cover art should not be considered a failure to download the book Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product."); + throw; } } } diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs index 94368534..95fdacb7 100644 --- a/Source/FileLiberator/DownloadOptions.Factory.cs +++ b/Source/FileLiberator/DownloadOptions.Factory.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; #nullable enable @@ -24,9 +25,10 @@ public partial class DownloadOptions /// /// Initiate an audiobook download from the audible api. /// - public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook) + public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token) { - var license = await ChooseContent(api, libraryBook, config); + var license = await ChooseContent(api, libraryBook, config, token); + token.ThrowIfCancellationRequested(); //Some audiobooks will have incorrect chapters in the metadata returned from the license request, //but the metadata returned by the content metadata endpoint will be correct. Call the content @@ -36,9 +38,8 @@ public partial class DownloadOptions if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs) license.ContentMetadata.ChapterInfo = metadata.ChapterInfo; - var options = BuildDownloadOptions(libraryBook, config, license); - - return options; + token.ThrowIfCancellationRequested(); + return BuildDownloadOptions(libraryBook, config, license); } private class LicenseInfo @@ -57,16 +58,18 @@ public partial class DownloadOptions => voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)]; } - private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config) + private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token) { var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High; if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm) { + token.ThrowIfCancellationRequested(); var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); return new LicenseInfo(license); } + token.ThrowIfCancellationRequested(); try { //try to request a widevine content license using the user's spatial audio settings @@ -85,8 +88,8 @@ public partial class DownloadOptions return new LicenseInfo(contentLic); using var client = new HttpClient(); - using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse); - var dash = new MpegDash(mpdResponse.Content.ReadAsStream()); + using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse, token); + var dash = new MpegDash(mpdResponse.Content.ReadAsStream(token)); if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri)) throw new InvalidDataException("Failed to get mpeg-dash content download url."); diff --git a/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs b/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs index f99d9bda..d4caadfc 100644 --- a/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs +++ b/Source/LibationAvalonia/Controls/DataGridCheckBoxColumnExt.cs @@ -9,7 +9,7 @@ namespace LibationAvalonia.Controls { //Only SeriesEntry types have three-state checks, individual LibraryEntry books are binary. var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox; - ele.IsThreeState = dataItem is ISeriesEntry; + ele.IsThreeState = dataItem is SeriesEntry; return ele; } } diff --git a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs index d4db742b..10b6c099 100644 --- a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs +++ b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs @@ -34,11 +34,11 @@ namespace LibationAvalonia.Controls private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e) { if (sender is DataGridCell cell && - cell.DataContext is IGridEntry clickedEntry && + cell.DataContext is GridEntry clickedEntry && OwningColumnProperty.GetValue(cell) is DataGridColumn column && OwningGridProperty.GetValue(column) is DataGrid grid) { - var allSelected = grid.SelectedItems.OfType().ToArray(); + var allSelected = grid.SelectedItems.OfType().ToArray(); var clickedIndex = Array.IndexOf(allSelected, clickedEntry); if (clickedIndex == -1) { @@ -101,7 +101,7 @@ namespace LibationAvalonia.Controls private static string RemoveLineBreaks(string text) => text.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' '); - private string GetRowClipboardContents(IGridEntry gridEntry) + private string GetRowClipboardContents(GridEntry gridEntry) { var contents = Grid.Columns.Where(c => c.IsVisible).OrderBy(c => c.DisplayIndex).Select(c => RemoveLineBreaks(GetCellValue(c, gridEntry))).ToArray(); return string.Join("\t", contents); @@ -109,7 +109,7 @@ namespace LibationAvalonia.Controls public required DataGrid Grid { get; init; } public required DataGridColumn Column { get; init; } - public required IGridEntry[] GridEntries { get; init; } + public required GridEntry[] GridEntries { get; init; } public required ContextMenu ContextMenu { get; init; } public AvaloniaList ContextMenuItems => ContextMenu.ItemsSource as AvaloniaList; diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs index 03e2f05f..272f5da7 100644 --- a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs +++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs @@ -58,10 +58,5 @@ namespace LibationAvalonia.Controls.Settings } ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged; } - - public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); - } } } diff --git a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs index 214ea33b..ee35a855 100644 --- a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs +++ b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs @@ -36,11 +36,11 @@ public partial class ThemePreviewControl : UserControl PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray()); } - QueuedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Queued }; - WorkingBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Working }; - CompletedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Completed }; - CancelledBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Cancelled }; - FailedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Failed }; + QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued }; + WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working }; + CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed }; + CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled }; + FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed }; //Set the current processable so that the empty queue doesn't try to advance. QueuedBook.AddDownloadPdf(); diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs index bf4f8b56..3e5a5725 100644 --- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs +++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs @@ -1,5 +1,6 @@ using AudibleApi; using AudibleUtilities; +using Avalonia.Threading; using LibationUiBase.Forms; using System.Threading.Tasks; @@ -17,42 +18,46 @@ namespace LibationAvalonia.Dialogs.Login } public async Task Get2faCodeAsync(string prompt) - { - var dialog = new _2faCodeDialog(prompt); - if (await dialog.ShowDialogAsync() is DialogResult.OK) - return dialog.Code; - - return null; - } + => await Dispatcher.UIThread.InvokeAsync(async () => + { + var dialog = new _2faCodeDialog(prompt); + if (await dialog.ShowDialogAsync() is DialogResult.OK) + return dialog.Code; + return null; + }); public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage) - { - var dialog = new CaptchaDialog(password, captchaImage); - if (await dialog.ShowDialogAsync() is DialogResult.OK) - return (dialog.Password, dialog.Answer); - return (null, null); - } + => await Dispatcher.UIThread.InvokeAsync(async () => + { + var dialog = new CaptchaDialog(password, captchaImage); + if (await dialog.ShowDialogAsync() is DialogResult.OK) + return (dialog.Password, dialog.Answer); + return (null, null); + }); public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig) - { - var dialog = new MfaDialog(mfaConfig); - if (await dialog.ShowDialogAsync() is DialogResult.OK) - return (dialog.SelectedName, dialog.SelectedValue); - return (null, null); - } + => await Dispatcher.UIThread.InvokeAsync(async () => + { + var dialog = new MfaDialog(mfaConfig); + if (await dialog.ShowDialogAsync() is DialogResult.OK) + return (dialog.SelectedName, dialog.SelectedValue); + return (null, null); + }); public async Task<(string email, string password)> GetLoginAsync() - { - var dialog = new LoginCallbackDialog(_account); - if (await dialog.ShowDialogAsync() is DialogResult.OK) - return (_account.AccountId, dialog.Password); - return (null, null); - } + => await Dispatcher.UIThread.InvokeAsync(async () => + { + var dialog = new LoginCallbackDialog(_account); + if (await dialog.ShowDialogAsync() is DialogResult.OK) + return (_account.AccountId, dialog.Password); + return (null, null); + }); public async Task ShowApprovalNeededAsync() - { - var dialog = new ApprovalNeededDialog(); - await dialog.ShowDialogAsync(); - } + => await Dispatcher.UIThread.InvokeAsync(async () => + { + var dialog = new ApprovalNeededDialog(); + await dialog.ShowDialogAsync(); + }); } } \ No newline at end of file diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs index d2872e3f..d6745ad5 100644 --- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs +++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs @@ -1,5 +1,6 @@ using AudibleApi; using AudibleUtilities; +using Avalonia.Threading; using LibationFileManager; using LibationUiBase.Forms; using System; @@ -21,6 +22,9 @@ namespace LibationAvalonia.Dialogs.Login } public async Task StartAsync(ChoiceIn choiceIn) + => await Dispatcher.UIThread.InvokeAsync(() => StartAsyncInternal(choiceIn)); + + private async Task StartAsyncInternal(ChoiceIn choiceIn) { if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10) { diff --git a/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs b/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs deleted file mode 100644 index 605f2e17..00000000 --- a/Source/LibationAvalonia/ViewModels/AvaloniaEntryStatus.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Avalonia.Media.Imaging; -using DataLayer; -using LibationUiBase.GridView; -using System; - -#nullable enable -namespace LibationAvalonia.ViewModels -{ - public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable - { - private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { } - public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook); - - protected override Bitmap LoadImage(byte[] picture) - => AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80); - - //Button icons are handled by LiberateStatusButton - protected override Bitmap? GetResourceImage(string rescName) => null; - } -} diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs index 952fb045..fa8b6377 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs @@ -202,7 +202,7 @@ namespace LibationAvalonia.ViewModels { try { - var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(accounts); + var (totalProcessed, newAdded) = await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts)); // 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) diff --git a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs index a35f1036..21d1b509 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs @@ -57,7 +57,7 @@ namespace LibationAvalonia.ViewModels } } - public void LiberateSeriesClicked(ISeriesEntry series) + public void LiberateSeriesClicked(SeriesEntry series) { try { diff --git a/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs index 2ae562c5..4a324ec5 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs @@ -5,6 +5,7 @@ using LibationFileManager; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; #nullable enable namespace LibationAvalonia.ViewModels @@ -27,7 +28,7 @@ namespace LibationAvalonia.ViewModels // in autoScan, new books SHALL NOT show dialog try { - await LibraryCommands.ImportAccountAsync(accounts); + await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts)); } catch (OperationCanceledException) { diff --git a/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs b/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs index d57594e3..384d247e 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs @@ -1,5 +1,6 @@ using LibationFileManager; using LibationUiBase; +using System; using System.IO; #nullable enable @@ -7,7 +8,7 @@ namespace LibationAvalonia.ViewModels { partial class MainVM { - private void Configure_NonUI() + public static void Configure_NonUI() { using var ms1 = new MemoryStream(); App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1); @@ -23,6 +24,20 @@ namespace LibationAvalonia.ViewModels PictureStorage.SetDefaultImage(PictureSize.Native, ms3.ToArray()); BaseUtil.SetLoadImageDelegate(AvaloniaUtils.TryLoadImageOrDefault); + BaseUtil.SetLoadResourceImageDelegate(LoadResourceImage); + } + private static Avalonia.Media.Imaging.Bitmap? LoadResourceImage(string resourceName) + { + try + { + using var stream = App.OpenAsset(resourceName); + return new Avalonia.Media.Imaging.Bitmap(stream); + } + catch (Exception ex) + { + Serilog.Log.Error(ex, "Failed to load resource image: {ResourceName}", resourceName); + return null; + } } } } diff --git a/Source/LibationAvalonia/ViewModels/MainVM.cs b/Source/LibationAvalonia/ViewModels/MainVM.cs index a57bf9c5..d369f064 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.cs @@ -2,6 +2,7 @@ using DataLayer; using LibationAvalonia.Views; using LibationFileManager; +using LibationUiBase.ProcessQueue; using ReactiveUI; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs deleted file mode 100644 index 3bc4b30f..00000000 --- a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DataLayer; -using LibationFileManager; -using LibationUiBase; -using LibationUiBase.ProcessQueue; - -#nullable enable -namespace LibationAvalonia.ViewModels; - -public class ProcessBookViewModel : ProcessBookViewModelBase -{ - - public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { } - - protected override object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize) - => AvaloniaUtils.TryLoadImageOrDefault(bytes, pictureSize); - -} \ No newline at end of file diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs deleted file mode 100644 index de329357..00000000 --- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Threading; -using DataLayer; -using LibationFileManager; -using LibationUiBase.ProcessQueue; -using System; -using System.Collections.ObjectModel; - -#nullable enable -namespace LibationAvalonia.ViewModels; - -public record LogEntry(DateTime LogDate, string? LogMessage) -{ - public string LogDateString => LogDate.ToShortTimeString(); -} - -public class ProcessQueueViewModel : ProcessQueueViewModelBase -{ - public ProcessQueueViewModel() : base(CreateEmptyList()) - { - Items = Queue.UnderlyingList as AvaloniaList - ?? throw new ArgumentNullException(nameof(Queue.UnderlyingList)); - - SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; - } - - private decimal _speedLimit; - public decimal SpeedLimitIncrement { get; private set; } - public ObservableCollection LogEntries { get; } = new(); - public AvaloniaList Items { get; } - - public decimal SpeedLimit - { - get - { - return _speedLimit; - } - set - { - var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024)); - var config = Configuration.Instance; - config.DownloadSpeedLimit = newValue; - - _speedLimit - = config.DownloadSpeedLimit <= newValue ? value - : value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024 - : 0; - - config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024); - - SpeedLimitIncrement = _speedLimit > 100 ? 10 - : _speedLimit > 10 ? 1 - : _speedLimit > 1 ? 0.1m - : 0.01m; - - RaisePropertyChanged(nameof(SpeedLimitIncrement)); - RaisePropertyChanged(nameof(SpeedLimit)); - } - } - - public override void WriteLine(string text) - => Dispatcher.UIThread.Invoke(() => LogEntries.Add(new(DateTime.Now, text.Trim()))); - - protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook) - => new ProcessBookViewModel(libraryBook, Logger); - - private static AvaloniaList CreateEmptyList() - { - if (Design.IsDesignMode) - _ = Configuration.Instance.LibationFiles; - return new AvaloniaList(); - } -} diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index e7416e14..304d8d21 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -26,9 +26,9 @@ namespace LibationAvalonia.ViewModels public event EventHandler? RemovableCountChanged; /// Backing list of all grid entries - private readonly AvaloniaList SOURCE = new(); + private readonly AvaloniaList SOURCE = new(); /// Grid entries included in the filter set. If null, all grid entries are shown - private HashSet? FilteredInGridEntries; + private HashSet? FilteredInGridEntries; public string? FilterString { get; private set; } private DataGridCollectionView? _gridEntries; @@ -43,15 +43,15 @@ namespace LibationAvalonia.ViewModels public List GetVisibleBookEntries() => FilteredInGridEntries? - .OfType() + .OfType() .Select(lbe => lbe.LibraryBook) .ToList() ?? SOURCE - .OfType() + .OfType() .Select(lbe => lbe.LibraryBook) .ToList(); - private IEnumerable GetAllBookEntries() + private IEnumerable GetAllBookEntries() => SOURCE .BookEntries(); @@ -112,8 +112,8 @@ namespace LibationAvalonia.ViewModels var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current); AvaloniaSynchronizationContext.SetSynchronizationContext(sc); - var geList = await LibraryBookEntry.GetAllProductsAsync(dbBooks); - var seriesEntries = await SeriesEntry.GetAllSeriesEntriesAsync(dbBooks); + var geList = await LibraryBookEntry.GetAllProductsAsync(dbBooks); + var seriesEntries = await SeriesEntry.GetAllSeriesEntriesAsync(dbBooks); //Add all IGridEntries to the SOURCE list. Note that SOURCE has not yet been linked to the UI via //the GridEntries property, so adding items to SOURCE will not trigger any refreshes or UI action. @@ -147,8 +147,8 @@ namespace LibationAvalonia.ViewModels private void GridEntries_CollectionChanged(object? sender = null, EventArgs? e = null) { var count - = FilteredInGridEntries?.OfType().Count() - ?? SOURCE.OfType().Count(); + = FilteredInGridEntries?.OfType().Count() + ?? SOURCE.OfType().Count(); VisibleCountChanged?.Invoke(this, count); } @@ -223,9 +223,9 @@ namespace LibationAvalonia.ViewModels GridEntries_CollectionChanged(); } - private void RemoveBooks(IEnumerable removedBooks, IEnumerable removedSeries) + private void RemoveBooks(IEnumerable removedBooks, IEnumerable removedSeries) { - foreach (var removed in removedBooks.Cast().Concat(removedSeries).Where(b => b is not null).ToList()) + foreach (var removed in removedBooks.Cast().Concat(removedSeries).Where(b => b is not null).ToList()) { if (GridEntries?.PassesFilter(removed) ?? false) GridEntries.Remove(removed); @@ -238,21 +238,21 @@ namespace LibationAvalonia.ViewModels } } - private void UpsertBook(LibraryBook book, ILibraryBookEntry? existingBookEntry) + private void UpsertBook(LibraryBook book, LibraryBookEntry? existingBookEntry) { if (existingBookEntry is null) // Add the new product to top - SOURCE.Insert(0, new LibraryBookEntry(book)); + SOURCE.Insert(0, new LibraryBookEntry(book)); else // update existing existingBookEntry.UpdateLibraryBook(book); } - private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks) + private void UpsertEpisode(LibraryBook episodeBook, LibraryBookEntry? existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks) { if (existingEpisodeEntry is null) { - ILibraryBookEntry episodeEntry; + LibraryBookEntry episodeEntry; var seriesEntry = seriesEntries.FindSeriesParent(episodeBook); @@ -270,7 +270,7 @@ namespace LibationAvalonia.ViewModels return; } - seriesEntry = new SeriesEntry(seriesBook, episodeBook); + seriesEntry = new SeriesEntry(seriesBook, episodeBook); seriesEntries.Add(seriesEntry); episodeEntry = seriesEntry.Children[0]; @@ -280,7 +280,7 @@ namespace LibationAvalonia.ViewModels else { //Series exists. Create and add episode child then update the SeriesEntry - episodeEntry = new LibraryBookEntry(episodeBook, seriesEntry); + episodeEntry = new LibraryBookEntry(episodeBook, seriesEntry); seriesEntry.Children.Add(episodeEntry); seriesEntry.Children.Sort((c1, c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex)); var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId); @@ -307,7 +307,7 @@ namespace LibationAvalonia.ViewModels } } - public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry) + public async Task ToggleSeriesExpanded(SeriesEntry seriesEntry) { seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded; @@ -332,7 +332,7 @@ namespace LibationAvalonia.ViewModels private bool CollectionFilter(object item) { - if (item is ILibraryBookEntry lbe + if (item is LibraryBookEntry lbe && lbe.Liberate.IsEpisode && lbe.Parent?.Liberate?.Expanded != true) return false; @@ -454,7 +454,7 @@ namespace LibationAvalonia.ViewModels private void GridEntry_PropertyChanged(object? sender, PropertyChangedEventArgs? e) { - if (e?.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry) + if (e?.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry) { int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true); RemovableCountChanged?.Invoke(this, removeCount); diff --git a/Source/LibationAvalonia/ViewModels/RowComparer.cs b/Source/LibationAvalonia/ViewModels/RowComparer.cs index 071bedc6..bcdf50b0 100644 --- a/Source/LibationAvalonia/ViewModels/RowComparer.cs +++ b/Source/LibationAvalonia/ViewModels/RowComparer.cs @@ -17,7 +17,7 @@ namespace LibationAvalonia.ViewModels public RowComparer(DataGridColumn? column) { Column = column; - PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded); + PropertyName = Column?.SortMemberPath ?? nameof(GridEntry.DateAdded); } //Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection diff --git a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs index df23a16c..d74710c8 100644 --- a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs +++ b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs @@ -57,7 +57,13 @@ namespace LibationAvalonia.ViewModels.Settings config.GridFontScaleFactor = linearRangeToScaleFactor(GridFontScaleFactor); config.GridScaleFactor = linearRangeToScaleFactor(GridScaleFactor); } - public void OpenLogFolderButton() => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); + public void OpenLogFolderButton() + { + if (System.IO.File.Exists(LogFileFilter.LogFilePath)) + Go.To.File(LogFileFilter.LogFilePath); + else + Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); + } public List KnownDirectories { get; } = new() { diff --git a/Source/LibationAvalonia/Views/LiberateStatusButton.axaml.cs b/Source/LibationAvalonia/Views/LiberateStatusButton.axaml.cs index f1217ca2..072137ce 100644 --- a/Source/LibationAvalonia/Views/LiberateStatusButton.axaml.cs +++ b/Source/LibationAvalonia/Views/LiberateStatusButton.axaml.cs @@ -53,8 +53,8 @@ namespace LibationAvalonia.Views private void LiberateStatusButton_DataContextChanged(object sender, EventArgs e) { //Force book status recheck when an entry is scrolled into view. - //This will force a recheck for a paprtially downloaded file. - var status = DataContext as ILibraryBookEntry; + //This will force a recheck for a partially downloaded file. + var status = DataContext as LibraryBookEntry; status?.Liberate.Invalidate(nameof(status.Liberate.BookStatus)); } diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs index 783e69e1..ee435f08 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs +++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs @@ -22,7 +22,7 @@ namespace LibationAvalonia.Views public MainWindow() { DataContext = new MainVM(this); - ApiExtended.LoginChoiceFactory = account => new Dialogs.Login.AvaloniaLoginChoiceEager(account); + ApiExtended.LoginChoiceFactory = account => Dispatcher.UIThread.Invoke(() => new Dialogs.Login.AvaloniaLoginChoiceEager(account)); AudibleApiStorage.LoadError += AudibleApiStorage_LoadError; InitializeComponent(); @@ -137,7 +137,7 @@ namespace LibationAvalonia.Views } public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook); - public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series); + public void ProductsDisplay_LiberateSeriesClicked(object _, SeriesEntry series) => ViewModel.LiberateSeriesClicked(series); public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook); BookDetailsDialog bookDetailsForm; diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml b/Source/LibationAvalonia/Views/ProcessBookControl.axaml index 864ce3ee..1796257f 100644 --- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml +++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:vm="clr-namespace:LibationAvalonia.ViewModels" + xmlns:vm="clr-namespace:LibationUiBase.ProcessQueue;assembly=LibationUiBase" xmlns:views="clr-namespace:LibationAvalonia.Views" x:DataType="vm:ProcessBookViewModel" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300" diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs index 9e66987d..f7adccf5 100644 --- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs +++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs @@ -2,7 +2,6 @@ using ApplicationServices; using Avalonia; using Avalonia.Controls; using DataLayer; -using LibationAvalonia.ViewModels; using LibationUiBase; using LibationUiBase.ProcessQueue; @@ -31,10 +30,8 @@ namespace LibationAvalonia.Views if (Design.IsDesignMode) { using var context = DbContexts.GetContext(); - DataContext = new ProcessBookViewModel( - context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), - LogMe.RegisterForm(default(ILogForm)) - ); + ViewModels.MainVM.Configure_NonUI(); + DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")); return; } } @@ -44,7 +41,7 @@ namespace LibationAvalonia.Views public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => CancelButtonClicked?.Invoke(DataItem); public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => PositionButtonClicked?.Invoke(DataItem, QueuePosition.Fisrt); + => PositionButtonClicked?.Invoke(DataItem, QueuePosition.First); public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp); public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml index f65a400e..163a6d3b 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml @@ -34,7 +34,7 @@ HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" AllowAutoHide="False"> - + diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs index 97ae56f9..35333cf4 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs @@ -1,9 +1,7 @@ using ApplicationServices; -using Avalonia; using Avalonia.Controls; using Avalonia.Data.Converters; using DataLayer; -using LibationAvalonia.ViewModels; using LibationUiBase; using LibationUiBase.ProcessQueue; using System; @@ -17,7 +15,7 @@ namespace LibationAvalonia.Views { public partial class ProcessQueueControl : UserControl { - private TrackedQueue? Queue => _viewModel?.Queue; + private TrackedQueue? Queue => _viewModel?.Queue; private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel; public ProcessQueueControl() @@ -31,48 +29,49 @@ namespace LibationAvalonia.Views #if DEBUG if (Design.IsDesignMode) { + _ = LibationFileManager.Configuration.Instance.LibationFiles; + ViewModels.MainVM.Configure_NonUI(); var vm = new ProcessQueueViewModel(); - var Logger = LogMe.RegisterForm(vm); DataContext = vm; using var context = DbContexts.GetContext(); List testList = new() { - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")) { Result = ProcessBookResult.FailedAbort, Status = ProcessBookStatus.Failed, }, - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG")) { Result = ProcessBookResult.FailedSkip, Status = ProcessBookStatus.Failed, }, - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q")) { Result = ProcessBookResult.FailedRetry, Status = ProcessBookStatus.Failed, }, - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO")) { Result = ProcessBookResult.ValidationFail, Status = ProcessBookStatus.Failed, }, - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4")) { Result = ProcessBookResult.Cancelled, Status = ProcessBookStatus.Cancelled, }, - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0")) { Result = ProcessBookResult.Success, Status = ProcessBookStatus.Completed, }, - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")) { Result = ProcessBookResult.None, Status = ProcessBookStatus.Working, }, - new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger) + new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")) { Result = ProcessBookResult.None, Status = ProcessBookStatus.Queued, diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml index 90517420..739c15d7 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml @@ -59,7 +59,7 @@ Width="75"> - + - + - + @@ -93,7 +93,7 @@ - + @@ -103,7 +103,7 @@ - + @@ -113,7 +113,7 @@ - + @@ -123,7 +123,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -143,7 +143,7 @@ - + @@ -153,7 +153,7 @@ - + @@ -163,7 +163,7 @@ - + @@ -172,7 +172,7 @@ - + @@ -192,7 +192,7 @@ - + @@ -213,7 +213,7 @@ - + @@ -223,7 +223,7 @@ - +