diff --git a/ApplicationServices/LibraryCommands.cs b/ApplicationServices/LibraryCommands.cs index 96012f68..ed36595e 100644 --- a/ApplicationServices/LibraryCommands.cs +++ b/ApplicationServices/LibraryCommands.cs @@ -11,13 +11,6 @@ using Serilog; namespace ApplicationServices { - // subtly different from DataLayer.LiberatedStatus - // - DataLayer.LiberatedStatus: has no concept of partially downloaded - // - ApplicationServices.LiberatedState: has no concept of Error/skipped - public enum LiberatedState { NotDownloaded, PartialDownload, Liberated } - - public enum PdfState { NoPdf, Downloaded, NotDownloaded } - public static class LibraryCommands { private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS; @@ -162,94 +155,24 @@ namespace ApplicationServices #endregion #region Update book details - public static int UpdateUserDefinedItem(Book book, string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus) + + public static int UpdateUserDefinedItem(Book book) { try { using var context = DbContexts.GetContext(); - var udi = book.UserDefinedItem; - - var tagsChanged = udi.Tags != newTags; - var bookStatusChanged = udi.BookStatus != bookStatus; - var pdfStatusChanged = udi.PdfStatus != pdfStatus; - - if (!tagsChanged && !bookStatusChanged && !pdfStatusChanged) - return 0; - - udi.Tags = newTags; - udi.BookStatus = bookStatus; - udi.PdfStatus = pdfStatus; - // Attach() NoTracking entities before SaveChanges() - context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified; + context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified; var qtyChanges = context.SaveChanges(); - - if (qtyChanges == 0) - return 0; - - if (tagsChanged) - SearchEngineCommands.UpdateBookTags(book); - if (bookStatusChanged || pdfStatusChanged) + if (qtyChanges > 0) SearchEngineCommands.UpdateLiberatedStatus(book); return qtyChanges; } catch (Exception ex) { - Log.Logger.Error(ex, "Error updating tags"); - throw; - } - } - - public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus) - { - try - { - using var context = DbContexts.GetContext(); - - var udi = libraryBook.Book.UserDefinedItem; - - if (udi.BookStatus == liberatedStatus) - return 0; - - // Attach() NoTracking entities before SaveChanges() - udi.BookStatus = liberatedStatus; - context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - var qtyChanges = context.SaveChanges(); - if (qtyChanges > 0) - SearchEngineCommands.UpdateLiberatedStatus(libraryBook.Book); - - return qtyChanges; - } - catch (Exception ex) - { - Log.Logger.Error(ex, "Error updating tags"); - throw; - } - } - - public static int UpdatePdf(LibraryBook libraryBook, LiberatedStatus liberatedStatus) - { - try - { - using var context = DbContexts.GetContext(); - - var udi = libraryBook.Book.UserDefinedItem; - - if (udi.PdfStatus == liberatedStatus) - return 0; - - // Attach() NoTracking entities before SaveChanges() - udi.PdfStatus = liberatedStatus; - context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - var qtyChanges = context.SaveChanges(); - - return qtyChanges; - } - catch (Exception ex) - { - Log.Logger.Error(ex, "Error updating tags"); + Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}"); throw; } } @@ -257,15 +180,15 @@ namespace ApplicationServices // below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those... - public static LiberatedState Liberated_Status(Book book) - => book.Audio_Exists ? LiberatedState.Liberated - : FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedState.PartialDownload - : LiberatedState.NotDownloaded; + public static LiberatedStatus Liberated_Status(Book book) + => book.Audio_Exists ? LiberatedStatus.Liberated + : FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload + : LiberatedStatus.NotLiberated; - public static PdfState Pdf_Status(Book book) - => !book.Supplements.Any() ? PdfState.NoPdf - : book.PDF_Exists ? PdfState.Downloaded - : PdfState.NotDownloaded; + public static LiberatedStatus? Pdf_Status(Book book) + => !book.Supplements.Any() ? null + : book.PDF_Exists ? LiberatedStatus.Liberated + : LiberatedStatus.NotLiberated; public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { } public static LibraryStats GetCounts() @@ -276,9 +199,9 @@ namespace ApplicationServices .AsParallel() .Select(lb => Liberated_Status(lb.Book)) .ToList(); - var booksFullyBackedUp = results.Count(r => r == LiberatedState.Liberated); - var booksDownloadedOnly = results.Count(r => r == LiberatedState.PartialDownload); - var booksNoProgress = results.Count(r => r == LiberatedState.NotDownloaded); + var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated); + var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload); + var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated); Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress }); @@ -287,8 +210,8 @@ namespace ApplicationServices .Where(lb => lb.Book.Supplements.Any()) .Select(lb => Pdf_Status(lb.Book)) .ToList(); - var pdfsDownloaded = boolResults.Count(r => r == PdfState.Downloaded); - var pdfsNotDownloaded = boolResults.Count(r => r == PdfState.NotDownloaded); + var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated); + var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated); Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded }); diff --git a/DataLayer/EfClasses/UserDefinedItem.cs b/DataLayer/EfClasses/UserDefinedItem.cs index 4178a20e..5bd52beb 100644 --- a/DataLayer/EfClasses/UserDefinedItem.cs +++ b/DataLayer/EfClasses/UserDefinedItem.cs @@ -14,7 +14,11 @@ namespace DataLayer NotLiberated = 0, Liberated = 1, /// Error occurred during liberation. Don't retry - Error = 2 + Error = 2, + + /// Application-state only. Not a valid persistence state. + PartialDownload = 0x1000 + } public class UserDefinedItem @@ -38,7 +42,15 @@ namespace DataLayer public string Tags { get => _tags; - set => _tags = sanitize(value); + set + { + var newTags = sanitize(value); + if (_tags != newTags) + { + _tags = newTags; + ItemChanged?.Invoke(this, nameof(Tags)); + } + } } public IEnumerable TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries); @@ -95,10 +107,38 @@ namespace DataLayer #endregion #region LiberatedStatuses - public LiberatedStatus BookStatus { get; set; } - public LiberatedStatus? PdfStatus { get; set; } - #endregion + private LiberatedStatus _bookStatus; + private LiberatedStatus? _pdfStatus; + public LiberatedStatus BookStatus + { + get => _bookStatus; + set + { + if (_bookStatus != value) + { + _bookStatus = value; + ItemChanged?.Invoke(this, nameof(BookStatus)); + } + } + } + public LiberatedStatus? PdfStatus + { + get => _pdfStatus; + set + { + if (_pdfStatus != value) + { + _pdfStatus = value; + ItemChanged?.Invoke(this, nameof(PdfStatus)); + } + } + } + #endregion + /// + /// Occurs when , , or values change. + /// + public static event EventHandler ItemChanged; public override string ToString() => $"{Book} {Rating} {Tags}"; } } diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs index 7b9ec687..db7fc144 100644 --- a/FileLiberator/DownloadDecryptBook.cs +++ b/FileLiberator/DownloadDecryptBook.cs @@ -16,22 +16,22 @@ namespace FileLiberator public class DownloadDecryptBook : IAudioDecodable { - private AaxcDownloadConverter aaxcDownloader; + private AaxcDownloadConverter aaxcDownloader; - public event EventHandler StreamingTimeRemaining; - public event EventHandler> RequestCoverArt; - public event EventHandler TitleDiscovered; - public event EventHandler AuthorsDiscovered; - public event EventHandler NarratorsDiscovered; - public event EventHandler CoverImageDiscovered; - public event EventHandler StreamingBegin; - public event EventHandler StreamingProgressChanged; - public event EventHandler StreamingCompleted; - public event EventHandler Begin; - public event EventHandler StatusUpdate; - public event EventHandler Completed; + public event EventHandler StreamingTimeRemaining; + public event EventHandler> RequestCoverArt; + public event EventHandler TitleDiscovered; + public event EventHandler AuthorsDiscovered; + public event EventHandler NarratorsDiscovered; + public event EventHandler CoverImageDiscovered; + public event EventHandler StreamingBegin; + public event EventHandler StreamingProgressChanged; + public event EventHandler StreamingCompleted; + public event EventHandler Begin; + public event EventHandler StatusUpdate; + public event EventHandler Completed; - public async Task ProcessAsync(LibraryBook libraryBook) + public async Task ProcessAsync(LibraryBook libraryBook) { Begin?.Invoke(this, libraryBook); @@ -47,10 +47,12 @@ namespace FileLiberator return new StatusHandler { "Decrypt failed" }; // moves files and returns dest dir - _ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); + var moveResults = MoveFilesToBooksDir(libraryBook.Book, outputAudioFilename); - // only need to update if success. if failure, it will remain at 0 == NotLiberated - ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated); + if (!moveResults.movedAudioFile) + return new StatusHandler { "Cannot find final audio file after decryption" }; + + libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated; return new StatusHandler(); } @@ -123,7 +125,7 @@ namespace FileLiberator } - private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) + private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) { if (e is null && Configuration.Instance.AllowLibationFixup) { @@ -140,10 +142,10 @@ namespace FileLiberator { TitleDiscovered?.Invoke(this, e.TitleSansUnabridged); AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]"); - NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]"); + NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]"); } - private static string moveFilesToBooksDir(Book product, string outputAudioFilename) + private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename) { // create final directory. move each file into it. MOVE AUDIO FILE LAST // new dir: safetitle_limit50char + " [" + productId + "]" @@ -158,6 +160,7 @@ namespace FileLiberator // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId); + bool movedAudioFile = false; foreach (var f in sortedFiles) { var dest @@ -170,11 +173,14 @@ namespace FileLiberator Cue.UpdateFileName(f, audioFileName); File.Move(f.FullName, dest); + + movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f); + } AudibleFileStorage.Audio.Refresh(); - return destinationDir; + return (destinationDir, movedAudioFile); } private static List getProductFilesSorted(Book product, string outputAudioFilename) @@ -197,26 +203,26 @@ namespace FileLiberator } private static void validate(LibraryBook libraryBook) - { - string errorString(string field) - => $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book."; + { + string errorString(string field) + => $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book."; - string errorTitle() - { - var title - = (libraryBook.Book.Title.Length > 53) - ? $"{libraryBook.Book.Title.Truncate(50)}..." - : libraryBook.Book.Title; - var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]"; - return errorBookTitle; - }; + string errorTitle() + { + var title + = (libraryBook.Book.Title.Length > 53) + ? $"{libraryBook.Book.Title.Truncate(50)}..." + : libraryBook.Book.Title; + var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]"; + return errorBookTitle; + }; - if (string.IsNullOrWhiteSpace(libraryBook.Account)) - throw new Exception(errorString("Account")); + if (string.IsNullOrWhiteSpace(libraryBook.Account)) + throw new Exception(errorString("Account")); - if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale)) - throw new Exception(errorString("Locale")); - } + if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale)) + throw new Exception(errorString("Locale")); + } public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists; diff --git a/FileLiberator/DownloadPdf.cs b/FileLiberator/DownloadPdf.cs index 356cb13a..a9b8815c 100644 --- a/FileLiberator/DownloadPdf.cs +++ b/FileLiberator/DownloadPdf.cs @@ -23,8 +23,7 @@ namespace FileLiberator await downloadPdfAsync(libraryBook, proposedDownloadFilePath); var result = verifyDownload(libraryBook); - var liberatedStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated; - ApplicationServices.LibraryCommands.UpdatePdf(libraryBook, liberatedStatus); + libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated; return result; } diff --git a/FileLiberator/FileLiberator.csproj b/FileLiberator/FileLiberator.csproj index 08a7d736..776a70fa 100644 --- a/FileLiberator/FileLiberator.csproj +++ b/FileLiberator/FileLiberator.csproj @@ -7,7 +7,6 @@ - diff --git a/FileLiberator/IProcessableExt.cs b/FileLiberator/IProcessableExt.cs index 3a4339e3..0ba57965 100644 --- a/FileLiberator/IProcessableExt.cs +++ b/FileLiberator/IProcessableExt.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using ApplicationServices; using DataLayer; using Dinah.Core; using Dinah.Core.ErrorHandling; @@ -18,10 +17,8 @@ namespace FileLiberator // when used in foreach: stateful. deferred execution - public static IEnumerable GetValidLibraryBooks(this IProcessable processable) - => DbContexts.GetContext() - .GetLibrary_Flat_NoTracking() - .Where(libraryBook => processable.Validate(libraryBook)); + public static IEnumerable GetValidLibraryBooks(this IProcessable processable, IEnumerable library) + => library.Where(libraryBook => processable.Validate(libraryBook)); public static async Task ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate) { diff --git a/Libation.sln b/Libation.sln index 8d11ecc4..1dcd66ee 100644 --- a/Libation.sln +++ b/Libation.sln @@ -86,7 +86,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -222,7 +222,7 @@ Global {8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} {1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} {59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E} - {393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF} + {393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05} {06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249} {2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF} {9F1AA3DE-962F-469B-82B2-46F93491389B} = {F61184E7-2426-4A13-ACEF-5689928E2CE2} diff --git a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs index 034be1af..6d604184 100644 --- a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs +++ b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs @@ -50,24 +50,24 @@ namespace LibationWinForms.BookLiberation public static class ProcessorAutomationController { - public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler completedAction = null) + public static async Task BackupSingleBookAsync(LibraryBook libraryBook) { Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId }); var logMe = LogMe.RegisterForm(); - var backupBook = CreateBackupBook(completedAction, logMe); + var backupBook = CreateBackupBook(logMe); // continue even if libraryBook is null. we'll display even that in the processing box await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync(); } - public static async Task BackupAllBooksAsync(EventHandler completedAction = null) + public static async Task BackupAllBooksAsync() { Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync)); var automatedBackupsForm = new AutomatedBackupsForm(); var logMe = LogMe.RegisterForm(automatedBackupsForm); - var backupBook = CreateBackupBook(completedAction, logMe); + var backupBook = CreateBackupBook(logMe); await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync(); } @@ -84,19 +84,19 @@ namespace LibationWinForms.BookLiberation await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync(); } - public static async Task BackupAllPdfsAsync(EventHandler completedAction = null) + public static async Task BackupAllPdfsAsync() { Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync)); var automatedBackupsForm = new AutomatedBackupsForm(); var logMe = LogMe.RegisterForm(automatedBackupsForm); - var downloadPdf = CreateProcessable(logMe, completedAction); + var downloadPdf = CreateProcessable(logMe); await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync(); } - private static IProcessable CreateBackupBook(EventHandler completedAction, LogMe logMe) + private static IProcessable CreateBackupBook(LogMe logMe) { var downloadPdf = CreateProcessable(logMe); @@ -104,7 +104,6 @@ namespace LibationWinForms.BookLiberation async void onDownloadDecryptBookCompleted(object sender, LibraryBook e) { await downloadPdf.TryProcessAsync(e); - completedAction(sender, e); } var downloadDecryptBook = CreateProcessable(logMe, onDownloadDecryptBookCompleted); @@ -246,7 +245,7 @@ $@" Title: {libraryBook.Book.Title} if (dialogResult == SkipResult) { - ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error); + libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error; LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}"); } @@ -305,7 +304,7 @@ An error occurred while trying to process this book. protected override async Task RunAsync() { // support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here - foreach (var libraryBook in Processable.GetValidLibraryBooks()) + foreach (var libraryBook in Processable.GetValidLibraryBooks(ApplicationServices.DbContexts.GetContext().GetLibrary_Flat_NoTracking())) { var keepGoing = await ProcessOneAsync(libraryBook, validate: false); if (!keepGoing) diff --git a/LibationWinForms/Form1.cs b/LibationWinForms/Form1.cs index a114b094..134a2123 100644 --- a/LibationWinForms/Form1.cs +++ b/LibationWinForms/Form1.cs @@ -384,15 +384,14 @@ namespace LibationWinForms #region liberate menu private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow); + => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(); private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow); + => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(); private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e) => await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync(); - private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId); #endregion #region Export menu diff --git a/LibationWinForms/GridEntry.cs b/LibationWinForms/GridEntry.cs index 7a90c437..fb716e31 100644 --- a/LibationWinForms/GridEntry.cs +++ b/LibationWinForms/GridEntry.cs @@ -12,6 +12,9 @@ using Dinah.Core.Drawing; namespace LibationWinForms { + /// + /// The View Model for a LibraryBook + /// internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable { #region implementation properties @@ -26,12 +29,15 @@ namespace LibationWinForms private Book Book => LibraryBook.Book; private Image _cover; + private Action Refilter { get; } - public GridEntry(LibraryBook libraryBook) + public GridEntry(LibraryBook libraryBook, Action refilterOnChanged = null) { LibraryBook = libraryBook; + Refilter = refilterOnChanged; _memberValues = CreateMemberValueDictionary(); + //Get cover art. If it's default, subscribe to PictureCached { (bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80)); @@ -58,7 +64,7 @@ namespace LibationWinForms Description = GetDescriptionDisplay(Book); } - //DisplayTags and Liberate properties are live. + UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged; } private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e) @@ -70,8 +76,77 @@ namespace LibationWinForms } } - #region Data Source properties + #region detect changes to the model, update the view, and save to database. + /// + /// This event handler receives notifications from the model that it has changed. + /// Save to the database and notify the view that it's changed. + /// + private void UserDefinedItem_ItemChanged(object sender, string itemName) + { + var udi = sender as UserDefinedItem; + + if (udi.Book.AudibleProductId != Book.AudibleProductId) + return; + + switch (itemName) + { + case nameof(udi.Tags): + { + Book.UserDefinedItem.Tags = udi.Tags; + NotifyPropertyChanged(nameof(DisplayTags)); + } + break; + case nameof(udi.BookStatus): + { + Book.UserDefinedItem.BookStatus = udi.BookStatus; + NotifyPropertyChanged(nameof(Liberate)); + } + break; + case nameof(udi.PdfStatus): + { + Book.UserDefinedItem.PdfStatus = udi.PdfStatus; + NotifyPropertyChanged(nameof(Liberate)); + } + break; + } + + if (!suspendCommit) + Commit(); + } + private bool suspendCommit = false; + + /// + /// Begin editing the model, suspending commits until is called. + /// + public void BeginEdit() => suspendCommit = true; + + /// + /// Save all edits to the database. + /// + public void EndEdit() + { + Commit(); + suspendCommit = false; + } + + private void Commit() + { + //We don't want LiberatedStatus.PartialDownload to be a persistent status. + var bookStatus = Book.UserDefinedItem.BookStatus; + var saveStatus = bookStatus == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : bookStatus; + Book.UserDefinedItem.BookStatus = saveStatus; + + LibraryCommands.UpdateUserDefinedItem(Book); + + Book.UserDefinedItem.BookStatus = bookStatus; + + Refilter?.Invoke(); + } + + #endregion + + #region Model properties exposed to the view public Image Cover { get @@ -96,8 +171,22 @@ namespace LibationWinForms public string Category { get; } public string Misc { get; } public string Description { get; } - public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); - public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book)); + public string DisplayTags + { + get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); + set => Book.UserDefinedItem.Tags = value; + } + public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate + { + get => (LibraryCommands.Liberated_Status(LibraryBook.Book), LibraryCommands.Pdf_Status(LibraryBook.Book)); + + set + { + LibraryBook.Book.UserDefinedItem.BookStatus = value.BookStatus; + LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus; + } + } + #endregion #region Data Sorting @@ -121,7 +210,7 @@ namespace LibationWinForms { nameof(Category), () => Category }, { nameof(Misc), () => Misc }, { nameof(DisplayTags), () => DisplayTags }, - { nameof(Liberate), () => Liberate.Item1 } + { nameof(Liberate), () => Liberate.BookStatus } }; // Instantiate comparers for every exposed member object type. @@ -131,7 +220,7 @@ namespace LibationWinForms { typeof(int), new ObjectComparer() }, { typeof(float), new ObjectComparer() }, { typeof(DateTime), new ObjectComparer() }, - { typeof(LiberatedState), new ObjectComparer() }, + { typeof(LiberatedStatus), new ObjectComparer() }, }; public virtual object GetMemberValue(string memberName) => _memberValues[memberName](); @@ -200,5 +289,11 @@ namespace LibationWinForms } #endregion + + ~GridEntry() + { + UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged; + FileManager.PictureStorage.PictureCached -= PictureStorage_PictureCached; + } } } diff --git a/LibationWinForms/LibationWinForms.csproj b/LibationWinForms/LibationWinForms.csproj index 343425c6..7eee10cc 100644 --- a/LibationWinForms/LibationWinForms.csproj +++ b/LibationWinForms/LibationWinForms.csproj @@ -14,6 +14,7 @@ + diff --git a/LibationWinForms/LiberateDataGridViewImageButtonColumn.cs b/LibationWinForms/LiberateDataGridViewImageButtonColumn.cs index b8509e5d..f1769f36 100644 --- a/LibationWinForms/LiberateDataGridViewImageButtonColumn.cs +++ b/LibationWinForms/LiberateDataGridViewImageButtonColumn.cs @@ -3,6 +3,7 @@ using System; using System.Drawing; using System.Windows.Forms; using System.Linq; +using DataLayer; namespace LibationWinForms { @@ -20,9 +21,11 @@ namespace LibationWinForms { base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts); - if (value is (LiberatedState liberatedState, PdfState pdfState)) + if (value is (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null)) { - (string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(liberatedState, pdfState); + var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value; + + (string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState); DrawButtonImage(graphics, buttonImage, cellBounds); @@ -30,29 +33,31 @@ namespace LibationWinForms } } - private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus) + private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus) { (string libState, string image_lib) = liberatedStatus switch { - LiberatedState.Liberated => ("Liberated", "green"), - LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"), - LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"), + LiberatedStatus.Liberated => ("Liberated", "green"), + LiberatedStatus.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"), + LiberatedStatus.NotLiberated => ("Book NOT downloaded", "red"), + LiberatedStatus.Error => ("Book downloaded ERROR", "red"), _ => throw new Exception("Unexpected liberation state") }; (string pdfState, string image_pdf) = pdfStatus switch { - PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"), - PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"), - PdfState.NoPdf => ("", ""), + LiberatedStatus.Liberated => ("\r\nPDF downloaded", "_pdf_yes"), + LiberatedStatus.NotLiberated => ("\r\nPDF NOT downloaded", "_pdf_no"), + LiberatedStatus.Error => ("\r\nPDF downloaded ERROR", "_pdf_no"), + null => ("", ""), _ => throw new Exception("Unexpected PDF state") }; var mouseoverText = libState + pdfState; - if (liberatedStatus == LiberatedState.NotDownloaded || - liberatedStatus == LiberatedState.PartialDownload || - pdfStatus == PdfState.NotDownloaded) + if (liberatedStatus == LiberatedStatus.NotLiberated || + liberatedStatus == LiberatedStatus.PartialDownload || + pdfStatus == LiberatedStatus.NotLiberated) mouseoverText += "\r\nClick to complete"; var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}"); diff --git a/LibationWinForms/ProductsGrid.cs b/LibationWinForms/ProductsGrid.cs index 46b721a4..b6c6b5b0 100644 --- a/LibationWinForms/ProductsGrid.cs +++ b/LibationWinForms/ProductsGrid.cs @@ -86,7 +86,7 @@ namespace LibationWinForms } // else: liberate - await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId)); + await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook); } private void Details_Click(GridEntry liveGridEntry) @@ -95,15 +95,14 @@ namespace LibationWinForms if (bookDetailsForm.ShowDialog() != DialogResult.OK) return; - var qtyChanges = LibraryCommands.UpdateUserDefinedItem(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus); - if (qtyChanges == 0) - return; + liveGridEntry.BeginEdit(); - //Re-apply filters - Filter(); + liveGridEntry.DisplayTags = bookDetailsForm.NewTags; + liveGridEntry.Liberate = (bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus); - //Update whole GridEntry row - liveGridEntry.NotifyPropertyChanged(); + liveGridEntry.EndEdit(); + + BackupCountsChanged?.Invoke(this, EventArgs.Empty); } #endregion @@ -132,7 +131,7 @@ namespace LibationWinForms } var orderedGridEntries = lib - .Select(lb => new GridEntry(lb)).ToList() + .Select(lb => new GridEntry(lb, Filter)).ToList() // default load order .OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate))) //// more advanced example: sort by author, then series, then title @@ -150,19 +149,6 @@ namespace LibationWinForms BackupCountsChanged?.Invoke(this, EventArgs.Empty); } - public void RefreshRow(string productId) - { - var liveGridEntry = getGridEntry((ge) => ge.AudibleProductId == productId); - - // update GridEntry Liberate cell - liveGridEntry?.NotifyPropertyChanged(nameof(liveGridEntry.Liberate)); - - // needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change - Filter(); - - BackupCountsChanged?.Invoke(this, EventArgs.Empty); - } - #endregion #region Filter @@ -195,12 +181,7 @@ namespace LibationWinForms #endregion #region DataGridView Macro - - private GridEntry getGridEntry(Func predicate) - => ((SortableBindingList)gridEntryBindingSource.DataSource).FirstOrDefault(predicate); - private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem(rowIndex); - #endregion } }