diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs index 89d787f4..f9490ecc 100644 --- a/AaxDecrypter/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -1,32 +1,27 @@ using AAXClean; using Dinah.Core; -using Dinah.Core.Diagnostics; using Dinah.Core.IO; -using Dinah.Core.Logging; +using Dinah.Core.Net.Http; using Dinah.Core.StepRunner; using System; using System.IO; namespace AaxDecrypter { - public enum OutputFormat - { - Mp4a, - Mp3 - } + public enum OutputFormat { Mp4a, Mp3 } public class AaxcDownloadConverter { public event EventHandler RetrievedTags; public event EventHandler RetrievedCoverArt; - public event EventHandler DecryptProgressUpdate; + public event EventHandler DecryptProgressUpdate; public event EventHandler DecryptTimeRemaining; + public string AppName { get; set; } = nameof(AaxcDownloadConverter); private string outputFileName { get; } private string cacheDir { get; } private DownloadLicense downloadLicense { get; } private AaxFile aaxFile; - private byte[] coverArt; private OutputFormat OutputFormat; private StepSequence steps { get; } @@ -65,11 +60,15 @@ namespace AaxDecrypter }; } + /// + /// Setting cover art by this method will insert the art into the audiobook metadata + /// public void SetCoverArt(byte[] coverArt) { if (coverArt is null) return; - this.coverArt = coverArt; + aaxFile?.AppleTags.SetCoverArt(coverArt); + RetrievedCoverArt?.Invoke(this, coverArt); } @@ -98,7 +97,7 @@ namespace AaxDecrypter try { nfsPersister = new NetworkFileStreamPersister(jsonDownloadState); - //If More thaan ~1 hour has elapsed since getting the download url, it will expire. + //If More than ~1 hour has elapsed since getting the download url, it will expire. //The new url will be to the same file. nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl)); } @@ -113,13 +112,11 @@ namespace AaxDecrypter { nfsPersister = NewNetworkFilePersister(); } - nfsPersister.NetworkFileStream.BeginDownloading(); aaxFile = new AaxFile(nfsPersister.NetworkFileStream); - coverArt = aaxFile.AppleTags.Cover; RetrievedTags?.Invoke(this, aaxFile.AppleTags); - RetrievedCoverArt?.Invoke(this, coverArt); + RetrievedCoverArt?.Invoke(this, aaxFile.AppleTags.Cover); return !isCanceled; } @@ -136,8 +133,14 @@ namespace AaxDecrypter public bool Step2_DownloadAndCombine() { + var zeroProgress = new DownloadProgress + { + BytesReceived = 0, + ProgressPercentage = 0, + TotalBytesToReceive = nfsPersister.NetworkFileStream.Length + }; - DecryptProgressUpdate?.Invoke(this, 0); + DecryptProgressUpdate?.Invoke(this, zeroProgress); if (File.Exists(outputFileName)) FileExt.SafeDelete(outputFileName); @@ -147,7 +150,6 @@ namespace AaxDecrypter aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV); aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; - var decryptionResult = OutputFormat == OutputFormat.Mp4a ? aaxFile.ConvertToMp4a(outFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outFile); aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; @@ -155,21 +157,9 @@ namespace AaxDecrypter downloadLicense.ChapterInfo = aaxFile.Chapters; - if (decryptionResult == ConversionResult.NoErrorsDetected - && coverArt is not null - && OutputFormat == OutputFormat.Mp4a) - { - //This handles a special case where the aaxc file doesn't contain cover art and - //Libation downloaded it instead (Animal Farm). Currently only works for Mp4a files. - using var decryptedBook = new Mp4File(outputFileName, FileAccess.ReadWrite); - decryptedBook.AppleTags?.SetCoverArt(coverArt); - decryptedBook.Save(); - decryptedBook.Close(); - } - nfsPersister.Dispose(); - DecryptProgressUpdate?.Invoke(this, 0); + DecryptProgressUpdate?.Invoke(this, zeroProgress); return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled; } @@ -185,7 +175,13 @@ namespace AaxDecrypter double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds; - DecryptProgressUpdate?.Invoke(this, (int)progressPercent); + DecryptProgressUpdate?.Invoke(this, + new DownloadProgress + { + ProgressPercentage = progressPercent, + BytesReceived = (long)(nfsPersister.NetworkFileStream.Length * progressPercent), + TotalBytesToReceive = nfsPersister.NetworkFileStream.Length + }); } public bool Step3_CreateCue() @@ -227,6 +223,7 @@ namespace AaxDecrypter { isCanceled = true; aaxFile?.Cancel(); + aaxFile?.Dispose(); } } } diff --git a/AaxDecrypter/NetworkFileStream.cs b/AaxDecrypter/NetworkFileStream.cs index d3849992..5d7544b5 100644 --- a/AaxDecrypter/NetworkFileStream.cs +++ b/AaxDecrypter/NetworkFileStream.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Net; using System.Threading; -using System.Threading.Tasks; namespace AaxDecrypter { @@ -27,7 +26,7 @@ namespace AaxDecrypter public CookieCollection GetCookies() { - return base.GetCookies(Uri); + return GetCookies(Uri); } } @@ -79,15 +78,14 @@ namespace AaxDecrypter #endregion #region Private Properties - private HttpWebRequest HttpRequest { get; set; } private FileStream _writeFile { get; } private FileStream _readFile { get; } private Stream _networkStream { get; set; } private bool hasBegunDownloading { get; set; } private bool isCancelled { get; set; } - private bool finishedDownloading { get; set; } - private Action downloadThreadCompleteCallback { get; set; } + private EventWaitHandle downloadEnded { get; set; } + private EventWaitHandle downloadedPiece { get; set; } #endregion @@ -147,7 +145,7 @@ namespace AaxDecrypter private void Update() { RequestHeaders = HttpRequest.Headers; - Updated?.Invoke(this, new EventArgs()); + Updated?.Invoke(this, EventArgs.Empty); } /// @@ -160,8 +158,8 @@ namespace AaxDecrypter if (uriToSameFile.Host != Uri.Host) throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}"); - if (hasBegunDownloading && !finishedDownloading) - throw new Exception("Cannot change Uri during a download operation."); + if (hasBegunDownloading) + throw new InvalidOperationException("Cannot change Uri after download has started."); Uri = uriToSameFile; HttpRequest = WebRequest.CreateHttp(Uri); @@ -176,25 +174,27 @@ namespace AaxDecrypter /// /// Begins downloading to in a background thread. /// - public void BeginDownloading() + private void BeginDownloading() { + downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset); + if (ContentLength != 0 && WritePosition == ContentLength) { hasBegunDownloading = true; - finishedDownloading = true; + downloadEnded.Set(); return; } if (ContentLength != 0 && WritePosition > ContentLength) - throw new Exception($"Specified write position (0x{WritePosition:X10}) is larger than the file size."); + throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10})."); var response = HttpRequest.GetResponse() as HttpWebResponse; if (response.StatusCode != HttpStatusCode.PartialContent) - throw new Exception($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); + throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); if (response.Headers.GetValues("Accept-Ranges").FirstOrDefault(r => r.EqualsInsensitive("bytes")) is null) - throw new Exception($"Server at {Uri.Host} does not support Http ranges"); + throw new WebException($"Server at {Uri.Host} does not support Http ranges"); //Content length is the length of the range request, and it is only equal //to the complete file length if requesting Range: bytes=0- @@ -202,10 +202,12 @@ namespace AaxDecrypter ContentLength = response.ContentLength; _networkStream = response.GetResponseStream(); + downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); //Download the file in the background. - Thread downloadThread = new Thread(() => DownloadFile()) { IsBackground = true }; - downloadThread.Start(); + new Thread(() => DownloadFile()) + { IsBackground = true } + .Start(); hasBegunDownloading = true; return; @@ -216,13 +218,13 @@ namespace AaxDecrypter /// private void DownloadFile() { - long downloadPosition = WritePosition; - long nextFlush = downloadPosition + DATA_FLUSH_SZ; + var downloadPosition = WritePosition; + var nextFlush = downloadPosition + DATA_FLUSH_SZ; - byte[] buff = new byte[DOWNLOAD_BUFF_SZ]; + var buff = new byte[DOWNLOAD_BUFF_SZ]; do { - int bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); + var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); _writeFile.Write(buff, 0, bytesRead); downloadPosition += bytesRead; @@ -233,6 +235,7 @@ namespace AaxDecrypter WritePosition = downloadPosition; Update(); nextFlush = downloadPosition + DATA_FLUSH_SZ; + downloadedPiece.Set(); } } while (downloadPosition < ContentLength && !isCancelled); @@ -243,13 +246,12 @@ namespace AaxDecrypter _networkStream.Close(); if (!isCancelled && WritePosition < ContentLength) - throw new Exception("File download ended before finishing."); + throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); if (WritePosition > ContentLength) - throw new Exception("Downloaded file is larger than expected."); + throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10})."); - finishedDownloading = true; - downloadThreadCompleteCallback?.Invoke(); + downloadEnded.Set(); } #endregion @@ -330,9 +332,7 @@ namespace AaxDecrypter var result = new WebHeaderCollection(); foreach (var kvp in jObj) - { result.Add(kvp.Key, kvp.Value.Value()); - } return result; } @@ -341,8 +341,8 @@ namespace AaxDecrypter public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - JObject jObj = new JObject(); - Type type = value.GetType(); + var jObj = new JObject(); + var type = value.GetType(); var headers = value as WebHeaderCollection; var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k])); jObj.Add(jHeaders); @@ -364,13 +364,21 @@ namespace AaxDecrypter public override bool CanWrite => false; [JsonIgnore] - public override long Length => ContentLength; + public override long Length + { + get + { + if (!hasBegunDownloading) + BeginDownloading(); + return ContentLength; + } + } [JsonIgnore] public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); } [JsonIgnore] - public override bool CanTimeout => base.CanTimeout; + public override bool CanTimeout => false; [JsonIgnore] public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; } @@ -387,63 +395,39 @@ namespace AaxDecrypter if (!hasBegunDownloading) BeginDownloading(); - long toRead = Math.Min(count, Length - Position); - long requiredPosition = Position + toRead; - - //read operation will block until file contains enough data - //to fulfil the request, or until cancelled. - while (requiredPosition > WritePosition && !isCancelled) - Thread.Sleep(2); - + var toRead = Math.Min(count, Length - Position); + WaitToPosition(Position + toRead); return _readFile.Read(buffer, offset, count); } public override long Seek(long offset, SeekOrigin origin) { - long newPosition; + var newPosition = origin switch + { + SeekOrigin.Current => Position + offset, + SeekOrigin.End => ContentLength + offset, + _ => offset, + }; - switch (origin) - { - case SeekOrigin.Current: - newPosition = Position + offset; - break; - case SeekOrigin.End: - newPosition = ContentLength + offset; - break; - default: - newPosition = offset; - break; - } - ReadToPosition(newPosition); - - _readFile.Position = newPosition; - return newPosition; + WaitToPosition(newPosition); + return _readFile.Position = newPosition; } /// - /// Ensures that the file has downloaded to at least , then returns. + /// Blocks until the file has downloaded to at least , then returns. /// - /// The minimum required data length in . - private void ReadToPosition(long neededPosition) - { - byte[] buff = new byte[DOWNLOAD_BUFF_SZ]; - do - { - Read(buff, 0, DOWNLOAD_BUFF_SZ); - } while (neededPosition > WritePosition); + /// The minimum required flished data length in . + private void WaitToPosition(long requiredPosition) + { + while (requiredPosition > WritePosition && !isCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ; } + public override void Close() { isCancelled = true; - downloadThreadCompleteCallback = CloseAction; - //ensure that close will run even if called after callback was fired. - if (finishedDownloading) - CloseAction(); + while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ; - } - private void CloseAction() - { _readFile.Close(); _writeFile.Close(); _networkStream?.Close(); @@ -451,5 +435,10 @@ namespace AaxDecrypter } #endregion + ~NetworkFileStream() + { + downloadEnded?.Close(); + downloadedPiece?.Close(); + } } } diff --git a/ApplicationServices/LibraryCommands.cs b/ApplicationServices/LibraryCommands.cs index 97bdae3e..c8f1a05f 100644 --- a/ApplicationServices/LibraryCommands.cs +++ b/ApplicationServices/LibraryCommands.cs @@ -20,6 +20,57 @@ namespace ApplicationServices public static class LibraryCommands { + private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS; + + public static async Task> FindInactiveBooks(Func loginCallbackFactoryFunc, List existingLibrary, params Account[] accounts) + { + //These are the minimum response groups required for the + //library scanner to pass all validation and filtering. + LibraryResponseGroups = + LibraryOptions.ResponseGroupOptions.ProductAttrs | + LibraryOptions.ResponseGroupOptions.ProductDesc | + LibraryOptions.ResponseGroupOptions.Relationships; + + if (accounts is null || accounts.Length == 0) + return new List(); + + try + { + var libraryItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts); + Log.Logger.Information($"GetAllLibraryItems: Total count {libraryItems.Count}"); + + var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList(); + + return missingBookList; + } + catch (AudibleApi.Authentication.LoginFailedException lfEx) + { + lfEx.SaveFiles(FileManager.Configuration.Instance.LibationFiles); + + // nuget Serilog.Exceptions would automatically log custom properties + // However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement: + // https://github.com/RehanSaeed/Serilog.Exceptions + // work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc + Log.Logger.Error(lfEx, "Error scanning library. Login failed. {@DebugInfo}", new + { + lfEx.RequestUrl, + ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode, + ResponseStatusCodeDesc = lfEx.ResponseStatusCode, + lfEx.ResponseInputFields, + lfEx.ResponseBodyFilePaths + }); + throw; + } + catch (Exception ex) + { + Log.Logger.Error(ex, "Error importing library"); + throw; + } + finally + { + LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS; + } + } #region FULL LIBRARY scan and import public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func loginCallbackFactoryFunc, params Account[] accounts) { @@ -95,7 +146,7 @@ namespace ApplicationServices Account = account?.MaskedLogEntry ?? "[null]" }); - var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api); + var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api, LibraryResponseGroups); return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList(); } diff --git a/FileLiberator/BackupBook.cs b/FileLiberator/BackupBook.cs deleted file mode 100644 index 718e0e7f..00000000 --- a/FileLiberator/BackupBook.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Threading.Tasks; -using DataLayer; -using Dinah.Core.ErrorHandling; -using FileManager; - -namespace FileLiberator -{ - /// - /// Download DRM book and decrypt audiobook files - /// - /// Processes: - /// Download: download aax file: the DRM encrypted audiobook - /// Decrypt: remove DRM encryption from audiobook. Store final book - /// Backup: perform all steps (downloaded, decrypt) still needed to get final book - /// - public class BackupBook : IProcessable - { - public event EventHandler Begin; - public event EventHandler StatusUpdate; - public event EventHandler Completed; - - public DownloadDecryptBook DownloadDecryptBook { get; } = new DownloadDecryptBook(); - public DownloadPdf DownloadPdf { get; } = new DownloadPdf(); - - public bool Validate(LibraryBook libraryBook) - => !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book); - - // do NOT use ConfigureAwait(false) on ProcessAsync() - // often calls events which prints to forms in the UI context - public async Task ProcessAsync(LibraryBook libraryBook) - { - Begin?.Invoke(this, libraryBook); - - try - { - { - var statusHandler = await DownloadDecryptBook.TryProcessAsync(libraryBook); - if (statusHandler.HasErrors) - return statusHandler; - } - - { - var statusHandler = await DownloadPdf.TryProcessAsync(libraryBook); - if (statusHandler.HasErrors) - return statusHandler; - } - - return new StatusHandler(); - } - finally - { - Completed?.Invoke(this, libraryBook); - } - } - } -} diff --git a/FileLiberator/ConvertToMp3.cs b/FileLiberator/ConvertToMp3.cs index 92eddcfa..6c682b20 100644 --- a/FileLiberator/ConvertToMp3.cs +++ b/FileLiberator/ConvertToMp3.cs @@ -3,6 +3,7 @@ using DataLayer; using Dinah.Core; using Dinah.Core.ErrorHandling; using Dinah.Core.IO; +using Dinah.Core.Net.Http; using FileManager; using System; using System.IO; @@ -11,25 +12,26 @@ using System.Threading.Tasks; namespace FileLiberator { - public class ConvertToMp3 : IDecryptable + public class ConvertToMp3 : IAudioDecodable { - public event EventHandler DecryptBegin; - public event EventHandler TitleDiscovered; - public event EventHandler AuthorsDiscovered; - public event EventHandler NarratorsDiscovered; - public event EventHandler CoverImageFilepathDiscovered; - public event EventHandler UpdateProgress; - public event EventHandler UpdateRemainingTime; - public event EventHandler DecryptCompleted; - public event EventHandler Begin; - public event EventHandler Completed; - - public event EventHandler StatusUpdate; - public event EventHandler> RequestCoverArt; private Mp4File m4bBook; - private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3"); + 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; + + private long fileSize; + private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3"); public void Cancel() => m4bBook?.Cancel(); @@ -43,19 +45,20 @@ namespace FileLiberator { Begin?.Invoke(this, libraryBook); - DecryptBegin?.Invoke(this, $"Begin converting {libraryBook} to mp3"); + StreamingBegin?.Invoke(this, $"Begin converting {libraryBook} to mp3"); try { var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book); - m4bBook = new Mp4File(m4bPath, FileAccess.Read); m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate; + fileSize = m4bBook.InputStream.Length; + TitleDiscovered?.Invoke(this, m4bBook.AppleTags.Title); AuthorsDiscovered?.Invoke(this, m4bBook.AppleTags.FirstAuthor); NarratorsDiscovered?.Invoke(this, m4bBook.AppleTags.Narrator); - CoverImageFilepathDiscovered?.Invoke(this, m4bBook.AppleTags.Cover); + CoverImageDiscovered?.Invoke(this, m4bBook.AppleTags.Cover); using var mp3File = File.OpenWrite(Path.GetTempFileName()); @@ -76,7 +79,7 @@ namespace FileLiberator } finally { - DecryptCompleted?.Invoke(this, $"Completed converting to mp3: {libraryBook.Book.Title}"); + StreamingCompleted?.Invoke(this, $"Completed converting to mp3: {libraryBook.Book.Title}"); Completed?.Invoke(this, libraryBook); } } @@ -88,11 +91,17 @@ namespace FileLiberator double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed; if (double.IsNormal(estTimeRemaining)) - UpdateRemainingTime?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining)); + StreamingTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining)); double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds; - UpdateProgress?.Invoke(this, (int)progressPercent); + StreamingProgressChanged?.Invoke(this, + new DownloadProgress + { + ProgressPercentage = progressPercent, + BytesReceived = (long)(fileSize * progressPercent), + TotalBytesToReceive = fileSize + }); } } } diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs index 1bfea2a6..d8596ae3 100644 --- a/FileLiberator/DownloadDecryptBook.cs +++ b/FileLiberator/DownloadDecryptBook.cs @@ -8,27 +8,30 @@ using AudibleApi; using DataLayer; using Dinah.Core; using Dinah.Core.ErrorHandling; +using Dinah.Core.Net.Http; using FileManager; namespace FileLiberator { - public class DownloadDecryptBook : IDecryptable + public class DownloadDecryptBook : IAudioDecodable { - public event EventHandler> RequestCoverArt; - public event EventHandler Begin; - public event EventHandler DecryptBegin; - public event EventHandler TitleDiscovered; - public event EventHandler AuthorsDiscovered; - public event EventHandler NarratorsDiscovered; - public event EventHandler CoverImageFilepathDiscovered; - public event EventHandler UpdateProgress; - public event EventHandler UpdateRemainingTime; - public event EventHandler DecryptCompleted; - public event EventHandler Completed; - public event EventHandler StatusUpdate; - private AaxcDownloadConverter aaxcDownloader; - public async Task ProcessAsync(LibraryBook libraryBook) + 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 async Task ProcessAsync(LibraryBook libraryBook) { Begin?.Invoke(this, libraryBook); @@ -63,7 +66,7 @@ namespace FileLiberator private async Task aaxToM4bConverterDecryptAsync(string cacheDir, string destinationDir, LibraryBook libraryBook) { - DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}"); + StreamingBegin?.Invoke(this, $"Begin decrypting {libraryBook}"); try { @@ -78,7 +81,7 @@ namespace FileLiberator contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl, contentLic?.Voucher?.Key, contentLic?.Voucher?.Iv, - "Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0" + Resources.UserAgent ); if (Configuration.Instance.AllowLibationFixup) @@ -103,8 +106,8 @@ namespace FileLiberator aaxcDownloader = new AaxcDownloadConverter(outFileName, cacheDir, aaxcDecryptDlLic, format) { AppName = "Libation" }; - aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress); - aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining); + aaxcDownloader.DecryptProgressUpdate += (s, progress) => StreamingProgressChanged?.Invoke(this, progress); + aaxcDownloader.DecryptTimeRemaining += (s, remaining) => StreamingTimeRemaining?.Invoke(this, remaining); aaxcDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; aaxcDownloader.RetrievedTags += aaxcDownloader_RetrievedTags; @@ -119,11 +122,12 @@ namespace FileLiberator } finally { - DecryptCompleted?.Invoke(this, $"Completed downloading and decrypting {libraryBook.Book.Title}"); + StreamingCompleted?.Invoke(this, $"Completed downloading and decrypting {libraryBook.Book.Title}"); } } - private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) + + private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) { if (e is null && Configuration.Instance.AllowLibationFixup) { @@ -132,7 +136,7 @@ namespace FileLiberator if (e is not null) { - CoverImageFilepathDiscovered?.Invoke(this, e); + CoverImageDiscovered?.Invoke(this, e); } } diff --git a/FileLiberator/DownloadFile.cs b/FileLiberator/DownloadFile.cs index 13f38479..9f23b1e0 100644 --- a/FileLiberator/DownloadFile.cs +++ b/FileLiberator/DownloadFile.cs @@ -6,20 +6,21 @@ using Dinah.Core.Net.Http; namespace FileLiberator { // currently only used to download the .zip flies for upgrade - public class DownloadFile : IDownloadable + public class DownloadFile : IStreamable { - public event EventHandler DownloadBegin; - public event EventHandler DownloadProgressChanged; - public event EventHandler DownloadCompleted; + public event EventHandler StreamingBegin; + public event EventHandler StreamingProgressChanged; + public event EventHandler StreamingCompleted; + public event EventHandler StreamingTimeRemaining; public async Task PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath) { var client = new HttpClient(); var progress = new Progress(); - progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e); + progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e); - DownloadBegin?.Invoke(this, proposedDownloadFilePath); + StreamingBegin?.Invoke(this, proposedDownloadFilePath); try { @@ -28,7 +29,7 @@ namespace FileLiberator } finally { - DownloadCompleted?.Invoke(this, proposedDownloadFilePath); + StreamingCompleted?.Invoke(this, proposedDownloadFilePath); } } } diff --git a/FileLiberator/DownloadableBase.cs b/FileLiberator/DownloadableBase.cs index da78a8a6..38b5a76e 100644 --- a/FileLiberator/DownloadableBase.cs +++ b/FileLiberator/DownloadableBase.cs @@ -6,16 +6,18 @@ using Dinah.Core.Net.Http; namespace FileLiberator { - public abstract class DownloadableBase : IDownloadableProcessable + public abstract class DownloadableBase : IProcessable { public event EventHandler Begin; public event EventHandler Completed; - public event EventHandler DownloadBegin; - public event EventHandler DownloadProgressChanged; - public event EventHandler DownloadCompleted; + public event EventHandler StreamingBegin; + public event EventHandler StreamingProgressChanged; + public event EventHandler StreamingCompleted; public event EventHandler StatusUpdate; + public event EventHandler StreamingTimeRemaining; + protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message); public abstract bool Validate(LibraryBook libraryBook); @@ -44,9 +46,9 @@ namespace FileLiberator protected async Task PerformDownloadAsync(string proposedDownloadFilePath, Func, Task> func) { var progress = new Progress(); - progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e); + progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e); - DownloadBegin?.Invoke(this, proposedDownloadFilePath); + StreamingBegin?.Invoke(this, proposedDownloadFilePath); try { @@ -57,7 +59,7 @@ namespace FileLiberator } finally { - DownloadCompleted?.Invoke(this, proposedDownloadFilePath); + StreamingCompleted?.Invoke(this, proposedDownloadFilePath); } } } diff --git a/FileLiberator/IAudioDecodable.cs b/FileLiberator/IAudioDecodable.cs new file mode 100644 index 00000000..128242ef --- /dev/null +++ b/FileLiberator/IAudioDecodable.cs @@ -0,0 +1,14 @@ +using System; + +namespace FileLiberator +{ + public interface IAudioDecodable : IProcessable + { + event EventHandler> RequestCoverArt; + event EventHandler TitleDiscovered; + event EventHandler AuthorsDiscovered; + event EventHandler NarratorsDiscovered; + event EventHandler CoverImageDiscovered; + void Cancel(); + } +} diff --git a/FileLiberator/IDecryptable.cs b/FileLiberator/IDecryptable.cs deleted file mode 100644 index 568c3624..00000000 --- a/FileLiberator/IDecryptable.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace FileLiberator -{ - public interface IDecryptable : IProcessable - { - event EventHandler DecryptBegin; - - event EventHandler> RequestCoverArt; - event EventHandler TitleDiscovered; - event EventHandler AuthorsDiscovered; - event EventHandler NarratorsDiscovered; - event EventHandler CoverImageFilepathDiscovered; - event EventHandler UpdateProgress; - event EventHandler UpdateRemainingTime; - - event EventHandler DecryptCompleted; - void Cancel(); - } -} diff --git a/FileLiberator/IDownloadable.cs b/FileLiberator/IDownloadable.cs deleted file mode 100644 index 782d96ee..00000000 --- a/FileLiberator/IDownloadable.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Dinah.Core.Net.Http; - -namespace FileLiberator -{ - public interface IDownloadable - { - event EventHandler DownloadBegin; - event EventHandler DownloadProgressChanged; - event EventHandler DownloadCompleted; - } -} diff --git a/FileLiberator/IDownloadableProcessable.cs b/FileLiberator/IDownloadableProcessable.cs deleted file mode 100644 index 6c8b41de..00000000 --- a/FileLiberator/IDownloadableProcessable.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace FileLiberator -{ - public interface IDownloadableProcessable : IDownloadable, IProcessable { } -} diff --git a/FileLiberator/IProcessable.cs b/FileLiberator/IProcessable.cs index 9814f21d..6def9301 100644 --- a/FileLiberator/IProcessable.cs +++ b/FileLiberator/IProcessable.cs @@ -5,7 +5,7 @@ using Dinah.Core.ErrorHandling; namespace FileLiberator { - public interface IProcessable + public interface IProcessable : IStreamable { event EventHandler Begin; diff --git a/FileLiberator/IProcessableExt.cs b/FileLiberator/IProcessableExt.cs index eff721d2..3a4339e3 100644 --- a/FileLiberator/IProcessableExt.cs +++ b/FileLiberator/IProcessableExt.cs @@ -23,17 +23,12 @@ namespace FileLiberator .GetLibrary_Flat_NoTracking() .Where(libraryBook => processable.Validate(libraryBook)); - public static async Task ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook) + public static async Task ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate) { - if (!processable.Validate(libraryBook)) + if (validate && !processable.Validate(libraryBook)) return new StatusHandler { "Validation failed" }; - return await processable.ProcessBookAsync_NoValidation(libraryBook); - } - - public static async Task ProcessBookAsync_NoValidation(this IProcessable processable, LibraryBook libraryBook) - { - Serilog.Log.Logger.Information("Begin " + nameof(ProcessBookAsync_NoValidation) + " {@DebugInfo}", new + Serilog.Log.Logger.Information("Begin " + nameof(ProcessSingleAsync) + " {@DebugInfo}", new { libraryBook.Book.Title, libraryBook.Book.AudibleProductId, diff --git a/FileLiberator/IStreamable.cs b/FileLiberator/IStreamable.cs new file mode 100644 index 00000000..94049a7a --- /dev/null +++ b/FileLiberator/IStreamable.cs @@ -0,0 +1,13 @@ +using System; +using Dinah.Core.Net.Http; + +namespace FileLiberator +{ + public interface IStreamable + { + event EventHandler StreamingBegin; + event EventHandler StreamingProgressChanged; + event EventHandler StreamingTimeRemaining; + event EventHandler StreamingCompleted; + } +} diff --git a/FileManager/AudibleFileStorage.cs b/FileManager/AudibleFileStorage.cs index b2ee12a5..6af95a17 100644 --- a/FileManager/AudibleFileStorage.cs +++ b/FileManager/AudibleFileStorage.cs @@ -34,7 +34,7 @@ namespace FileManager } } - private static BackgroundFileSystem BookDirectoryFiles { get; } = new BackgroundFileSystem(); + private static BackgroundFileSystem BookDirectoryFiles { get; set; } #endregion #region instance @@ -47,6 +47,7 @@ namespace FileManager { extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList(); extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}"); + BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); } public void Refresh() @@ -76,7 +77,15 @@ namespace FileManager { //If user changed the BooksDirectory, reinitialize. if (storageDir != BookDirectoryFiles.RootDirectory) - BookDirectoryFiles.Init(storageDir, "*.*", SearchOption.AllDirectories); + { + lock (BookDirectoryFiles) + { + if (storageDir != BookDirectoryFiles.RootDirectory) + { + BookDirectoryFiles = new BackgroundFileSystem(storageDir, "*.*", SearchOption.AllDirectories); + } + } + } firstOrNull = BookDirectoryFiles.FindFile(regexPattern, RegexOptions.IgnoreCase); } diff --git a/FileManager/BackgroundFileSystem.cs b/FileManager/BackgroundFileSystem.cs index a239581c..1ed670ac 100644 --- a/FileManager/BackgroundFileSystem.cs +++ b/FileManager/BackgroundFileSystem.cs @@ -18,12 +18,19 @@ namespace FileManager private FileSystemWatcher fileSystemWatcher { get; set; } private BlockingCollection directoryChangesEvents { get; set; } private Task backgroundScanner { get; set; } - private List fsCache { get; set; } + private List fsCache { get; } = new(); + + public BackgroundFileSystem(string rootDirectory, string searchPattern, SearchOption searchOptions) + { + RootDirectory = rootDirectory; + SearchPattern = searchPattern; + SearchOption = searchOptions; + + Init(); + } public string FindFile(string regexPattern, RegexOptions options) { - if (fsCache is null) return null; - lock (fsCache) { return fsCache.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, options)); @@ -32,8 +39,6 @@ namespace FileManager public void RefreshFiles() { - if (fsCache is null) return; - lock (fsCache) { fsCache.Clear(); @@ -41,19 +46,12 @@ namespace FileManager } } - public void Init(string rootDirectory, string searchPattern, SearchOption searchOptions) + private void Init() { - RootDirectory = rootDirectory; - SearchPattern = searchPattern; - SearchOption = searchOptions; + Stop(); - //Calling CompleteAdding() will cause background scanner to terminate. - directoryChangesEvents?.CompleteAdding(); - fsCache?.Clear(); - directoryChangesEvents?.Dispose(); - fileSystemWatcher?.Dispose(); - - fsCache = Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption).ToList(); + lock (fsCache) + fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption)); directoryChangesEvents = new BlockingCollection(); fileSystemWatcher = new FileSystemWatcher(RootDirectory); @@ -64,28 +62,31 @@ namespace FileManager fileSystemWatcher.IncludeSubdirectories = true; fileSystemWatcher.EnableRaisingEvents = true; - //Wait for background scanner to terminate before reinitializing. - backgroundScanner?.Wait(); backgroundScanner = new Task(BackgroundScanner); backgroundScanner.Start(); } + private void Stop() + { + //Stop raising events + fileSystemWatcher?.Dispose(); - private void AddUniqueFiles(IEnumerable newFiles) - { - foreach (var file in newFiles) - { - AddUniqueFile(file); - } - } - private void AddUniqueFile(string newFile) - { - if (!fsCache.Contains(newFile)) - fsCache.Add(newFile); + //Calling CompleteAdding() will cause background scanner to terminate. + directoryChangesEvents?.CompleteAdding(); + + //Wait for background scanner to terminate before reinitializing. + backgroundScanner?.Wait(); + + //Dispose of directoryChangesEvents after backgroundScanner exists. + directoryChangesEvents?.Dispose(); + + lock (fsCache) + fsCache.Clear(); } private void FileSystemWatcher_Error(object sender, ErrorEventArgs e) { - Init(RootDirectory, SearchPattern, SearchOption); + Stop(); + Init(); } private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e) @@ -97,28 +98,26 @@ namespace FileManager private void BackgroundScanner() { while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1)) - UpdateLocalCache(change); + { + lock (fsCache) + UpdateLocalCache(change); + } } - private void UpdateLocalCache(FileSystemEventArgs change) + private void UpdateLocalCache(FileSystemEventArgs change) { - lock (fsCache) + if (change.ChangeType == WatcherChangeTypes.Deleted) { - if (change.ChangeType == WatcherChangeTypes.Deleted) - { - RemovePath(change.FullPath); - } - else if (change.ChangeType == WatcherChangeTypes.Created) - { - AddPath(change.FullPath); - } - else if (change.ChangeType == WatcherChangeTypes.Renamed) - { - var renameChange = change as RenamedEventArgs; - - RemovePath(renameChange.OldFullPath); - AddPath(renameChange.FullPath); - } + RemovePath(change.FullPath); + } + else if (change.ChangeType == WatcherChangeTypes.Created) + { + AddPath(change.FullPath); + } + else if (change.ChangeType == WatcherChangeTypes.Renamed && change is RenamedEventArgs renameChange) + { + RemovePath(renameChange.OldFullPath); + AddPath(renameChange.FullPath); } } @@ -137,6 +136,21 @@ namespace FileManager else AddUniqueFile(path); } + private void AddUniqueFiles(IEnumerable newFiles) + { + foreach (var file in newFiles) + { + AddUniqueFile(file); + } + } + private void AddUniqueFile(string newFile) + { + if (!fsCache.Contains(newFile)) + fsCache.Add(newFile); + } + #endregion + + ~BackgroundFileSystem() => Stop(); } } diff --git a/FileManager/PictureStorage.cs b/FileManager/PictureStorage.cs index ef0ecdf2..8c143dcf 100644 --- a/FileManager/PictureStorage.cs +++ b/FileManager/PictureStorage.cs @@ -1,12 +1,19 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using System.Threading.Tasks; namespace FileManager { - public enum PictureSize { _80x80, _300x300, _500x500 } + public enum PictureSize { _80x80 = 80, _300x300 = 300, _500x500 = 500 } + public class PictureCachedEventArgs : EventArgs + { + public PictureDefinition Definition { get; internal set; } + public byte[] Picture { get; internal set; } + } public struct PictureDefinition { public string PictureId { get; } @@ -27,34 +34,60 @@ namespace FileManager private static string getPath(PictureDefinition def) => Path.Combine(ImagesDirectory, $"{def.PictureId}{def.Size}.jpg"); - private static System.Timers.Timer timer { get; } static PictureStorage() { - timer = new System.Timers.Timer(700) - { - AutoReset = true, - Enabled = true - }; - timer.Elapsed += (_, __) => timerDownload(); + new Task(BackgroundDownloader, TaskCreationOptions.LongRunning) + .Start(); } - public static event EventHandler PictureCached; + public static event EventHandler PictureCached; + private static BlockingCollection DownloadQueue { get; } = new BlockingCollection(); private static Dictionary cache { get; } = new Dictionary(); + private static Dictionary defaultImages { get; } = new Dictionary(); public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def) { - if (!cache.ContainsKey(def)) + lock (cache) { + if (cache.ContainsKey(def)) + return (false, cache[def]); + var path = getPath(def); - cache[def] - = File.Exists(path) - ? File.ReadAllBytes(path) - : null; + + if (File.Exists(path)) + { + cache[def] = File.ReadAllBytes(path); + return (false, cache[def]); + } + + DownloadQueue.Add(def); + return (true, getDefaultImage(def.Size)); + } + } + + public static byte[] GetPictureSynchronously(PictureDefinition def) + { + lock (cache) + { + if (!cache.ContainsKey(def) || cache[def] == null) + { + var path = getPath(def); + byte[] bytes; + + if (File.Exists(path)) + bytes = File.ReadAllBytes(path); + else + { + bytes = downloadBytes(def); + saveFile(def, bytes); + } + + cache[def] = bytes; + } + return cache[def]; } - return (cache[def] == null, cache[def] ?? getDefaultImage(def.Size)); } - private static Dictionary defaultImages { get; } = new Dictionary(); public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes) => defaultImages[pictureSize] = bytes; private static byte[] getDefaultImage(PictureSize size) @@ -62,45 +95,26 @@ namespace FileManager ? defaultImages[size] : new byte[0]; - // necessary to avoid IO errors. ReadAllBytes and WriteAllBytes can conflict in some cases, esp when debugging - private static bool isProcessing; - private static void timerDownload() + static void BackgroundDownloader() { - // must live outside try-catch, else 'finally' can reset another thread's lock - if (isProcessing) - return; - - try + while (!DownloadQueue.IsCompleted) { - isProcessing = true; - - var def = cache - .Where(kvp => kvp.Value is null) - .Select(kvp => kvp.Key) - // 80x80 should be 1st since it's enum value == 0 - .OrderBy(d => d.PictureId) - .FirstOrDefault(); - - // no more null entries. all requsted images are cached - if (string.IsNullOrWhiteSpace(def.PictureId)) - return; + if (!DownloadQueue.TryTake(out var def, System.Threading.Timeout.InfiniteTimeSpan)) + continue; var bytes = downloadBytes(def); saveFile(def, bytes); - cache[def] = bytes; + lock (cache) + cache[def] = bytes; - PictureCached?.Invoke(nameof(PictureStorage), def.PictureId); - } - finally - { - isProcessing = false; + PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes }); } } private static HttpClient imageDownloadClient { get; } = new HttpClient(); private static byte[] downloadBytes(PictureDefinition def) { - var sz = def.Size.ToString().Split('x')[1]; + var sz = (int)def.Size; return imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result; } @@ -110,4 +124,4 @@ namespace FileManager File.WriteAllBytes(path, bytes); } } -} +} \ No newline at end of file diff --git a/InternalUtilities/AudibleApiActions.cs b/InternalUtilities/AudibleApiActions.cs index 9cbe58ee..0d1e2edd 100644 --- a/InternalUtilities/AudibleApiActions.cs +++ b/InternalUtilities/AudibleApiActions.cs @@ -47,18 +47,18 @@ namespace InternalUtilities // 2 retries == 3 total .RetryAsync(2); - public static Task> GetLibraryValidatedAsync(Api api) + public static Task> GetLibraryValidatedAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS) { // bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed // worse, this 1st dummy call doesn't seem to help: // var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS }); // i don't want to incur the cost of making a full dummy call every time because it fails sometimes - return policy.ExecuteAsync(() => getItemsAsync(api)); + return policy.ExecuteAsync(() => getItemsAsync(api, responseGroups)); } - private static async Task> getItemsAsync(Api api) + private static async Task> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups) { - var items = await api.GetAllLibraryItemsAsync(); + var items = await api.GetAllLibraryItemsAsync(responseGroups); // remove episode parents items.RemoveAll(i => i.IsEpisodes); diff --git a/Libation.sln b/Libation.sln index 9de9110b..777fbb84 100644 --- a/Libation.sln +++ b/Libation.sln @@ -74,8 +74,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "Appl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsDesktopUtilities", "WindowsDesktopUtilities\WindowsDesktopUtilities.csproj", "{E7EFD64D-6630-4426-B09C-B6862A92E3FD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}" @@ -190,10 +188,6 @@ Global {059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.Build.0 = Debug|Any CPU {059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.ActiveCfg = Release|Any CPU {059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.Build.0 = Release|Any CPU - {E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.Build.0 = Release|Any CPU {F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -243,7 +237,6 @@ Global {282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2} {B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF} {059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1} - {E7EFD64D-6630-4426-B09C-B6862A92E3FD} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249} {F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14} {8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53} {C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53} diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index c0d121d3..41db5551 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -1,5 +1,5 @@  - + WinExe @@ -13,7 +13,11 @@ win-x64 - 5.4.9.0 + 5.4.9.280 + + + + TRACE;DEBUG diff --git a/LibationLauncher/Program.cs b/LibationLauncher/Program.cs index 2fa3c87a..bcfb1f4d 100644 --- a/LibationLauncher/Program.cs +++ b/LibationLauncher/Program.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows.Forms; -using AudibleApi; using AudibleApi.Authorization; using DataLayer; using Microsoft.EntityFrameworkCore; @@ -13,7 +12,6 @@ using FileManager; using InternalUtilities; using LibationWinForms; using LibationWinForms.Dialogs; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; @@ -59,8 +57,10 @@ namespace LibationLauncher ensureSerilogConfig(config); configureLogging(config); logStartupState(config); - checkForUpdate(config); +#if !DEBUG + checkForUpdate(config); +#endif Application.Run(new Form1()); } diff --git a/LibationWinForms/AsyncNotifyPropertyChanged.cs b/LibationWinForms/AsyncNotifyPropertyChanged.cs new file mode 100644 index 00000000..20b8b512 --- /dev/null +++ b/LibationWinForms/AsyncNotifyPropertyChanged.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace LibationWinForms +{ + public abstract class AsyncNotifyPropertyChanged : SynchronizeInvoker, INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + public AsyncNotifyPropertyChanged() { } + + protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") + =>BeginInvoke(PropertyChanged, new object[] { this, new PropertyChangedEventArgs(propertyName) }); + } +} diff --git a/LibationWinForms/BookLiberation/AudioConvertForm.cs b/LibationWinForms/BookLiberation/AudioConvertForm.cs new file mode 100644 index 00000000..29020425 --- /dev/null +++ b/LibationWinForms/BookLiberation/AudioConvertForm.cs @@ -0,0 +1,24 @@ +using DataLayer; +using System; + +namespace LibationWinForms.BookLiberation +{ + class AudioConvertForm : AudioDecodeForm + { + #region AudioDecodeForm overrides + public override string DecodeActionName => "Converting"; + #endregion + + #region IProcessable event handler overrides + public override void OnBegin(object sender, LibraryBook libraryBook) + { + LogMe.Info($"Convert Step, Begin: {libraryBook.Book}"); + + base.OnBegin(sender, libraryBook); + } + public override void OnCompleted(object sender, LibraryBook libraryBook) + => LogMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}"); + + #endregion + } +} diff --git a/LibationWinForms/BookLiberation/DecryptForm.resx b/LibationWinForms/BookLiberation/AudioConvertForm.resx similarity index 98% rename from LibationWinForms/BookLiberation/DecryptForm.resx rename to LibationWinForms/BookLiberation/AudioConvertForm.resx index f298a7be..e8ae276d 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.resx +++ b/LibationWinForms/BookLiberation/AudioConvertForm.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/BookLiberation/DecryptForm.Designer.cs b/LibationWinForms/BookLiberation/AudioDecodeForm.Designer.cs similarity index 99% rename from LibationWinForms/BookLiberation/DecryptForm.Designer.cs rename to LibationWinForms/BookLiberation/AudioDecodeForm.Designer.cs index e279b1ec..b16425dd 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.Designer.cs +++ b/LibationWinForms/BookLiberation/AudioDecodeForm.Designer.cs @@ -1,6 +1,6 @@ namespace LibationWinForms.BookLiberation { - partial class DecryptForm + partial class AudioDecodeForm { /// /// Required designer variable. diff --git a/LibationWinForms/BookLiberation/AudioDecodeForm.cs b/LibationWinForms/BookLiberation/AudioDecodeForm.cs new file mode 100644 index 00000000..4d8f66b8 --- /dev/null +++ b/LibationWinForms/BookLiberation/AudioDecodeForm.cs @@ -0,0 +1,94 @@ +using DataLayer; +using Dinah.Core.Net.Http; +using Dinah.Core.Windows.Forms; +using LibationWinForms.BookLiberation.BaseForms; +using System; + +namespace LibationWinForms.BookLiberation +{ + public partial class AudioDecodeForm : LiberationBaseForm + { + public virtual string DecodeActionName { get; } = "Decoding"; + public AudioDecodeForm() => InitializeComponent(); + + private Func GetCoverArtDelegate; + + // book info + private string title; + private string authorNames; + private string narratorNames; + + #region IProcessable event handler overrides + public override void OnBegin(object sender, LibraryBook libraryBook) + { + GetCoverArtDelegate = () => FileManager.PictureStorage.GetPictureSynchronously( + new FileManager.PictureDefinition( + libraryBook.Book.PictureId, + FileManager.PictureSize._500x500)); + + //Set default values from library + OnTitleDiscovered(sender, libraryBook.Book.Title); + OnAuthorsDiscovered(sender, string.Join(", ", libraryBook.Book.Authors)); + OnNarratorsDiscovered(sender, string.Join(", ", libraryBook.Book.NarratorNames)); + OnCoverImageDiscovered(sender, + FileManager.PictureStorage.GetPicture( + new FileManager.PictureDefinition( + libraryBook.Book.PictureId, + FileManager.PictureSize._80x80)).bytes); + } + #endregion + + #region IStreamable event handler overrides + public override void OnStreamingBegin(object sender, string beginString) { } + public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress) + { + if (!downloadProgress.ProgressPercentage.HasValue) + return; + + if (downloadProgress.ProgressPercentage == 0) + updateRemainingTime(0); + else + progressBar1.UIThread(() => progressBar1.Value = (int)downloadProgress.ProgressPercentage); + } + + public override void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining) + => updateRemainingTime((int)timeRemaining.TotalSeconds); + + public override void OnStreamingCompleted(object sender, string completedString) { } + #endregion + + #region IAudioDecodable event handlers + public override void OnRequestCoverArt(object sender, Action setCoverArtDelegate) + => setCoverArtDelegate(GetCoverArtDelegate?.Invoke()); + + public override void OnTitleDiscovered(object sender, string title) + { + this.UIThread(() => this.Text = DecodeActionName + " " + title); + this.title = title; + updateBookInfo(); + } + + public override void OnAuthorsDiscovered(object sender, string authors) + { + authorNames = authors; + updateBookInfo(); + } + + public override void OnNarratorsDiscovered(object sender, string narrators) + { + narratorNames = narrators; + updateBookInfo(); + } + + public override void OnCoverImageDiscovered(object sender, byte[] coverArt) + => pictureBox1.UIThread(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt)); + #endregion + + // thread-safe UI updates + private void updateBookInfo() + => bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}"); + + private void updateRemainingTime(int remaining) + => remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec"); + } +} diff --git a/LibationWinForms/BookLiberation/AudioDecodeForm.resx b/LibationWinForms/BookLiberation/AudioDecodeForm.resx new file mode 100644 index 00000000..e8ae276d --- /dev/null +++ b/LibationWinForms/BookLiberation/AudioDecodeForm.resx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/LibationWinForms/BookLiberation/AudioDecryptForm.cs b/LibationWinForms/BookLiberation/AudioDecryptForm.cs new file mode 100644 index 00000000..b28e106d --- /dev/null +++ b/LibationWinForms/BookLiberation/AudioDecryptForm.cs @@ -0,0 +1,24 @@ +using DataLayer; +using System; + +namespace LibationWinForms.BookLiberation +{ + class AudioDecryptForm : AudioDecodeForm + { + #region AudioDecodeForm overrides + public override string DecodeActionName => "Decrypting"; + #endregion + + #region IProcessable event handler overrides + public override void OnBegin(object sender, LibraryBook libraryBook) + { + LogMe.Info($"Download & Decrypt Step, Begin: {libraryBook.Book}"); + + base.OnBegin(sender, libraryBook); + } + public override void OnCompleted(object sender, LibraryBook libraryBook) + => LogMe.Info($"Download & Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}"); + + #endregion + } +} diff --git a/LibationWinForms/BookLiberation/AudioDecryptForm.resx b/LibationWinForms/BookLiberation/AudioDecryptForm.resx new file mode 100644 index 00000000..e8ae276d --- /dev/null +++ b/LibationWinForms/BookLiberation/AudioDecryptForm.resx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/LibationWinForms/BookLiberation/AutomatedBackupsForm.cs b/LibationWinForms/BookLiberation/AutomatedBackupsForm.cs index 95261858..14a114e7 100644 --- a/LibationWinForms/BookLiberation/AutomatedBackupsForm.cs +++ b/LibationWinForms/BookLiberation/AutomatedBackupsForm.cs @@ -1,34 +1,35 @@ -using System; +using Dinah.Core.Windows.Forms; +using System; +using System.Linq; using System.Windows.Forms; -using Dinah.Core.Windows.Forms; namespace LibationWinForms.BookLiberation { - public partial class AutomatedBackupsForm : Form - { - public bool KeepGoingChecked => keepGoingCb.Checked; + public partial class AutomatedBackupsForm : Form + { + public bool KeepGoingChecked => keepGoingCb.Checked; - public bool KeepGoing => keepGoingCb.Enabled && keepGoingCb.Checked; + public bool KeepGoing => keepGoingCb.Enabled && keepGoingCb.Checked; - public AutomatedBackupsForm() - { - InitializeComponent(); - } + public AutomatedBackupsForm() + { + InitializeComponent(); + } - public void WriteLine(string text) - { - if (!IsDisposed) - logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}")); - } + public void WriteLine(string text) + { + if (!IsDisposed) + logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}")); + } public void FinalizeUI() - { - keepGoingCb.Enabled = false; + { + keepGoingCb.Enabled = false; - if (!IsDisposed) - logTb.AppendText(""); - } + if (!IsDisposed) + logTb.AppendText(""); + } - private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false; - } + private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false; + } } diff --git a/LibationWinForms/BookLiberation/AutomatedBackupsForm.resx b/LibationWinForms/BookLiberation/AutomatedBackupsForm.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/BookLiberation/AutomatedBackupsForm.resx +++ b/LibationWinForms/BookLiberation/AutomatedBackupsForm.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs b/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs new file mode 100644 index 00000000..b69fa90d --- /dev/null +++ b/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs @@ -0,0 +1,160 @@ +using DataLayer; +using Dinah.Core.Net.Http; +using Dinah.Core.Windows.Forms; +using FileLiberator; +using System; +using System.Windows.Forms; + +namespace LibationWinForms.BookLiberation.BaseForms +{ + public class LiberationBaseForm : Form + { + protected IStreamable Streamable { get; private set; } + protected LogMe LogMe { get; private set; } + private SynchronizeInvoker Invoker { get; init; } + + public LiberationBaseForm() + { + //SynchronizationContext.Current will be null until the process contains a Form. + //If this is the first form created, it will not exist until after execution + //reaches inside the constructor (after base class has been initialized). + Invoker = new SynchronizeInvoker(); + } + + public void RegisterFileLiberator(IStreamable streamable, LogMe logMe = null) + { + if (streamable is null) return; + + Streamable = streamable; + LogMe = logMe; + + Subscribe(streamable); + + if (Streamable is IProcessable processable) + Subscribe(processable); + if (Streamable is IAudioDecodable audioDecodable) + Subscribe(audioDecodable); + } + + #region Event Subscribers and Unsubscribers + private void Subscribe(IStreamable streamable) + { + UnsubscribeStreamable(this, EventArgs.Empty); + + streamable.StreamingBegin += OnStreamingBeginShow; + streamable.StreamingBegin += OnStreamingBegin; + streamable.StreamingProgressChanged += OnStreamingProgressChanged; + streamable.StreamingTimeRemaining += OnStreamingTimeRemaining; + streamable.StreamingCompleted += OnStreamingCompleted; + streamable.StreamingCompleted += OnStreamingCompletedClose; + + FormClosed += UnsubscribeStreamable; + } + private void Subscribe(IProcessable processable) + { + UnsubscribeProcessable(this, null); + + processable.Begin += OnBegin; + processable.StatusUpdate += OnStatusUpdate; + processable.Completed += OnCompleted; + + //The form is created on IProcessable.Begin and we + //dispose of it on IProcessable.Completed + processable.Completed += OnCompletedDispose; + + //Don't unsubscribe from Dispose because it fires when + //IStreamable.StreamingCompleted closes the form, and + //the IProcessable events need to live past that event. + processable.Completed += UnsubscribeProcessable; + } + private void Subscribe(IAudioDecodable audioDecodable) + { + UnsubscribeAudioDecodable(this, EventArgs.Empty); + + audioDecodable.RequestCoverArt += OnRequestCoverArt; + audioDecodable.TitleDiscovered += OnTitleDiscovered; + audioDecodable.AuthorsDiscovered += OnAuthorsDiscovered; + audioDecodable.NarratorsDiscovered += OnNarratorsDiscovered; + audioDecodable.CoverImageDiscovered += OnCoverImageDiscovered; + + Disposed += UnsubscribeAudioDecodable; + } + private void UnsubscribeStreamable(object sender, EventArgs e) + { + FormClosed -= UnsubscribeStreamable; + + Streamable.StreamingBegin -= OnStreamingBeginShow; + Streamable.StreamingBegin -= OnStreamingBegin; + Streamable.StreamingProgressChanged -= OnStreamingProgressChanged; + Streamable.StreamingTimeRemaining -= OnStreamingTimeRemaining; + Streamable.StreamingCompleted -= OnStreamingCompleted; + Streamable.StreamingCompleted -= OnStreamingCompletedClose; + } + private void UnsubscribeProcessable(object sender, LibraryBook e) + { + if (Streamable is not IProcessable processable) + return; + + processable.Completed -= UnsubscribeProcessable; + processable.Completed -= OnCompletedDispose; + processable.Completed -= OnCompleted; + processable.StatusUpdate -= OnStatusUpdate; + processable.Begin -= OnBegin; + } + private void UnsubscribeAudioDecodable(object sender, EventArgs e) + { + if (Streamable is not IAudioDecodable audioDecodable) + return; + + Disposed -= UnsubscribeAudioDecodable; + audioDecodable.RequestCoverArt -= OnRequestCoverArt; + audioDecodable.TitleDiscovered -= OnTitleDiscovered; + audioDecodable.AuthorsDiscovered -= OnAuthorsDiscovered; + audioDecodable.NarratorsDiscovered -= OnNarratorsDiscovered; + audioDecodable.CoverImageDiscovered -= OnCoverImageDiscovered; + + audioDecodable.Cancel(); + } + #endregion + + #region Form creation and disposal handling + + /// + /// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose + /// + private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThread(() => Close()); + private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(() => Dispose()); + + /// + /// If StreamingBegin is fired from a worker thread, the window will be created on that + /// worker thread. We need to make certain that we show the window on the UI thread (same + /// thread that created form), otherwise the renderer will be on a worker thread which + /// could cause it to freeze. Form.BeginInvoke won't work until the form is created + /// (ie. shown) because Control doesn't get a window handle until it is Shown. + /// + private void OnStreamingBeginShow(object sender, string beginString) => Invoker.Invoke(Show); + + #endregion + + #region IStreamable event handlers + public virtual void OnStreamingBegin(object sender, string beginString) { } + public virtual void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress) { } + public virtual void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining) { } + public virtual void OnStreamingCompleted(object sender, string completedString) { } + #endregion + + #region IProcessable event handlers + public virtual void OnBegin(object sender, LibraryBook libraryBook) { } + public virtual void OnStatusUpdate(object sender, string statusUpdate) { } + public virtual void OnCompleted(object sender, LibraryBook libraryBook) { } + #endregion + + #region IAudioDecodable event handlers + public virtual void OnRequestCoverArt(object sender, Action setCoverArtDelegate) { } + public virtual void OnTitleDiscovered(object sender, string title) { } + public virtual void OnAuthorsDiscovered(object sender, string authors) { } + public virtual void OnNarratorsDiscovered(object sender, string narrators) { } + public virtual void OnCoverImageDiscovered(object sender, byte[] coverArt) { } + #endregion + } +} diff --git a/LibationWinForms/BookLiberation/DecryptForm.cs b/LibationWinForms/BookLiberation/DecryptForm.cs deleted file mode 100644 index e6919b16..00000000 --- a/LibationWinForms/BookLiberation/DecryptForm.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Windows.Forms; -using Dinah.Core.Windows.Forms; - -namespace LibationWinForms.BookLiberation -{ - public partial class DecryptForm : Form - { - public DecryptForm() => InitializeComponent(); - - // book info - private string title; - private string authorNames; - private string narratorNames; - - public void SetTitle(string actionName, string title) - { - this.UIThread(() => this.Text = actionName + " " + title); - this.title = title; - updateBookInfo(); - } - public void SetAuthorNames(string authorNames) - { - this.authorNames = authorNames; - updateBookInfo(); - } - public void SetNarratorNames(string narratorNames) - { - this.narratorNames = narratorNames; - updateBookInfo(); - } - - // thread-safe UI updates - private void updateBookInfo() - => bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}"); - - public void SetCoverImage(System.Drawing.Image coverImage) - => pictureBox1.UIThread(() => pictureBox1.Image = coverImage); - - public void UpdateProgress(int percentage) - { - if (percentage == 0) - updateRemainingTime(0); - else - progressBar1.UIThread(() => progressBar1.Value = percentage); - } - - public void UpdateRemainingTime(TimeSpan remaining) - => updateRemainingTime((int)remaining.TotalSeconds); - - private void updateRemainingTime(int remaining) - => remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec"); - } -} diff --git a/LibationWinForms/BookLiberation/DownloadForm.Designer.cs b/LibationWinForms/BookLiberation/DownloadForm.Designer.cs index 7f5f80c3..b6746fe5 100644 --- a/LibationWinForms/BookLiberation/DownloadForm.Designer.cs +++ b/LibationWinForms/BookLiberation/DownloadForm.Designer.cs @@ -1,4 +1,7 @@ -namespace LibationWinForms.BookLiberation +using DataLayer; +using System; + +namespace LibationWinForms.BookLiberation { partial class DownloadForm { @@ -95,9 +98,10 @@ } - #endregion - private System.Windows.Forms.Label filenameLbl; + #endregion + + private System.Windows.Forms.Label filenameLbl; private System.Windows.Forms.ProgressBar progressBar1; private System.Windows.Forms.Label progressLbl; private System.Windows.Forms.Label lastUpdateLbl; diff --git a/LibationWinForms/BookLiberation/DownloadForm.cs b/LibationWinForms/BookLiberation/DownloadForm.cs index ffdf28e0..e503deac 100644 --- a/LibationWinForms/BookLiberation/DownloadForm.cs +++ b/LibationWinForms/BookLiberation/DownloadForm.cs @@ -1,59 +1,66 @@ -using System; -using System.Windows.Forms; +using Dinah.Core.Net.Http; using Dinah.Core.Windows.Forms; +using LibationWinForms.BookLiberation.BaseForms; +using System; +using System.Linq; +using System.Windows.Forms; namespace LibationWinForms.BookLiberation { - public partial class DownloadForm : Form - { - public DownloadForm() - { - InitializeComponent(); + public partial class DownloadForm : LiberationBaseForm + { + public DownloadForm() + { + InitializeComponent(); - progressLbl.Text = ""; - filenameLbl.Text = ""; - } + progressLbl.Text = ""; + filenameLbl.Text = ""; + } - // thread-safe UI updates - public void UpdateFilename(string title) => filenameLbl.UIThread(() => filenameLbl.Text = title); - public void DownloadProgressChanged(long BytesReceived, long? TotalBytesToReceive) - { - // this won't happen with download file. it will happen with download string - if (!TotalBytesToReceive.HasValue || TotalBytesToReceive.Value <= 0) - return; + #region IStreamable event handler overrides + public override void OnStreamingBegin(object sender, string beginString) + { + filenameLbl.UIThread(() => filenameLbl.Text = beginString); + } + public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress) + { + // this won't happen with download file. it will happen with download string + if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0) + return; - progressLbl.UIThread(() => progressLbl.Text = $"{BytesReceived:#,##0} of {TotalBytesToReceive.Value:#,##0}"); + progressLbl.UIThread(() => progressLbl.Text = $"{downloadProgress.BytesReceived:#,##0} of {downloadProgress.TotalBytesToReceive.Value:#,##0}"); - var d = double.Parse(BytesReceived.ToString()) / double.Parse(TotalBytesToReceive.Value.ToString()) * 100.0; - var i = int.Parse(Math.Truncate(d).ToString()); - progressBar1.UIThread(() => progressBar1.Value = i); + var d = double.Parse(downloadProgress.BytesReceived.ToString()) / double.Parse(downloadProgress.TotalBytesToReceive.Value.ToString()) * 100.0; + var i = int.Parse(Math.Truncate(d).ToString()); + progressBar1.UIThread(() => progressBar1.Value = i); - lastDownloadProgress = DateTime.Now; - } + lastDownloadProgress = DateTime.Now; + } + #endregion - #region timer - private Timer timer { get; } = new Timer { Interval = 1000 }; - private void DownloadForm_Load(object sender, EventArgs e) - { - timer.Tick += new EventHandler(timer_Tick); - timer.Start(); - } - private DateTime lastDownloadProgress = DateTime.Now; - private void timer_Tick(object sender, EventArgs e) - { - // if no update in the last 30 seconds, display frozen label - lastUpdateLbl.UIThread(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now); - if (lastUpdateLbl.Visible) - { - var diff = DateTime.Now - lastDownloadProgress; - var min = (int)diff.TotalMinutes; - var minText = min > 0 ? $"{min}min " : ""; + #region timer + private Timer timer { get; } = new Timer { Interval = 1000 }; + private void DownloadForm_Load(object sender, EventArgs e) + { + timer.Tick += new EventHandler(timer_Tick); + timer.Start(); + } + private DateTime lastDownloadProgress = DateTime.Now; + private void timer_Tick(object sender, EventArgs e) + { + // if no update in the last 30 seconds, display frozen label + lastUpdateLbl.UIThread(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now); + if (lastUpdateLbl.Visible) + { + var diff = DateTime.Now - lastDownloadProgress; + var min = (int)diff.TotalMinutes; + var minText = min > 0 ? $"{min}min " : ""; - lastUpdateLbl.UIThread(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago"); - } - } - private void DownloadForm_FormClosing(object sender, FormClosingEventArgs e) => timer.Stop(); - #endregion - } + lastUpdateLbl.UIThread(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago"); + } + } + private void DownloadForm_FormClosing(object sender, FormClosingEventArgs e) => timer.Stop(); + #endregion + } } diff --git a/LibationWinForms/BookLiberation/DownloadForm.resx b/LibationWinForms/BookLiberation/DownloadForm.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/BookLiberation/DownloadForm.resx +++ b/LibationWinForms/BookLiberation/DownloadForm.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/BookLiberation/PdfDownloadForm.cs b/LibationWinForms/BookLiberation/PdfDownloadForm.cs new file mode 100644 index 00000000..3def88c2 --- /dev/null +++ b/LibationWinForms/BookLiberation/PdfDownloadForm.cs @@ -0,0 +1,10 @@ +using DataLayer; + +namespace LibationWinForms.BookLiberation +{ + internal class PdfDownloadForm : DownloadForm + { + public override void OnBegin(object sender, LibraryBook libraryBook) => LogMe.Info($"PDF Step, Begin: {libraryBook.Book}"); + public override void OnCompleted(object sender, LibraryBook libraryBook) => LogMe.Info($"PDF Step, Completed: {libraryBook.Book}"); + } +} diff --git a/LibationWinForms/BookLiberation/PdfDownloadForm.resx b/LibationWinForms/BookLiberation/PdfDownloadForm.resx new file mode 100644 index 00000000..03e6393f --- /dev/null +++ b/LibationWinForms/BookLiberation/PdfDownloadForm.resx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs index f378bae7..f30618ae 100644 --- a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs +++ b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs @@ -1,505 +1,269 @@ -using System; +using DataLayer; +using Dinah.Core; +using FileLiberator; +using LibationWinForms.BookLiberation.BaseForms; +using System; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; -using DataLayer; -using Dinah.Core; -using Dinah.Core.ErrorHandling; -using Dinah.Core.Windows.Forms; -using FileLiberator; namespace LibationWinForms.BookLiberation { - // decouple serilog and form. include convenience factory method - public class LogMe - { - public event EventHandler LogInfo; - public event EventHandler LogErrorString; - public event EventHandler<(Exception, string)> LogError; + // decouple serilog and form. include convenience factory method + public class LogMe + { + public event EventHandler LogInfo; + public event EventHandler LogErrorString; + public event EventHandler<(Exception, string)> LogError; - private LogMe() - { - LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}"); - LogErrorString += (_, text) => Serilog.Log.Logger.Error(text); - LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error"); - } + private LogMe() + { + LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}"); + LogErrorString += (_, text) => Serilog.Log.Logger.Error(text); + LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error"); + } - public static LogMe RegisterForm(AutomatedBackupsForm form = null) - { - var logMe = new LogMe(); + public static LogMe RegisterForm(AutomatedBackupsForm form = null) + { + var logMe = new LogMe(); - if (form is null) - return logMe; + if (form is null) + return logMe; - logMe.LogInfo += (_, text) => form?.WriteLine(text); + logMe.LogInfo += (_, text) => form?.WriteLine(text); - logMe.LogErrorString += (_, text) => form?.WriteLine(text); - - logMe.LogError += (_, tuple) => - { - form?.WriteLine(tuple.Item2 ?? "Automated backup: error"); - form?.WriteLine("ERROR: " + tuple.Item1.Message); - }; - - return logMe; - } - - public void Info(string text) => LogInfo?.Invoke(this, text); - public void Error(string text) => LogErrorString?.Invoke(this, text); - public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text)); - } - - public static class ProcessorAutomationController - { - public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler completedAction = null) - { - Serilog.Log.Logger.Information("Begin backup single {@DebugInfo}", new { libraryBook?.Book?.AudibleProductId }); - - var backupBook = getWiredUpBackupBook(completedAction); - - (Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook); - - // continue even if libraryBook is null. we'll display even that in the processing box - await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync(); - - unsubscribeEvents(); - } - - public static async Task BackupAllBooksAsync(EventHandler completedAction = null) - { - Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync)); - - var backupBook = getWiredUpBackupBook(completedAction); - var automatedBackupsForm = new AutomatedBackupsForm(); - - (Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook, automatedBackupsForm); - - await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync(); - - unsubscribeEvents(); - } - - public static async Task ConvertAllBooksAsync() - { - Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync)); - - var convertBook = new ConvertToMp3(); - convertBook.Begin += (_, l) => wireUpEvents(convertBook, l, "Converting"); - - var automatedBackupsForm = new AutomatedBackupsForm(); - - var logMe = LogMe.RegisterForm(automatedBackupsForm); - - void statusUpdate(object _, string str) => logMe.Info("- " + str); - void convertBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Convert Step, Begin: {libraryBook.Book}"); - void convertBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}"); - convertBook.Begin += convertBookBegin; - convertBook.StatusUpdate += statusUpdate; - convertBook.Completed += convertBookCompleted; - - await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync(); - - convertBook.Begin -= convertBookBegin; - convertBook.StatusUpdate -= statusUpdate; - convertBook.Completed -= convertBookCompleted; - } - - private static BackupBook getWiredUpBackupBook(EventHandler completedAction) - { - var backupBook = new BackupBook(); - - backupBook.DownloadDecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DownloadDecryptBook, l); - backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf); - - if (completedAction != null) - { - backupBook.DownloadDecryptBook.Completed += completedAction; - backupBook.DownloadPdf.Completed += completedAction; - } - - return backupBook; - } - - private static (Action unsubscribeEvents, LogMe) attachToBackupsForm(BackupBook backupBook, AutomatedBackupsForm automatedBackupsForm = null) - { - #region create logger - var logMe = LogMe.RegisterForm(automatedBackupsForm); - #endregion - - #region define how model actions will affect form behavior - void statusUpdate(object _, string str) => logMe.Info("- " + str); - void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}"); - // extra line after book is completely finished - void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}"); - void downloadPdfBegin(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Begin: {libraryBook.Book}"); - // extra line after book is completely finished - void downloadPdfCompleted(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}"); - #endregion - - #region subscribe new form to model's events - backupBook.DownloadDecryptBook.Begin += decryptBookBegin; - backupBook.DownloadDecryptBook.StatusUpdate += statusUpdate; - backupBook.DownloadDecryptBook.Completed += decryptBookCompleted; - backupBook.DownloadPdf.Begin += downloadPdfBegin; - backupBook.DownloadPdf.StatusUpdate += statusUpdate; - backupBook.DownloadPdf.Completed += downloadPdfCompleted; - #endregion - - #region when form closes, unsubscribe from model's events - // unsubscribe so disposed forms aren't still trying to receive notifications - Action unsubscribe = () => - { - backupBook.DownloadDecryptBook.Begin -= decryptBookBegin; - backupBook.DownloadDecryptBook.StatusUpdate -= statusUpdate; - backupBook.DownloadDecryptBook.Completed -= decryptBookCompleted; - backupBook.DownloadPdf.Begin -= downloadPdfBegin; - backupBook.DownloadPdf.StatusUpdate -= statusUpdate; - backupBook.DownloadPdf.Completed -= downloadPdfCompleted; - }; - #endregion - - return (unsubscribe, logMe); - } - - public static async Task BackupAllPdfsAsync(EventHandler completedAction = null) - { - Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync)); - - var downloadPdf = getWiredUpDownloadPdf(completedAction); - - (AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf); - await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync(); - } - - private static DownloadPdf getWiredUpDownloadPdf(EventHandler completedAction) - { - var downloadPdf = new DownloadPdf(); - - downloadPdf.Begin += (_, __) => wireUpEvents(downloadPdf); - - if (completedAction != null) - downloadPdf.Completed += completedAction; - - return downloadPdf; - } - - public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false) - { - var downloadDialog = new DownloadForm(); - downloadDialog.UpdateFilename(destination); - downloadDialog.Show(); - - new System.Threading.Thread(() => - { - var downloadFile = new DownloadFile(); - - downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.UIThread(() => - downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive) - ); - downloadFile.DownloadCompleted += (_, __) => downloadDialog.UIThread(() => - { - downloadDialog.Close(); - if (showDownloadCompletedDialog) - MessageBox.Show("File downloaded"); - }); - - downloadFile.PerformDownloadFileAsync(url, destination).GetAwaiter().GetResult(); - }) - { IsBackground = true } - .Start(); - } - - // subscribed to Begin event because a new form should be created+processed+closed on each iteration - private static void wireUpEvents(IDownloadableProcessable downloadable) - { - #region create form - var downloadDialog = new DownloadForm(); - #endregion - - // extra complexity for wiring up download form: - // case 1: download is needed - // dialog created. subscribe to events - // downloadable.DownloadBegin fires. shows dialog - // downloadable.DownloadCompleted fires. closes dialog. which fires FormClosing, FormClosed, Disposed - // Disposed unsubscribe from events - // case 2: download is not needed - // dialog created. subscribe to events - // dialog is never shown nor closed - // downloadable.Completed fires. disposes dialog and unsubscribes from events - - #region define how model actions will affect form behavior - void downloadBegin(object _, string str) - { - downloadDialog.UpdateFilename(str); - downloadDialog.Show(); - } - - // close form on DOWNLOAD completed, not final Completed. Else for BackupBook this form won't close until DECRYPT is also complete - void fileDownloadCompleted(object _, string __) => downloadDialog.Close(); - - void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress) - => downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive); - - void unsubscribe(object _ = null, EventArgs __ = null) - { - downloadable.DownloadBegin -= downloadBegin; - downloadable.DownloadCompleted -= fileDownloadCompleted; - downloadable.DownloadProgressChanged -= downloadProgressChanged; - downloadable.Completed -= dialogDispose; - } - - // unless we dispose, if the form is created but un-used/never-shown then weird UI stuff can happen - // also, since event unsubscribe occurs on FormClosing and an unused form is never closed, then the events will never be unsubscribed - void dialogDispose(object _, object __) - { - if (!downloadDialog.IsDisposed) - downloadDialog.Dispose(); - } - #endregion - - #region subscribe new form to model's events - downloadable.DownloadBegin += downloadBegin; - downloadable.DownloadCompleted += fileDownloadCompleted; - downloadable.DownloadProgressChanged += downloadProgressChanged; - downloadable.Completed += dialogDispose; - #endregion - - #region when form closes, unsubscribe from model's events - // unsubscribe so disposed forms aren't still trying to receive notifications - // FormClosing is more UI safe but won't fire unless the form is shown and closed - // if form was shown, Disposed will fire for FormClosing, FormClosed, and Disposed - // if not shown, it will still fire for Disposed - downloadDialog.Disposed += unsubscribe; - #endregion - } - - // subscribed to Begin event because a new form should be created+processed+closed on each iteration - private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook, string actionName = "Decrypting") - { - #region create form - var decryptDialog = new DecryptForm(); - #endregion - - #region Set initially displayed book properties from library info. - decryptDialog.SetTitle(actionName, libraryBook.Book.Title); - decryptDialog.SetAuthorNames(string.Join(", ", libraryBook.Book.Authors)); - decryptDialog.SetNarratorNames(string.Join(", ", libraryBook.Book.NarratorNames)); - decryptDialog.SetCoverImage( - WindowsDesktopUtilities.WinAudibleImageServer.GetImage( - libraryBook.Book.PictureId, - FileManager.PictureSize._80x80 - )); - #endregion - - #region define how model actions will affect form behavior - void decryptBegin(object _, string __) => decryptDialog.Show(); - - void titleDiscovered(object _, string title) => decryptDialog.SetTitle(actionName, title); - void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors); - void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators); - void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(Dinah.Core.Drawing.ImageReader.ToImage(coverBytes)); - void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage); - void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining); - void decryptCompleted(object _, string __) => decryptDialog.Close(); - - void requestCoverArt(object _, Action setCoverArtDelegate) - { - var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500); - (bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(picDef); - - if (isDefault) - { - void pictureCached(object _, string pictureId) - { - if (pictureId == libraryBook.Book.PictureId) - { - FileManager.PictureStorage.PictureCached -= pictureCached; - - var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500); - (_, picture) = FileManager.PictureStorage.GetPicture(picDef); - - setCoverArtDelegate(picture); - } - }; - FileManager.PictureStorage.PictureCached += pictureCached; - } - else - setCoverArtDelegate(picture); - } - #endregion - - #region subscribe new form to model's events - decryptBook.DecryptBegin += decryptBegin; - - decryptBook.TitleDiscovered += titleDiscovered; - decryptBook.AuthorsDiscovered += authorsDiscovered; - decryptBook.NarratorsDiscovered += narratorsDiscovered; - decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered; - decryptBook.UpdateProgress += updateProgress; - decryptBook.UpdateRemainingTime += updateRemainingTime; - decryptBook.RequestCoverArt += requestCoverArt; - - decryptBook.DecryptCompleted += decryptCompleted; - #endregion - - #region when form closes, unsubscribe from model's events - // unsubscribe so disposed forms aren't still trying to receive notifications - decryptDialog.FormClosing += (_, __) => - { - decryptBook.DecryptBegin -= decryptBegin; - - decryptBook.TitleDiscovered -= titleDiscovered; - decryptBook.AuthorsDiscovered -= authorsDiscovered; - decryptBook.NarratorsDiscovered -= narratorsDiscovered; - decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered; - decryptBook.UpdateProgress -= updateProgress; - decryptBook.UpdateRemainingTime -= updateRemainingTime; - decryptBook.RequestCoverArt -= requestCoverArt; - - decryptBook.DecryptCompleted -= decryptCompleted; - decryptBook.Cancel(); - }; - #endregion - } - - private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable) - { - #region create form and logger - var automatedBackupsForm = new AutomatedBackupsForm(); - var logMe = LogMe.RegisterForm(automatedBackupsForm); - #endregion - - #region define how model actions will affect form behavior - void begin(object _, LibraryBook libraryBook) => logMe.Info($"Begin: {libraryBook.Book}"); - void statusUpdate(object _, string str) => logMe.Info("- " + str); - // extra line after book is completely finished - void completed(object _, LibraryBook libraryBook) => logMe.Info($"Completed: {libraryBook.Book}{Environment.NewLine}"); - #endregion - - #region subscribe new form to model's events - downloadable.Begin += begin; - downloadable.StatusUpdate += statusUpdate; - downloadable.Completed += completed; - #endregion - - #region when form closes, unsubscribe from model's events - // unsubscribe so disposed forms aren't still trying to receive notifications - automatedBackupsForm.FormClosing += (_, __) => - { - downloadable.Begin -= begin; - downloadable.StatusUpdate -= statusUpdate; - downloadable.Completed -= completed; - }; - #endregion - - return (automatedBackupsForm, logMe); - } - } - - abstract class BackupRunner - { - protected LogMe LogMe { get; } - protected IProcessable Processable { get; } - protected AutomatedBackupsForm AutomatedBackupsForm { get; } - - protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm = null) - { - LogMe = logMe; - Processable = processable; - AutomatedBackupsForm = automatedBackupsForm; - } - - protected abstract Task RunAsync(); - - protected abstract string SkipDialogText { get; } - protected abstract MessageBoxButtons SkipDialogButtons { get; } - protected abstract DialogResult CreateSkipFileResult { get; } - - public async Task RunBackupAsync() - { - AutomatedBackupsForm?.Show(); - - try - { - await RunAsync(); - } - catch (Exception ex) - { - LogMe.Error(ex); - } - - AutomatedBackupsForm?.FinalizeUI(); - LogMe.Info("DONE"); - } - - protected async Task ProcessOneAsync(Func> func, LibraryBook libraryBook) - { - string logMessage; - - try - { - var statusHandler = await func(libraryBook); - - if (statusHandler.IsSuccess) - return true; - - foreach (var errorMessage in statusHandler.Errors) - LogMe.Error(errorMessage); - - logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}"); - } - catch (Exception ex) - { - LogMe.Error(ex); - - logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace; - } - - LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed"); - - string details; - try - { - static string trunc(string str) - => string.IsNullOrWhiteSpace(str) ? "[empty]" - : (str.Length > 50) ? $"{str.Truncate(47)}..." - : str; - - details = + logMe.LogErrorString += (_, text) => form?.WriteLine(text); + + logMe.LogError += (_, tuple) => + { + form?.WriteLine(tuple.Item2 ?? "Automated backup: error"); + form?.WriteLine("ERROR: " + tuple.Item1.Message); + }; + + return logMe; + } + + public void Info(string text) => LogInfo?.Invoke(this, text); + public void Error(string text) => LogErrorString?.Invoke(this, text); + public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text)); + } + + public static class ProcessorAutomationController + { + public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler completedAction = null) + { + Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId }); + + var logMe = LogMe.RegisterForm(); + var backupBook = CreateBackupBook(completedAction, 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) + { + Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync)); + + var automatedBackupsForm = new AutomatedBackupsForm(); + var logMe = LogMe.RegisterForm(automatedBackupsForm); + var backupBook = CreateBackupBook(completedAction, logMe); + + await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync(); + } + + public static async Task ConvertAllBooksAsync() + { + Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync)); + + var automatedBackupsForm = new AutomatedBackupsForm(); + var logMe = LogMe.RegisterForm(automatedBackupsForm); + + var convertBook = CreateProcessable(logMe); + + await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync(); + } + + public static async Task BackupAllPdfsAsync(EventHandler completedAction = null) + { + Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync)); + + var automatedBackupsForm = new AutomatedBackupsForm(); + var logMe = LogMe.RegisterForm(automatedBackupsForm); + + var downloadPdf = CreateProcessable(logMe, completedAction); + + await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync(); + } + + private static IProcessable CreateBackupBook(EventHandler completedAction, LogMe logMe) + { + var downloadPdf = CreateProcessable(logMe); + + //Chain pdf download on DownloadDecryptBook.Completed + async void onDownloadDecryptBookCompleted(object sender, LibraryBook e) + { + await downloadPdf.TryProcessAsync(e); + completedAction(sender, e); + } + + var downloadDecryptBook = CreateProcessable(logMe, onDownloadDecryptBookCompleted); + return downloadDecryptBook; + } + + public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false) + { + Serilog.Log.Logger.Information($"Begin {nameof(DownloadFile)} for {url}"); + + void onDownloadFileStreamingCompleted(object sender, string savedFile) + { + Serilog.Log.Logger.Information($"Completed {nameof(DownloadFile)} for {url}. Saved to {savedFile}"); + + if (showDownloadCompletedDialog) + MessageBox.Show($"File downloaded to:{Environment.NewLine}{Environment.NewLine}{savedFile}"); + } + + var downloadFile = new DownloadFile(); + var downloadForm = new DownloadForm(); + downloadForm.RegisterFileLiberator(downloadFile); + downloadFile.StreamingCompleted += onDownloadFileStreamingCompleted; + + async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination); + new Task(runDownload).Start(); + } + + /// + /// Create a new and links it to a new . + /// + /// The derived type to create. + /// The derived Form to create on , Show on , Close on , and Dispose on + /// The logger + /// An additional event handler to handle + /// A new of type + private static TProcessable CreateProcessable(LogMe logMe, EventHandler completedAction = null) + where TForm : LiberationBaseForm, new() + where TProcessable : IProcessable, new() + { + var strProc = new TProcessable(); + + strProc.Begin += (sender, libraryBook) => + { + var processForm = new TForm(); + processForm.RegisterFileLiberator(strProc, logMe); + processForm.OnBegin(sender, libraryBook); + }; + + strProc.Completed += completedAction; + + return strProc; + } + } + + internal abstract class BackupRunner + { + protected LogMe LogMe { get; } + protected IProcessable Processable { get; } + protected AutomatedBackupsForm AutomatedBackupsForm { get; } + + protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm = null) + { + LogMe = logMe; + Processable = processable; + AutomatedBackupsForm = automatedBackupsForm; + } + + protected abstract Task RunAsync(); + protected abstract string SkipDialogText { get; } + protected abstract MessageBoxButtons SkipDialogButtons { get; } + protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; } + protected abstract DialogResult CreateSkipFileResult { get; } + + public async Task RunBackupAsync() + { + AutomatedBackupsForm?.Show(); + + try + { + await RunAsync(); + } + catch (Exception ex) + { + LogMe.Error(ex); + } + + AutomatedBackupsForm?.FinalizeUI(); + LogMe.Info("DONE"); + } + + protected async Task ProcessOneAsync(LibraryBook libraryBook, bool validate) + { + string logMessage; + + try + { + var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate); + + if (statusHandler.IsSuccess) + return true; + + foreach (var errorMessage in statusHandler.Errors) + LogMe.Error(errorMessage); + + logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}"); + } + catch (Exception ex) + { + LogMe.Error(ex); + + logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace; + } + + LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed"); + + string details; + try + { + static string trunc(string str) + => string.IsNullOrWhiteSpace(str) ? "[empty]" + : (str.Length > 50) ? $"{str.Truncate(47)}..." + : str; + + details = $@" Title: {libraryBook.Book.Title} ID: {libraryBook.Book.AudibleProductId} Author: {trunc(libraryBook.Book.AuthorNames)} Narr: {trunc(libraryBook.Book.NarratorNames)}"; - } - catch - { - details = "[Error retrieving details]"; - } + } + catch + { + details = "[Error retrieving details]"; + } - var dialogResult = MessageBox.Show(string.Format(SkipDialogText, details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question); + var dialogResult = MessageBox.Show(string.Format(SkipDialogText, details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton); - if (dialogResult == DialogResult.Abort) - return false; + if (dialogResult == DialogResult.Abort) + return false; - if (dialogResult == CreateSkipFileResult) - { - ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null); - var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage); - LogMe.Info($@" + if (dialogResult == CreateSkipFileResult) + { + ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null); + var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage); + LogMe.Info($@" Created new 'skip' file [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title} {path} ".Trim()); - } + } - return true; - } - } - class BackupSingle : BackupRunner - { - private LibraryBook _libraryBook { get; } + return true; + } + } - protected override string SkipDialogText => @" + internal class BackupSingle : BackupRunner + { + private LibraryBook _libraryBook { get; } + + protected override string SkipDialogText => @" An error occurred while trying to process this book. Skip this book permanently? {0} @@ -507,24 +271,26 @@ An error occurred while trying to process this book. Skip this book permanently? - Click NO to skip the book this time only. We'll try again later. ".Trim(); - protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo; - protected override DialogResult CreateSkipFileResult => DialogResult.Yes; + protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo; + protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2; + protected override DialogResult CreateSkipFileResult => DialogResult.Yes; - public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook) - : base(logMe, processable) - { - _libraryBook = libraryBook; - } + public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook) + : base(logMe, processable) + { + _libraryBook = libraryBook; + } - protected override async Task RunAsync() - { - if (_libraryBook is not null) - await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook); - } - } - class BackupLoop : BackupRunner - { - protected override string SkipDialogText => @" + protected override async Task RunAsync() + { + if (_libraryBook is not null) + await ProcessOneAsync(_libraryBook, validate: true); + } + } + + internal class BackupLoop : BackupRunner + { + protected override string SkipDialogText => @" An error occurred while trying to process this book. {0} @@ -534,33 +300,34 @@ An error occurred while trying to process this book. - IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.) ".Trim(); - protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore; - protected override DialogResult CreateSkipFileResult => DialogResult.Ignore; + protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore; + protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1; + protected override DialogResult CreateSkipFileResult => DialogResult.Ignore; - public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm) - : base(logMe, processable, automatedBackupsForm) { } + public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm) + : base(logMe, processable, automatedBackupsForm) { } - 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()) - { - var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook); - if (!keepGoing) - return; + 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()) + { + var keepGoing = await ProcessOneAsync(libraryBook, validate: false); + if (!keepGoing) + return; - if (AutomatedBackupsForm.IsDisposed) - break; + if (AutomatedBackupsForm.IsDisposed) + break; - if (!AutomatedBackupsForm.KeepGoing) - { - if (!AutomatedBackupsForm.KeepGoingChecked) - LogMe.Info("'Keep going' is unchecked"); - return; - } - } + if (!AutomatedBackupsForm.KeepGoing) + { + if (!AutomatedBackupsForm.KeepGoingChecked) + LogMe.Info("'Keep going' is unchecked"); + return; + } + } - LogMe.Info("Done. All books have been processed"); - } - } + LogMe.Info("Done. All books have been processed"); + } + } } diff --git a/LibationWinForms/DataGridViewImageButtonColumn.cs b/LibationWinForms/DataGridViewImageButtonColumn.cs new file mode 100644 index 00000000..e607a069 --- /dev/null +++ b/LibationWinForms/DataGridViewImageButtonColumn.cs @@ -0,0 +1,50 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace LibationWinForms +{ + public abstract class DataGridViewImageButtonColumn : DataGridViewButtonColumn + { + private DataGridViewImageButtonCell _cellTemplate; + public override DataGridViewCell CellTemplate + { + get => GetCellTemplate(); + set + { + if (value is DataGridViewImageButtonCell cellTemplate) + _cellTemplate = cellTemplate; + } + } + + protected abstract DataGridViewImageButtonCell NewCell(); + + private DataGridViewImageButtonCell GetCellTemplate() + { + if (_cellTemplate is null) + return NewCell(); + else + return _cellTemplate; + } + + public override object Clone() + { + var clone = (DataGridViewImageButtonColumn)base.Clone(); + clone._cellTemplate = _cellTemplate; + + return clone; + } + } + + public class DataGridViewImageButtonCell : DataGridViewButtonCell + { + protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds) + { + var w = image.Width; + var h = image.Height; + var x = cellBounds.Left + (cellBounds.Width - w) / 2; + var y = cellBounds.Top + (cellBounds.Height - h) / 2; + + graphics.DrawImage(image, new Rectangle(x, y, w, h)); + } + } +} diff --git a/LibationWinForms/Dialogs/AccountsDialog.cs b/LibationWinForms/Dialogs/AccountsDialog.cs index 0884bcfc..cfc5988e 100644 --- a/LibationWinForms/Dialogs/AccountsDialog.cs +++ b/LibationWinForms/Dialogs/AccountsDialog.cs @@ -1,21 +1,21 @@ -using System; +using AudibleApi; +using InternalUtilities; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; -using AudibleApi; -using InternalUtilities; namespace LibationWinForms.Dialogs { public partial class AccountsDialog : Form { - const string COL_Delete = nameof(DeleteAccount); - const string COL_LibraryScan = nameof(LibraryScan); - const string COL_AccountId = nameof(AccountId); - const string COL_AccountName = nameof(AccountName); - const string COL_Locale = nameof(Locale); + private const string COL_Delete = nameof(DeleteAccount); + private const string COL_LibraryScan = nameof(LibraryScan); + private const string COL_AccountId = nameof(AccountId); + private const string COL_AccountName = nameof(AccountName); + private const string COL_Locale = nameof(Locale); - Form1 _parent { get; } + private Form1 _parent { get; } public AccountsDialog(Form1 parent) { @@ -100,7 +100,7 @@ namespace LibationWinForms.Dialogs this.Close(); } - class AccountDto + private class AccountDto { public string AccountId { get; set; } public string AccountName { get; set; } diff --git a/LibationWinForms/Dialogs/AccountsDialog.resx b/LibationWinForms/Dialogs/AccountsDialog.resx index 18b23138..f1117452 100644 --- a/LibationWinForms/Dialogs/AccountsDialog.resx +++ b/LibationWinForms/Dialogs/AccountsDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/BookDetailsDialog.cs b/LibationWinForms/Dialogs/BookDetailsDialog.cs index 27c5ab21..04a5d29e 100644 --- a/LibationWinForms/Dialogs/BookDetailsDialog.cs +++ b/LibationWinForms/Dialogs/BookDetailsDialog.cs @@ -1,27 +1,28 @@ using System; +using System.Linq; using System.Windows.Forms; namespace LibationWinForms.Dialogs { - public partial class BookDetailsDialog : Form - { - public string NewTags { get; private set; } + public partial class BookDetailsDialog : Form + { + public string NewTags { get; private set; } - public BookDetailsDialog() - { - InitializeComponent(); - } - public BookDetailsDialog(string title, string rawTags) : this() - { - this.Text = $"Edit Tags - {title}"; + public BookDetailsDialog() + { + InitializeComponent(); + } + public BookDetailsDialog(string title, string rawTags) : this() + { + this.Text = $"Edit Tags - {title}"; - this.newTagsTb.Text = rawTags; - } + this.newTagsTb.Text = rawTags; + } - private void SaveBtn_Click(object sender, EventArgs e) - { - NewTags = this.newTagsTb.Text; - DialogResult = DialogResult.OK; - } - } + private void SaveBtn_Click(object sender, EventArgs e) + { + NewTags = this.newTagsTb.Text; + DialogResult = DialogResult.OK; + } + } } diff --git a/LibationWinForms/Dialogs/BookDetailsDialog.resx b/LibationWinForms/Dialogs/BookDetailsDialog.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/Dialogs/BookDetailsDialog.resx +++ b/LibationWinForms/Dialogs/BookDetailsDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs b/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs index 5684d485..e30f44c2 100644 --- a/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs +++ b/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; +using FileManager; +using System; using System.Linq; +using System.Collections.Generic; using System.Windows.Forms; -using Dinah.Core; -using FileManager; namespace LibationWinForms.Dialogs { @@ -58,7 +57,7 @@ namespace LibationWinForms.Dialogs = knownDir != Configuration.KnownDirectories.None // this could be a well known dir which isn't an option in this particular dropdown. This will always be true of LibationFiles && this.directorySelectControl.SelectDirectory(knownDir); - + customDirectoryRb.Checked = !isKnown; knownDirectoryRb.Checked = isKnown; this.customTb.Text = isKnown ? "" : customDir; diff --git a/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.resx b/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.resx index f298a7be..e8ae276d 100644 --- a/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.resx +++ b/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/Dialogs/DirectorySelectControl.cs b/LibationWinForms/Dialogs/DirectorySelectControl.cs index ab8def7a..639886ba 100644 --- a/LibationWinForms/Dialogs/DirectorySelectControl.cs +++ b/LibationWinForms/Dialogs/DirectorySelectControl.cs @@ -1,9 +1,9 @@ -using System; +using Dinah.Core; +using FileManager; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; -using Dinah.Core; -using FileManager; namespace LibationWinForms.Dialogs { @@ -13,7 +13,7 @@ namespace LibationWinForms.Dialogs { public string Description { get; } public Configuration.KnownDirectories Value { get; } - private DirectorySelectControl _parentControl; + private readonly DirectorySelectControl _parentControl; public string FullPath => _parentControl.AddSubDirectoryToPath(Configuration.GetKnownDirectoryPath(Value)); diff --git a/LibationWinForms/Dialogs/DirectorySelectControl.resx b/LibationWinForms/Dialogs/DirectorySelectControl.resx index f298a7be..e8ae276d 100644 --- a/LibationWinForms/Dialogs/DirectorySelectControl.resx +++ b/LibationWinForms/Dialogs/DirectorySelectControl.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/Dialogs/EditQuickFilters.cs b/LibationWinForms/Dialogs/EditQuickFilters.cs index abb183ce..999e9b2f 100644 --- a/LibationWinForms/Dialogs/EditQuickFilters.cs +++ b/LibationWinForms/Dialogs/EditQuickFilters.cs @@ -1,101 +1,100 @@ -using System; +using FileManager; +using System; using System.Linq; using System.Windows.Forms; -using FileManager; namespace LibationWinForms.Dialogs { public partial class EditQuickFilters : Form - { - const string BLACK_UP_POINTING_TRIANGLE = "\u25B2"; - const string BLACK_DOWN_POINTING_TRIANGLE = "\u25BC"; + { + private const string BLACK_UP_POINTING_TRIANGLE = "\u25B2"; + private const string BLACK_DOWN_POINTING_TRIANGLE = "\u25BC"; + private const string COL_Original = nameof(Original); + private const string COL_Delete = nameof(Delete); + private const string COL_Filter = nameof(Filter); + private const string COL_MoveUp = nameof(MoveUp); + private const string COL_MoveDown = nameof(MoveDown); - const string COL_Original = nameof(Original); - const string COL_Delete = nameof(Delete); - const string COL_Filter = nameof(Filter); - const string COL_MoveUp = nameof(MoveUp); - const string COL_MoveDown = nameof(MoveDown); + private Form1 _parent { get; } - Form1 _parent { get; } + public EditQuickFilters(Form1 parent) + { + _parent = parent; - public EditQuickFilters(Form1 parent) - { - _parent = parent; + InitializeComponent(); - InitializeComponent(); + dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; - dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; + populateGridValues(); + } - populateGridValues(); - } + private void populateGridValues() + { + var filters = QuickFilters.Filters; + if (!filters.Any()) + return; - private void populateGridValues() - { - var filters = QuickFilters.Filters; - if (!filters.Any()) - return; + foreach (var filter in filters) + dataGridView1.Rows.Add(filter, "X", filter, BLACK_UP_POINTING_TRIANGLE, BLACK_DOWN_POINTING_TRIANGLE); + } - foreach (var filter in filters) - dataGridView1.Rows.Add(filter, "X", filter, BLACK_UP_POINTING_TRIANGLE, BLACK_DOWN_POINTING_TRIANGLE); - } + private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) + { + e.Row.Cells[COL_Delete].Value = "X"; + e.Row.Cells[COL_MoveUp].Value = BLACK_UP_POINTING_TRIANGLE; + e.Row.Cells[COL_MoveDown].Value = BLACK_DOWN_POINTING_TRIANGLE; + } - private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) - { - e.Row.Cells[COL_Delete].Value = "X"; - e.Row.Cells[COL_MoveUp].Value = BLACK_UP_POINTING_TRIANGLE; - e.Row.Cells[COL_MoveDown].Value = BLACK_DOWN_POINTING_TRIANGLE; - } + private void saveBtn_Click(object sender, EventArgs e) + { + var list = dataGridView1.Rows + .OfType() + .Select(r => r.Cells[COL_Filter].Value?.ToString()) + .ToList(); + QuickFilters.ReplaceAll(list); - private void saveBtn_Click(object sender, EventArgs e) - { - var list = dataGridView1.Rows - .OfType() - .Select(r => r.Cells[COL_Filter].Value?.ToString()) - .ToList(); - QuickFilters.ReplaceAll(list); + _parent.UpdateFilterDropDown(); + this.DialogResult = DialogResult.OK; + this.Close(); + } - _parent.UpdateFilterDropDown(); - this.DialogResult = DialogResult.OK; - this.Close(); - } + private void cancelBtn_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.Cancel; + this.Close(); + } - private void cancelBtn_Click(object sender, EventArgs e) - { - this.DialogResult = DialogResult.Cancel; - this.Close(); - } + private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e) + { + var dgv = (DataGridView)sender; - private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e) - { - var dgv = (DataGridView)sender; - - var col = dgv.Columns[e.ColumnIndex]; - if (col is DataGridViewButtonColumn && e.RowIndex >= 0) - { - var row = dgv.Rows[e.RowIndex]; - switch (col.Name) - { - case COL_Delete: - // if final/edit row: do nothing - if (e.RowIndex < dgv.RowCount - 1) - dgv.Rows.Remove(row); - break; - case COL_MoveUp: - // if top: do nothing - if (e.RowIndex < 1) - break; - dgv.Rows.Remove(row); - dgv.Rows.Insert(e.RowIndex - 1, row); - break; - case COL_MoveDown: - // if final/edit row or bottom filter row: do nothing - if (e.RowIndex >= dgv.RowCount - 2) - break; - dgv.Rows.Remove(row); - dgv.Rows.Insert(e.RowIndex + 1, row); - break; - } - } - } - } + var col = dgv.Columns[e.ColumnIndex]; + if (col is DataGridViewButtonColumn && e.RowIndex >= 0) + { + var row = dgv.Rows[e.RowIndex]; + switch (col.Name) + { + case COL_Delete: + // if final/edit row: do nothing + if (e.RowIndex < dgv.RowCount - 1) + dgv.Rows.Remove(row); + break; + case COL_MoveUp: + // if top: do nothing + if (e.RowIndex < 1) + break; + dgv.Rows.Remove(row); + dgv.Rows.Insert(e.RowIndex - 1, row); + break; + case COL_MoveDown: + // if final/edit row or bottom filter row: do nothing + if (e.RowIndex >= dgv.RowCount - 2) + break; + dgv.Rows.Remove(row); + dgv.Rows.Insert(e.RowIndex + 1, row); + break; + } + } + } + } } diff --git a/LibationWinForms/Dialogs/EditQuickFilters.resx b/LibationWinForms/Dialogs/EditQuickFilters.resx index 714d166d..9c876821 100644 --- a/LibationWinForms/Dialogs/EditQuickFilters.resx +++ b/LibationWinForms/Dialogs/EditQuickFilters.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/IndexLibraryDialog.cs b/LibationWinForms/Dialogs/IndexLibraryDialog.cs index e534cc28..1adb538a 100644 --- a/LibationWinForms/Dialogs/IndexLibraryDialog.cs +++ b/LibationWinForms/Dialogs/IndexLibraryDialog.cs @@ -1,8 +1,9 @@ -using System; -using System.Windows.Forms; -using ApplicationServices; +using ApplicationServices; using InternalUtilities; using LibationWinForms.Login; +using System; +using System.Linq; +using System.Windows.Forms; namespace LibationWinForms.Dialogs { diff --git a/LibationWinForms/Dialogs/IndexLibraryDialog.resx b/LibationWinForms/Dialogs/IndexLibraryDialog.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/Dialogs/IndexLibraryDialog.resx +++ b/LibationWinForms/Dialogs/IndexLibraryDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/LibationFilesDialog.cs b/LibationWinForms/Dialogs/LibationFilesDialog.cs index c6c67383..f827beb8 100644 --- a/LibationWinForms/Dialogs/LibationFilesDialog.cs +++ b/LibationWinForms/Dialogs/LibationFilesDialog.cs @@ -1,6 +1,7 @@ -using System; +using FileManager; +using System; +using System.Linq; using System.Windows.Forms; -using FileManager; namespace LibationWinForms.Dialogs { diff --git a/LibationWinForms/Dialogs/LibationFilesDialog.resx b/LibationWinForms/Dialogs/LibationFilesDialog.resx index f298a7be..e8ae276d 100644 --- a/LibationWinForms/Dialogs/LibationFilesDialog.resx +++ b/LibationWinForms/Dialogs/LibationFilesDialog.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.cs b/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.cs index 0d08e58b..a98c8bda 100644 --- a/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.cs +++ b/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Windows.Forms; namespace LibationWinForms.Dialogs.Login diff --git a/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx b/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx index f298a7be..e8ae276d 100644 --- a/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx +++ b/LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/Dialogs/Login/AudibleLoginDialog.cs b/LibationWinForms/Dialogs/Login/AudibleLoginDialog.cs index 426bae71..2e366e88 100644 --- a/LibationWinForms/Dialogs/Login/AudibleLoginDialog.cs +++ b/LibationWinForms/Dialogs/Login/AudibleLoginDialog.cs @@ -1,7 +1,8 @@ -using System; -using System.Windows.Forms; -using Dinah.Core; +using Dinah.Core; using InternalUtilities; +using System; +using System.Linq; +using System.Windows.Forms; namespace LibationWinForms.Dialogs.Login { diff --git a/LibationWinForms/Dialogs/Login/AudibleLoginDialog.resx b/LibationWinForms/Dialogs/Login/AudibleLoginDialog.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/Dialogs/Login/AudibleLoginDialog.resx +++ b/LibationWinForms/Dialogs/Login/AudibleLoginDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/Login/CaptchaDialog.cs b/LibationWinForms/Dialogs/Login/CaptchaDialog.cs index e9b3becb..ad5264d4 100644 --- a/LibationWinForms/Dialogs/Login/CaptchaDialog.cs +++ b/LibationWinForms/Dialogs/Login/CaptchaDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Drawing; using System.IO; using System.Windows.Forms; diff --git a/LibationWinForms/Dialogs/Login/CaptchaDialog.resx b/LibationWinForms/Dialogs/Login/CaptchaDialog.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/Dialogs/Login/CaptchaDialog.resx +++ b/LibationWinForms/Dialogs/Login/CaptchaDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/Login/MfaDialog.cs b/LibationWinForms/Dialogs/Login/MfaDialog.cs index add77588..e369b367 100644 --- a/LibationWinForms/Dialogs/Login/MfaDialog.cs +++ b/LibationWinForms/Dialogs/Login/MfaDialog.cs @@ -1,11 +1,5 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; namespace LibationWinForms.Dialogs.Login @@ -14,7 +8,7 @@ namespace LibationWinForms.Dialogs.Login { private RadioButton[] radioButtons { get; } - AudibleApi.MfaConfig _mfaConfig { get; } + private AudibleApi.MfaConfig _mfaConfig { get; } public MfaDialog(AudibleApi.MfaConfig mfaConfig) { @@ -32,7 +26,8 @@ namespace LibationWinForms.Dialogs.Login setRadioButton(1, this.radioButton2); setRadioButton(2, this.radioButton3); - Serilog.Log.Logger.Information("{@DebugInfo}", new { + Serilog.Log.Logger.Information("{@DebugInfo}", new + { paramButtonCount = mfaConfig.Buttons.Count, visibleRadioButtonCount = radioButtons.Count(rb => rb.Visible) }); @@ -65,7 +60,8 @@ namespace LibationWinForms.Dialogs.Login { var selected = radioButtons.FirstOrDefault(rb => rb.Checked); - Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { + Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new + { rb1_visible = radioButton1.Visible, rb1_checked = radioButton1.Checked, diff --git a/LibationWinForms/Dialogs/Login/MfaDialog.resx b/LibationWinForms/Dialogs/Login/MfaDialog.resx index f298a7be..e8ae276d 100644 --- a/LibationWinForms/Dialogs/Login/MfaDialog.resx +++ b/LibationWinForms/Dialogs/Login/MfaDialog.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/Dialogs/Login/WinformResponder.cs b/LibationWinForms/Dialogs/Login/WinformResponder.cs index 28507f4d..cc73d713 100644 --- a/LibationWinForms/Dialogs/Login/WinformResponder.cs +++ b/LibationWinForms/Dialogs/Login/WinformResponder.cs @@ -1,7 +1,8 @@ -using System; -using AudibleApi; +using AudibleApi; using InternalUtilities; using LibationWinForms.Dialogs.Login; +using System; +using System.Linq; namespace LibationWinForms.Login { diff --git a/LibationWinForms/Dialogs/Login/_2faCodeDialog.cs b/LibationWinForms/Dialogs/Login/_2faCodeDialog.cs index 7634c271..4156468a 100644 --- a/LibationWinForms/Dialogs/Login/_2faCodeDialog.cs +++ b/LibationWinForms/Dialogs/Login/_2faCodeDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Windows.Forms; namespace LibationWinForms.Dialogs.Login diff --git a/LibationWinForms/Dialogs/Login/_2faCodeDialog.resx b/LibationWinForms/Dialogs/Login/_2faCodeDialog.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/Dialogs/Login/_2faCodeDialog.resx +++ b/LibationWinForms/Dialogs/Login/_2faCodeDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.cs b/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.cs index 14f2e76f..c4b44d92 100644 --- a/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.cs +++ b/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.cs @@ -1,7 +1,8 @@ -using System; +using Dinah.Core; +using System; +using System.Linq; using System.Drawing; using System.Windows.Forms; -using Dinah.Core; namespace LibationWinForms.Dialogs { diff --git a/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.resx b/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.resx index be1522cd..fe34710c 100644 --- a/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.resx +++ b/LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/Dialogs/RemoveBooksDialog.Designer.cs b/LibationWinForms/Dialogs/RemoveBooksDialog.Designer.cs new file mode 100644 index 00000000..5bf4e6c9 --- /dev/null +++ b/LibationWinForms/Dialogs/RemoveBooksDialog.Designer.cs @@ -0,0 +1,189 @@ + +namespace LibationWinForms.Dialogs +{ + partial class RemoveBooksDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); + this._dataGridView = new System.Windows.Forms.DataGridView(); + this.removeDataGridViewCheckBoxColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn(); + this.coverDataGridViewImageColumn = new System.Windows.Forms.DataGridViewImageColumn(); + this.titleDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.purchaseDateGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components); + this.btnRemoveBooks = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit(); + this.SuspendLayout(); + // + // _dataGridView + // + this._dataGridView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this._dataGridView.AutoGenerateColumns = false; + this._dataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this._dataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.removeDataGridViewCheckBoxColumn, + this.coverDataGridViewImageColumn, + this.titleDataGridViewTextBoxColumn, + this.authorsDataGridViewTextBoxColumn, + this.miscDataGridViewTextBoxColumn, + this.purchaseDateGridViewTextBoxColumn}); + this._dataGridView.DataSource = this.gridEntryBindingSource; + dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window; + dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText; + dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight; + dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText; + dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.True; + this._dataGridView.DefaultCellStyle = dataGridViewCellStyle2; + this._dataGridView.Location = new System.Drawing.Point(0, 0); + this._dataGridView.Name = "_dataGridView"; + this._dataGridView.RowHeadersVisible = false; + this._dataGridView.RowTemplate.Height = 82; + this._dataGridView.Size = new System.Drawing.Size(800, 409); + this._dataGridView.TabIndex = 0; + // + // removeDataGridViewCheckBoxColumn + // + this.removeDataGridViewCheckBoxColumn.DataPropertyName = "Remove"; + this.removeDataGridViewCheckBoxColumn.FalseValue = "False"; + this.removeDataGridViewCheckBoxColumn.Frozen = true; + this.removeDataGridViewCheckBoxColumn.HeaderText = "Remove"; + this.removeDataGridViewCheckBoxColumn.MinimumWidth = 60; + this.removeDataGridViewCheckBoxColumn.Name = "removeDataGridViewCheckBoxColumn"; + this.removeDataGridViewCheckBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.removeDataGridViewCheckBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic; + this.removeDataGridViewCheckBoxColumn.TrueValue = "True"; + this.removeDataGridViewCheckBoxColumn.Width = 60; + // + // coverDataGridViewImageColumn + // + this.coverDataGridViewImageColumn.DataPropertyName = "Cover"; + this.coverDataGridViewImageColumn.HeaderText = "Cover"; + this.coverDataGridViewImageColumn.MinimumWidth = 80; + this.coverDataGridViewImageColumn.Name = "coverDataGridViewImageColumn"; + this.coverDataGridViewImageColumn.ReadOnly = true; + this.coverDataGridViewImageColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.coverDataGridViewImageColumn.Width = 80; + // + // titleDataGridViewTextBoxColumn + // + this.titleDataGridViewTextBoxColumn.DataPropertyName = "Title"; + this.titleDataGridViewTextBoxColumn.HeaderText = "Title"; + this.titleDataGridViewTextBoxColumn.Name = "titleDataGridViewTextBoxColumn"; + this.titleDataGridViewTextBoxColumn.ReadOnly = true; + this.titleDataGridViewTextBoxColumn.Width = 200; + // + // authorsDataGridViewTextBoxColumn + // + this.authorsDataGridViewTextBoxColumn.DataPropertyName = "Authors"; + this.authorsDataGridViewTextBoxColumn.HeaderText = "Authors"; + this.authorsDataGridViewTextBoxColumn.Name = "authorsDataGridViewTextBoxColumn"; + this.authorsDataGridViewTextBoxColumn.ReadOnly = true; + // + // miscDataGridViewTextBoxColumn + // + this.miscDataGridViewTextBoxColumn.DataPropertyName = "Misc"; + this.miscDataGridViewTextBoxColumn.HeaderText = "Misc"; + this.miscDataGridViewTextBoxColumn.Name = "miscDataGridViewTextBoxColumn"; + this.miscDataGridViewTextBoxColumn.ReadOnly = true; + this.miscDataGridViewTextBoxColumn.Width = 150; + // + // purchaseDateGridViewTextBoxColumn + // + this.purchaseDateGridViewTextBoxColumn.DataPropertyName = "PurchaseDate"; + this.purchaseDateGridViewTextBoxColumn.HeaderText = "Purchase Date"; + this.purchaseDateGridViewTextBoxColumn.Name = "purchaseDateGridViewTextBoxColumn"; + this.purchaseDateGridViewTextBoxColumn.ReadOnly = true; + this.purchaseDateGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False; + // + // gridEntryBindingSource + // + this.gridEntryBindingSource.AllowNew = false; + this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.Dialogs.RemovableGridEntry); + // + // btnRemoveBooks + // + this.btnRemoveBooks.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnRemoveBooks.Location = new System.Drawing.Point(570, 419); + this.btnRemoveBooks.Name = "btnRemoveBooks"; + this.btnRemoveBooks.Size = new System.Drawing.Size(218, 23); + this.btnRemoveBooks.TabIndex = 1; + this.btnRemoveBooks.Text = "Remove Selected Books from Libation"; + this.btnRemoveBooks.UseVisualStyleBackColor = true; + this.btnRemoveBooks.Click += new System.EventHandler(this.btnRemoveBooks_Click); + // + // label1 + // + this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 423); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(178, 15); + this.label1.TabIndex = 2; + this.label1.Text = "{0} book{1} selected for removal."; + // + // RemoveBooksDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.label1); + this.Controls.Add(this.btnRemoveBooks); + this.Controls.Add(this._dataGridView); + this.Name = "RemoveBooksDialog"; + this.Text = "RemoveBooksDialog"; + this.Shown += new System.EventHandler(this.RemoveBooksDialog_Shown); + ((System.ComponentModel.ISupportInitialize)(this._dataGridView)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.DataGridView _dataGridView; + private System.Windows.Forms.BindingSource gridEntryBindingSource; + private System.Windows.Forms.Button btnRemoveBooks; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn; + private System.Windows.Forms.DataGridViewImageColumn coverDataGridViewImageColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn titleDataGridViewTextBoxColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn authorsDataGridViewTextBoxColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn miscDataGridViewTextBoxColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGridViewTextBoxColumn; + } +} \ No newline at end of file diff --git a/LibationWinForms/Dialogs/RemoveBooksDialog.cs b/LibationWinForms/Dialogs/RemoveBooksDialog.cs new file mode 100644 index 00000000..40457837 --- /dev/null +++ b/LibationWinForms/Dialogs/RemoveBooksDialog.cs @@ -0,0 +1,170 @@ +using ApplicationServices; +using DataLayer; +using InternalUtilities; +using LibationWinForms.Login; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Linq; +using System.Windows.Forms; + +namespace LibationWinForms.Dialogs +{ + public partial class RemoveBooksDialog : Form + { + public bool BooksRemoved { get; private set; } + + private Account[] _accounts { get; } + private readonly List _libraryBooks; + private readonly SortableBindingList2 _removableGridEntries; + private readonly string _labelFormat; + private int SelectedCount => SelectedEntries?.Count() ?? 0; + private IEnumerable SelectedEntries => _removableGridEntries?.Where(b => b.Remove); + + public RemoveBooksDialog(params Account[] accounts) + { + _libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking(); + _accounts = accounts; + + InitializeComponent(); + _labelFormat = label1.Text; + + _dataGridView.CellContentClick += (s, e) => _dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit); + _dataGridView.CellValueChanged += DataGridView1_CellValueChanged; + _dataGridView.BindingContextChanged += (s, e) => UpdateSelection(); + + var orderedGridEntries = _libraryBooks + .Select(lb => new RemovableGridEntry(lb)) + .OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate))) + .ToList(); + + _removableGridEntries = new SortableBindingList2(orderedGridEntries); + gridEntryBindingSource.DataSource = _removableGridEntries; + + _dataGridView.Enabled = false; + } + + private void DataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e) + { + if (e.ColumnIndex == 0) + UpdateSelection(); + } + + private async void RemoveBooksDialog_Shown(object sender, EventArgs e) + { + if (_accounts == null || _accounts.Length == 0) + return; + try + { + var rmovedBooks = await LibraryCommands.FindInactiveBooks((account) => new WinformResponder(account), _libraryBooks, _accounts); + + var removable = _removableGridEntries.Where(rge => rmovedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId)); + + if (!removable.Any()) + return; + + foreach (var r in removable) + r.Remove = true; + + UpdateSelection(); + } + catch (Exception ex) + { + MessageBoxAlertAdmin.Show( + "Error scanning library. You may still manually select books to remove from Libation's library.", + "Error scanning library", + ex); + } + finally + { + _dataGridView.Enabled = true; + } + } + + private void btnRemoveBooks_Click(object sender, EventArgs e) + { + var selectedBooks = SelectedEntries.ToList(); + + if (selectedBooks.Count == 0) return; + + string titles = string.Join("\r\n", selectedBooks.Select(rge => "-" + rge.Title)); + + string thisThese = selectedBooks.Count > 1 ? "these" : "this"; + string bookBooks = selectedBooks.Count > 1 ? "books" : "book"; + + var result = MessageBox.Show( + this, + $"Are you sure you want to remove {thisThese} {selectedBooks.Count} {bookBooks} from Libation's library?\r\n\r\n{titles}", + "Remove books from Libation?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question, + MessageBoxDefaultButton.Button1); + + if (result == DialogResult.Yes) + { + using var context = DbContexts.GetContext(); + + var libBooks = context.GetLibrary_Flat_NoTracking(); + + var removeLibraryBooks = libBooks.Where(lb => selectedBooks.Any(rge => rge.AudibleProductId == lb.Book.AudibleProductId)).ToList(); + + context.Library.RemoveRange(removeLibraryBooks); + context.SaveChanges(); + + foreach (var rEntry in selectedBooks) + _removableGridEntries.Remove(rEntry); + + BooksRemoved = removeLibraryBooks.Count > 0; + + UpdateSelection(); + } + } + private void UpdateSelection() + { + _dataGridView.Sort(_dataGridView.Columns[0], ListSortDirection.Descending); + var selectedCount = SelectedCount; + label1.Text = string.Format(_labelFormat, selectedCount, selectedCount != 1 ? "s" : string.Empty); + btnRemoveBooks.Enabled = selectedCount > 0; + } + } + + internal class RemovableGridEntry : GridEntry + { + private static readonly IComparer BoolComparer = new ObjectComparer(); + + private bool _remove = false; + public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { } + + public bool Remove + { + get + { + return _remove; + } + set + { + if (_remove != value) + { + _remove = value; + NotifyPropertyChanged(); + } + } + } + + public override object GetMemberValue(string memberName) + { + if (memberName == nameof(Remove)) + return Remove; + return base.GetMemberValue(memberName); + } + + public override IComparer GetMemberComparer(Type memberType) + { + if (memberType == typeof(bool)) + return BoolComparer; + return base.GetMemberComparer(memberType); + } + } +} diff --git a/LibationWinForms/Dialogs/RemoveBooksDialog.resx b/LibationWinForms/Dialogs/RemoveBooksDialog.resx new file mode 100644 index 00000000..5ffc920f --- /dev/null +++ b/LibationWinForms/Dialogs/RemoveBooksDialog.resx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/LibationWinForms/Dialogs/ScanAccountsDialog.cs b/LibationWinForms/Dialogs/ScanAccountsDialog.cs index 6f31ef06..c5453cfb 100644 --- a/LibationWinForms/Dialogs/ScanAccountsDialog.cs +++ b/LibationWinForms/Dialogs/ScanAccountsDialog.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; +using InternalUtilities; +using System; using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; using System.Windows.Forms; -using InternalUtilities; namespace LibationWinForms.Dialogs { @@ -12,7 +10,7 @@ namespace LibationWinForms.Dialogs { public List CheckedAccounts { get; } = new List(); - Form1 _parent { get; } + private Form1 _parent { get; } public ScanAccountsDialog(Form1 parent) { @@ -21,7 +19,7 @@ namespace LibationWinForms.Dialogs InitializeComponent(); } - class listItem + private class listItem { public Account Account { get; set; } public string Text { get; set; } diff --git a/LibationWinForms/Dialogs/ScanAccountsDialog.resx b/LibationWinForms/Dialogs/ScanAccountsDialog.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/Dialogs/ScanAccountsDialog.resx +++ b/LibationWinForms/Dialogs/ScanAccountsDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/SearchSyntaxDialog.cs b/LibationWinForms/Dialogs/SearchSyntaxDialog.cs index 1300a74a..ed897b11 100644 --- a/LibationWinForms/Dialogs/SearchSyntaxDialog.cs +++ b/LibationWinForms/Dialogs/SearchSyntaxDialog.cs @@ -1,20 +1,21 @@ using System; +using System.Linq; using System.Windows.Forms; namespace LibationWinForms.Dialogs { - public partial class SearchSyntaxDialog : Form - { - public SearchSyntaxDialog() - { - InitializeComponent(); + public partial class SearchSyntaxDialog : Form + { + public SearchSyntaxDialog() + { + InitializeComponent(); - label2.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields()); - label3.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields()); - label4.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields()); - label5.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields()); - } + label2.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields()); + label3.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields()); + label4.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields()); + label5.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields()); + } - private void CloseBtn_Click(object sender, EventArgs e) => this.Close(); - } + private void CloseBtn_Click(object sender, EventArgs e) => this.Close(); + } } diff --git a/LibationWinForms/Dialogs/SearchSyntaxDialog.resx b/LibationWinForms/Dialogs/SearchSyntaxDialog.resx index 1af7de15..e8ae276d 100644 --- a/LibationWinForms/Dialogs/SearchSyntaxDialog.resx +++ b/LibationWinForms/Dialogs/SearchSyntaxDialog.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Dialogs/SettingsDialog.cs b/LibationWinForms/Dialogs/SettingsDialog.cs index fee85e1d..371385db 100644 --- a/LibationWinForms/Dialogs/SettingsDialog.cs +++ b/LibationWinForms/Dialogs/SettingsDialog.cs @@ -1,15 +1,16 @@ -using System; +using Dinah.Core; +using FileManager; +using System; +using System.Linq; using System.IO; using System.Windows.Forms; -using Dinah.Core; -using FileManager; namespace LibationWinForms.Dialogs { public partial class SettingsDialog : Form { - Configuration config { get; } = Configuration.Instance; - Func desc { get; } = Configuration.GetDescription; + private Configuration config { get; } = Configuration.Instance; + private Func desc { get; } = Configuration.GetDescription; public SettingsDialog() => InitializeComponent(); @@ -57,13 +58,13 @@ namespace LibationWinForms.Dialogs inProgressSelectControl.SelectDirectory(config.InProgress); } - private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e) - { + private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e) + { convertLosslessRb.Enabled = allowLibationFixupCbox.Checked; convertLossyRb.Enabled = allowLibationFixupCbox.Checked; if (!allowLibationFixupCbox.Checked) - { + { convertLosslessRb.Checked = true; } } diff --git a/LibationWinForms/Dialogs/SettingsDialog.resx b/LibationWinForms/Dialogs/SettingsDialog.resx index f298a7be..e8ae276d 100644 --- a/LibationWinForms/Dialogs/SettingsDialog.resx +++ b/LibationWinForms/Dialogs/SettingsDialog.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/Dialogs/SetupDialog.cs b/LibationWinForms/Dialogs/SetupDialog.cs index 95882edd..f863da84 100644 --- a/LibationWinForms/Dialogs/SetupDialog.cs +++ b/LibationWinForms/Dialogs/SetupDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Windows.Forms; namespace LibationWinForms.Dialogs diff --git a/LibationWinForms/Dialogs/SetupDialog.resx b/LibationWinForms/Dialogs/SetupDialog.resx index 859984c0..9e9a5108 100644 --- a/LibationWinForms/Dialogs/SetupDialog.resx +++ b/LibationWinForms/Dialogs/SetupDialog.resx @@ -1,4 +1,5 @@ - + + diff --git a/LibationWinForms/EditTagsDataGridViewImageButtonColumn.cs b/LibationWinForms/EditTagsDataGridViewImageButtonColumn.cs new file mode 100644 index 00000000..e736b938 --- /dev/null +++ b/LibationWinForms/EditTagsDataGridViewImageButtonColumn.cs @@ -0,0 +1,39 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace LibationWinForms +{ + public class EditTagsDataGridViewImageButtonColumn : DataGridViewImageButtonColumn + { + protected override DataGridViewImageButtonCell NewCell() + => new EditTagsDataGridViewImageButtonCell(); + } + + internal class EditTagsDataGridViewImageButtonCell : DataGridViewImageButtonCell + { + private static readonly Image ButtonImage = Properties.Resources.edit_tags_25x25; + private static readonly Color HiddenForeColor = Color.LightGray; + + protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts) + { + var tagsString = (string)value; + + var foreColor = tagsString?.Contains("hidden") == true ? HiddenForeColor : DataGridView.DefaultCellStyle.ForeColor; + + if (DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor != foreColor) + { + DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor = foreColor; + } + + if (tagsString.Length == 0) + { + base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts); + DrawButtonImage(graphics, ButtonImage, cellBounds); + } + else + { + base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts); + } + } + } +} diff --git a/LibationWinForms/Form1.Designer.cs b/LibationWinForms/Form1.Designer.cs index 93c3914a..bd26f759 100644 --- a/LibationWinForms/Form1.Designer.cs +++ b/LibationWinForms/Form1.Designer.cs @@ -39,6 +39,9 @@ this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.removeLibraryBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.removeAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.removeSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -128,7 +131,8 @@ this.noAccountsYetAddAccountToolStripMenuItem, this.scanLibraryToolStripMenuItem, this.scanLibraryOfAllAccountsToolStripMenuItem, - this.scanLibraryOfSomeAccountsToolStripMenuItem}); + this.scanLibraryOfSomeAccountsToolStripMenuItem, + this.removeLibraryBooksToolStripMenuItem}); this.importToolStripMenuItem.Name = "importToolStripMenuItem"; this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20); this.importToolStripMenuItem.Text = "&Import"; @@ -161,6 +165,29 @@ this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts..."; this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click); // + // removeLibraryBooksToolStripMenuItem + // + this.removeLibraryBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.removeAllAccountsToolStripMenuItem, + this.removeSomeAccountsToolStripMenuItem}); + this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem"; + this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22); + this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books"; + // + // removeAllAccountsToolStripMenuItem + // + this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem"; + this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22); + this.removeAllAccountsToolStripMenuItem.Text = "All Accounts"; + this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click); + // + // removeSomeAccountsToolStripMenuItem + // + this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem"; + this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22); + this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts"; + this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click); + // // liberateToolStripMenuItem // this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -368,5 +395,8 @@ private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem convertAllM4bToMp3ToolStripMenuItem; - } + private System.Windows.Forms.ToolStripMenuItem removeLibraryBooksToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem removeAllAccountsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem removeSomeAccountsToolStripMenuItem; + } } diff --git a/LibationWinForms/Form1.cs b/LibationWinForms/Form1.cs index 8acabf08..7caaf420 100644 --- a/LibationWinForms/Form1.cs +++ b/LibationWinForms/Form1.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Windows.Forms; -using ApplicationServices; +using ApplicationServices; using DataLayer; using Dinah.Core; using Dinah.Core.Drawing; @@ -11,332 +6,377 @@ using Dinah.Core.Windows.Forms; using FileManager; using InternalUtilities; using LibationWinForms.Dialogs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; namespace LibationWinForms { - public partial class Form1 : Form - { - private string backupsCountsLbl_Format { get; } - private string pdfsCountsLbl_Format { get; } + public partial class Form1 : Form + { + private string backupsCountsLbl_Format { get; } + private string pdfsCountsLbl_Format { get; } private string visibleCountLbl_Format { get; } private string beginBookBackupsToolStripMenuItem_format { get; } private string beginPdfBackupsToolStripMenuItem_format { get; } public Form1() - { - InitializeComponent(); + { + InitializeComponent(); - // back up string formats - backupsCountsLbl_Format = backupsCountsLbl.Text; - pdfsCountsLbl_Format = pdfsCountsLbl.Text; - visibleCountLbl_Format = visibleCountLbl.Text; + // back up string formats + backupsCountsLbl_Format = backupsCountsLbl.Text; + pdfsCountsLbl_Format = pdfsCountsLbl.Text; + visibleCountLbl_Format = visibleCountLbl.Text; beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text; - beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text; + beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text; - // after backing up formats: can set default/temp visible text - backupsCountsLbl.Text = "[Calculating backed up book quantities]"; - pdfsCountsLbl.Text = "[Calculating backed up PDFs]"; + // after backing up formats: can set default/temp visible text + backupsCountsLbl.Text = "[Calculating backed up book quantities]"; + pdfsCountsLbl.Text = "[Calculating backed up PDFs]"; setVisibleCount(null, 0); - if (this.DesignMode) - return; + if (this.DesignMode) + return; - // independent UI updates - this.Load += setBackupCountsAsync; - this.Load += (_, __) => RestoreSizeAndLocation(); - this.Load += (_, __) => RefreshImportMenu(); + // independent UI updates + this.Load += setBackupCountsAsync; + this.Load += (_, __) => RestoreSizeAndLocation(); + this.Load += (_, __) => RefreshImportMenu(); - // start background service - this.Load += (_, __) => startBackgroundImageDownloader(); - } + var format = System.Drawing.Imaging.ImageFormat.Jpeg; + PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format)); + PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format)); + PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format)); + } - private static void startBackgroundImageDownloader() - { - // load default/missing cover images. this will also initiate the background image downloader - var format = System.Drawing.Imaging.ImageFormat.Jpeg; - PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format)); - PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format)); - PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format)); - } - - private void Form1_Load(object sender, EventArgs e) + private void Form1_Load(object sender, EventArgs e) { - if (this.DesignMode) - return; + if (this.DesignMode) + return; - reloadGrid(); - - // also applies filter. ONLY call AFTER loading grid - loadInitialQuickFilterState(); - } + reloadGrid(); - private void Form1_FormClosing(object sender, FormClosingEventArgs e) - { - SaveSizeAndLocation(); - } + // also applies filter. ONLY call AFTER loading grid + loadInitialQuickFilterState(); + + } + + private void Form1_FormClosing(object sender, FormClosingEventArgs e) + { + SaveSizeAndLocation(); + } private void RestoreSizeAndLocation() { - var config = Configuration.Instance; + var config = Configuration.Instance; - var width = config.MainFormWidth; - var height = config.MainFormHeight; + var width = config.MainFormWidth; + var height = config.MainFormHeight; - // too small -- something must have gone wrong. use defaults - if (width < 25 || height < 25) - { - width = 1023; - height = 578; - } + // too small -- something must have gone wrong. use defaults + if (width < 25 || height < 25) + { + width = 1023; + height = 578; + } - // Fit to the current screen size in case the screen resolution changed since the size was last persisted - if (width > Screen.PrimaryScreen.WorkingArea.Width) - width = Screen.PrimaryScreen.WorkingArea.Width; - if (height > Screen.PrimaryScreen.WorkingArea.Height) - height = Screen.PrimaryScreen.WorkingArea.Height; + // Fit to the current screen size in case the screen resolution changed since the size was last persisted + if (width > Screen.PrimaryScreen.WorkingArea.Width) + width = Screen.PrimaryScreen.WorkingArea.Width; + if (height > Screen.PrimaryScreen.WorkingArea.Height) + height = Screen.PrimaryScreen.WorkingArea.Height; - var x = config.MainFormX; - var y = config.MainFormY; + var x = config.MainFormX; + var y = config.MainFormY; - var rect = new System.Drawing.Rectangle(x, y, width, height); + var rect = new System.Drawing.Rectangle(x, y, width, height); - // is proposed rect on a screen? - if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect))) - { - this.StartPosition = FormStartPosition.Manual; - this.DesktopBounds = rect; - } - else - { - this.StartPosition = FormStartPosition.WindowsDefaultLocation; - this.Size = rect.Size; - } + // is proposed rect on a screen? + if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect))) + { + this.StartPosition = FormStartPosition.Manual; + this.DesktopBounds = rect; + } + else + { + this.StartPosition = FormStartPosition.WindowsDefaultLocation; + this.Size = rect.Size; + } - // FINAL: for Maximized: start normal state, set size and location, THEN set max state - this.WindowState = config.MainFormIsMaximized ? FormWindowState.Maximized : FormWindowState.Normal; - } + // FINAL: for Maximized: start normal state, set size and location, THEN set max state + this.WindowState = config.MainFormIsMaximized ? FormWindowState.Maximized : FormWindowState.Normal; + } - private void SaveSizeAndLocation() - { - System.Drawing.Point location; - System.Drawing.Size size; + private void SaveSizeAndLocation() + { + System.Drawing.Point location; + System.Drawing.Size size; - // save location and size if the state is normal - if (this.WindowState == FormWindowState.Normal) - { - location = this.Location; - size = this.Size; - } - else - { - // save the RestoreBounds if the form is minimized or maximized - location = this.RestoreBounds.Location; - size = this.RestoreBounds.Size; - } - - var config = Configuration.Instance; + // save location and size if the state is normal + if (this.WindowState == FormWindowState.Normal) + { + location = this.Location; + size = this.Size; + } + else + { + // save the RestoreBounds if the form is minimized or maximized + location = this.RestoreBounds.Location; + size = this.RestoreBounds.Size; + } - config.MainFormX = location.X; - config.MainFormY = location.Y; + var config = Configuration.Instance; - config.MainFormWidth = size.Width; - config.MainFormHeight = size.Height; + config.MainFormX = location.X; + config.MainFormY = location.Y; - config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized; - } + config.MainFormWidth = size.Width; + config.MainFormHeight = size.Height; - #region reload grid - bool isProcessingGridSelect = false; - private void reloadGrid() - { - // suppressed filter while init'ing UI - var prev_isProcessingGridSelect = isProcessingGridSelect; - isProcessingGridSelect = true; - setGrid(); - isProcessingGridSelect = prev_isProcessingGridSelect; + config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized; + } - // UI init complete. now we can apply filter - doFilter(lastGoodFilter); - } + #region reload grid + private bool isProcessingGridSelect = false; + private void reloadGrid() + { + // suppressed filter while init'ing UI + var prev_isProcessingGridSelect = isProcessingGridSelect; + isProcessingGridSelect = true; + setGrid(); + isProcessingGridSelect = prev_isProcessingGridSelect; - ProductsGrid currProductsGrid; - private void setGrid() - { - SuspendLayout(); - { - if (currProductsGrid != null) - { - gridPanel.Controls.Remove(currProductsGrid); - currProductsGrid.VisibleCountChanged -= setVisibleCount; - currProductsGrid.BackupCountsChanged -= setBackupCountsAsync; - currProductsGrid.Dispose(); - } + // UI init complete. now we can apply filter + doFilter(lastGoodFilter); + } - currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill }; - currProductsGrid.VisibleCountChanged += setVisibleCount; - currProductsGrid.BackupCountsChanged += setBackupCountsAsync; - gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid)); - currProductsGrid.Display(); - } - ResumeLayout(); - } - #endregion + private ProductsGrid currProductsGrid; + private void setGrid() + { + SuspendLayout(); + { + if (currProductsGrid != null) + { + gridPanel.Controls.Remove(currProductsGrid); + currProductsGrid.VisibleCountChanged -= setVisibleCount; + currProductsGrid.BackupCountsChanged -= setBackupCountsAsync; + currProductsGrid.Dispose(); + } - #region bottom: qty books visible - private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty); - #endregion + currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill }; + currProductsGrid.VisibleCountChanged += setVisibleCount; + currProductsGrid.BackupCountsChanged += setBackupCountsAsync; + gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid)); + currProductsGrid.Display(); + } + ResumeLayout(); + } + #endregion - #region bottom: backup counts - private async void setBackupCountsAsync(object _, object __) - { - LibraryCommands.LibraryStats libraryStats = null; - await Task.Run(() => libraryStats = LibraryCommands.GetCounts()); + #region bottom: qty books visible + private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty); + #endregion - setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress); - setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded); - } - private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress) - { - // enable/disable export - var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress); - exportLibraryToolStripMenuItem.Enabled = hasResults; + #region bottom: backup counts + private async void setBackupCountsAsync(object _, object __) + { + LibraryCommands.LibraryStats libraryStats = null; + await Task.Run(() => libraryStats = LibraryCommands.GetCounts()); - // update bottom numbers - var pending = booksNoProgress + booksDownloadedOnly; - var statusStripText - = !hasResults ? "No books. Begin by importing your library" - : pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp) - : $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up"; + setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress); + setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded); + } + private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress) + { + // enable/disable export + var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress); + exportLibraryToolStripMenuItem.Enabled = hasResults; - // update menu item - var menuItemText - = pending > 0 - ? $"{pending} remaining" - : "All books have been liberated"; + // update bottom numbers + var pending = booksNoProgress + booksDownloadedOnly; + var statusStripText + = !hasResults ? "No books. Begin by importing your library" + : pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp) + : $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up"; - // update UI - statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText); - menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0); - menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText)); - } - private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded) - { - // update bottom numbers - var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded); - var statusStripText - = !hasResults ? "" - : pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded) - : $"| All {pdfsDownloaded} PDFs downloaded"; + // update menu item + var menuItemText + = pending > 0 + ? $"{pending} remaining" + : "All books have been liberated"; - // update menu item - var menuItemText - = pdfsNotDownloaded > 0 - ? $"{pdfsNotDownloaded} remaining" - : "All PDFs have been downloaded"; + // update UI + statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText); + menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0); + menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText)); + } + private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded) + { + // update bottom numbers + var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded); + var statusStripText + = !hasResults ? "" + : pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded) + : $"| All {pdfsDownloaded} PDFs downloaded"; - // update UI - statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText); - menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0); - menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText)); - } - #endregion + // update menu item + var menuItemText + = pdfsNotDownloaded > 0 + ? $"{pdfsNotDownloaded} remaining" + : "All PDFs have been downloaded"; - #region filter - private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog(); + // update UI + statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText); + menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0); + menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText)); + } + #endregion - private void AddFilterBtn_Click(object sender, EventArgs e) - { - QuickFilters.Add(this.filterSearchTb.Text); - UpdateFilterDropDown(); - } + #region filter + private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog(); - private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e) - { - if (e.KeyChar == (char)Keys.Return) - { - doFilter(); + private void AddFilterBtn_Click(object sender, EventArgs e) + { + QuickFilters.Add(this.filterSearchTb.Text); + UpdateFilterDropDown(); + } - // silence the 'ding' - e.Handled = true; - } - } - private void filterBtn_Click(object sender, EventArgs e) => doFilter(); + private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e) + { + if (e.KeyChar == (char)Keys.Return) + { + doFilter(); - string lastGoodFilter = ""; - private void doFilter(string filterString) - { - this.filterSearchTb.Text = filterString; - doFilter(); - } - private void doFilter() - { - if (isProcessingGridSelect || currProductsGrid == null) - return; + // silence the 'ding' + e.Handled = true; + } + } + private void filterBtn_Click(object sender, EventArgs e) => doFilter(); - try - { - currProductsGrid.Filter(filterSearchTb.Text); - lastGoodFilter = filterSearchTb.Text; - } - catch (Exception ex) - { - MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error); + private string lastGoodFilter = ""; + private void doFilter(string filterString) + { + this.filterSearchTb.Text = filterString; + doFilter(); + } + private void doFilter() + { + if (isProcessingGridSelect || currProductsGrid == null) + return; - // re-apply last good filter - doFilter(lastGoodFilter); - } - } - #endregion + try + { + currProductsGrid.Filter(filterSearchTb.Text); + lastGoodFilter = filterSearchTb.Text; + } + catch (Exception ex) + { + MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error); - #region Import menu - public void RefreshImportMenu() - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var count = persister.AccountsSettings.Accounts.Count; + // re-apply last good filter + doFilter(lastGoodFilter); + } + } + #endregion - noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0; - scanLibraryToolStripMenuItem.Visible = count == 1; - scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1; - scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1; - } + #region Import menu + public void RefreshImportMenu() + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var count = persister.AccountsSettings.Accounts.Count; - private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e) - { - MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account"); - new AccountsDialog(this).ShowDialog(); - } + noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0; + scanLibraryToolStripMenuItem.Visible = count == 1; + scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1; + scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1; - private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault(); - scanLibraries(firstAccount); - } + removeLibraryBooksToolStripMenuItem.Visible = count != 0; - private void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var allAccounts = persister.AccountsSettings.GetAll(); - scanLibraries(allAccounts); - } + if (count == 1) + { + removeLibraryBooksToolStripMenuItem.Click += removeThisAccountToolStripMenuItem_Click; + } - private void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e) - { - using var scanAccountsDialog = new ScanAccountsDialog(this); + removeSomeAccountsToolStripMenuItem.Visible = count > 1; + removeAllAccountsToolStripMenuItem.Visible = count > 1; + } - if (scanAccountsDialog.ShowDialog() != DialogResult.OK) - return; + private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e) + { + MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account"); + new AccountsDialog(this).ShowDialog(); + } - if (!scanAccountsDialog.CheckedAccounts.Any()) - return; + private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault(); + scanLibraries(firstAccount); + } - scanLibraries(scanAccountsDialog.CheckedAccounts); - } + private void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var allAccounts = persister.AccountsSettings.GetAll(); + scanLibraries(allAccounts); + } - private void scanLibraries(IEnumerable accounts) => scanLibraries(accounts.ToArray()); - private void scanLibraries(params Account[] accounts) + private void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var scanAccountsDialog = new ScanAccountsDialog(this); + + if (scanAccountsDialog.ShowDialog() != DialogResult.OK) + return; + + if (!scanAccountsDialog.CheckedAccounts.Any()) + return; + + scanLibraries(scanAccountsDialog.CheckedAccounts); + } + + private void removeThisAccountToolStripMenuItem_Click(object sender, EventArgs e) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault(); + scanLibrariesRemovedBooks(firstAccount); + } + + private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var allAccounts = persister.AccountsSettings.GetAll(); + scanLibrariesRemovedBooks(allAccounts.ToArray()); + } + + private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var scanAccountsDialog = new ScanAccountsDialog(this); + + if (scanAccountsDialog.ShowDialog() != DialogResult.OK) + return; + + if (!scanAccountsDialog.CheckedAccounts.Any()) + return; + + scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray()); + } + + private void scanLibrariesRemovedBooks(params Account[] accounts) + { + using var dialog = new RemoveBooksDialog(accounts); + dialog.ShowDialog(); + + if (dialog.BooksRemoved) + reloadGrid(); + } + + private void scanLibraries(IEnumerable accounts) => scanLibraries(accounts.ToArray()); + private void scanLibraries(params Account[] accounts) { using var dialog = new IndexLibraryDialog(accounts); dialog.ShowDialog(); @@ -348,112 +388,113 @@ namespace LibationWinForms if (totalProcessed > 0) reloadGrid(); - } - #endregion + } + #endregion - #region liberate menu - private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow); + #region liberate menu + private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e) + => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow); - private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow); + private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) + => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow); - private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e) - => await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync(); + 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 + private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId); + #endregion - #region Export menu - private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e) - { - try - { - var saveFileDialog = new SaveFileDialog - { - Title = "Where to export Library", - Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*" - }; + #region Export menu + private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e) + { + try + { + var saveFileDialog = new SaveFileDialog + { + Title = "Where to export Library", + Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*" + }; - if (saveFileDialog.ShowDialog() != DialogResult.OK) - return; + if (saveFileDialog.ShowDialog() != DialogResult.OK) + return; - // FilterIndex is 1-based, NOT 0-based - switch (saveFileDialog.FilterIndex) - { - case 1: // xlsx - default: - LibraryExporter.ToXlsx(saveFileDialog.FileName); - break; - case 2: // csv - LibraryExporter.ToCsv(saveFileDialog.FileName); - break; - case 3: // json - LibraryExporter.ToJson(saveFileDialog.FileName); - break; - } + // FilterIndex is 1-based, NOT 0-based + switch (saveFileDialog.FilterIndex) + { + case 1: // xlsx + default: + LibraryExporter.ToXlsx(saveFileDialog.FileName); + break; + case 2: // csv + LibraryExporter.ToCsv(saveFileDialog.FileName); + break; + case 3: // json + LibraryExporter.ToJson(saveFileDialog.FileName); + break; + } - MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName); - } - catch (Exception ex) - { - MessageBoxAlertAdmin.Show("Error attempting to export your library.", "Error exporting", ex); - } - } - #endregion + MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName); + } + catch (Exception ex) + { + MessageBoxAlertAdmin.Show("Error attempting to export your library.", "Error exporting", ex); + } + } + #endregion - #region quick filters menu - private void loadInitialQuickFilterState() - { - // set inital state. do once only - firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault; + #region quick filters menu + private void loadInitialQuickFilterState() + { + // set inital state. do once only + firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault; - // load default filter. do once only - if (QuickFilters.UseDefault) - doFilter(QuickFilters.Filters.FirstOrDefault()); + // load default filter. do once only + if (QuickFilters.UseDefault) + doFilter(QuickFilters.Filters.FirstOrDefault()); - // do after every save - UpdateFilterDropDown(); - } + // do after every save + UpdateFilterDropDown(); + } - private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e) - { - firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked; - QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked; - } + private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e) + { + firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked; + QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked; + } - object quickFilterTag { get; } = new object(); - public void UpdateFilterDropDown() - { - // remove old - for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--) - { - var menuItem = quickFiltersToolStripMenuItem.DropDownItems[i]; - if (menuItem.Tag == quickFilterTag) - quickFiltersToolStripMenuItem.DropDownItems.Remove(menuItem); - } + private object quickFilterTag { get; } = new object(); + public void UpdateFilterDropDown() + { + // remove old + for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--) + { + var menuItem = quickFiltersToolStripMenuItem.DropDownItems[i]; + if (menuItem.Tag == quickFilterTag) + quickFiltersToolStripMenuItem.DropDownItems.Remove(menuItem); + } - // re-populate - var index = 0; - foreach (var filter in QuickFilters.Filters) - { - var menuItem = new ToolStripMenuItem - { - Tag = quickFilterTag, - Text = $"&{++index}: {filter}" - }; - menuItem.Click += (_, __) => doFilter(filter); - quickFiltersToolStripMenuItem.DropDownItems.Add(menuItem); - } - } + // re-populate + var index = 0; + foreach (var filter in QuickFilters.Filters) + { + var menuItem = new ToolStripMenuItem + { + Tag = quickFilterTag, + Text = $"&{++index}: {filter}" + }; + menuItem.Click += (_, __) => doFilter(filter); + quickFiltersToolStripMenuItem.DropDownItems.Add(menuItem); + } + } - private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog(); - #endregion + private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog(); + #endregion - #region settings menu - private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog(); + #region settings menu + private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog(); - private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog(); - #endregion - } + private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog(); + #endregion + + } } diff --git a/LibationWinForms/GridEntry.cs b/LibationWinForms/GridEntry.cs index 5f24c447..032828b9 100644 --- a/LibationWinForms/GridEntry.cs +++ b/LibationWinForms/GridEntry.cs @@ -1,87 +1,142 @@ -using System; +using ApplicationServices; +using DataLayer; +using Dinah.Core.Drawing; +using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; -using ApplicationServices; -using DataLayer; namespace LibationWinForms { - internal class GridEntry + internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable { - private LibraryBook libraryBook { get; } - private Book book => libraryBook.Book; - - public Book GetBook() => book; - public LibraryBook GetLibraryBook() => libraryBook; - - public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook; - + #region implementation properties // hide from public fields from Data Source GUI with [Browsable(false)] [Browsable(false)] - public string AudibleProductId => book.AudibleProductId; + public string AudibleProductId => Book.AudibleProductId; [Browsable(false)] - public string Tags => book.UserDefinedItem.Tags; - [Browsable(false)] - public IEnumerable TagsEnumerated => book.UserDefinedItem.TagsEnumerated; - [Browsable(false)] - public string PictureId => book.PictureId; - [Browsable(false)] - public LiberatedState Liberated_Status => LibraryCommands.Liberated_Status(book); - [Browsable(false)] - public PdfState Pdf_Status => LibraryCommands.Pdf_Status(book); + public LibraryBook LibraryBook { get; } - // displayValues is what gets displayed - // the value that gets returned from the property is the cell's value - // this allows for the value to be sorted one way and displayed another - // eg: - // orig title: The Computer - // formatReplacement: The Computer - // value for sorting: Computer - private Dictionary displayValues { get; } = new Dictionary(); - public bool TryDisplayValue(string key, out string value) => displayValues.TryGetValue(key, out value); + #endregion - public Image Cover => - WindowsDesktopUtilities.WinAudibleImageServer.GetImage(book.PictureId, FileManager.PictureSize._80x80); + private Book Book => LibraryBook.Book; + private Image _cover; - public string Title + public GridEntry(LibraryBook libraryBook) { - get + LibraryBook = libraryBook; + _memberValues = CreateMemberValueDictionary(); + + //Get cover art. If it's default, subscribe to PictureCached { - displayValues[nameof(Title)] = book.Title; - return getSortName(book.Title); + (bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80)); + + if (isDefault) + FileManager.PictureStorage.PictureCached += PictureStorage_PictureCached; + + //Mutable property. Set the field so PropertyChanged isn't fired. + _cover = ImageReader.ToImage(picture); + } + + //Immutable properties + { + Title = Book.Title; + Series = Book.SeriesNames; + Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min"; + MyRating = ValueOrDefault(Book.UserDefinedItem.Rating?.ToStarString(), ""); + PurchaseDate = libraryBook.DateAdded.ToString("d"); + ProductRating = ValueOrDefault(Book.Rating?.ToStarString(), ""); + Authors = Book.AuthorNames; + Narrators = Book.NarratorNames; + Category = string.Join(" > ", Book.CategoriesNames); + Misc = GetMiscDisplay(libraryBook); + Description = GetDescriptionDisplay(Book); + } + + //DisplayTags and Liberate properties are live. + } + + private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e) + { + if (e.Definition.PictureId == Book.PictureId) + { + Cover = ImageReader.ToImage(e.Picture); + FileManager.PictureStorage.PictureCached -= PictureStorage_PictureCached; } } - public string Authors => book.AuthorNames; - public string Narrators => book.NarratorNames; + #region Data Source properties - public int Length + public Image Cover { get { - displayValues[nameof(Length)] - = book.LengthInMinutes == 0 - ? "" - : $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min"; - - return book.LengthInMinutes; + return _cover; } - } - - public string Series - { - get + private set { - displayValues[nameof(Series)] = book.SeriesNames; - return getSortName(book.SeriesNames); + _cover = value; + NotifyPropertyChanged(); } } - private static string[] sortPrefixIgnores { get; } = new[] { "the", "a", "an" }; - private static string getSortName(string unformattedName) + public string ProductRating { get; } + public string PurchaseDate { get; } + public string MyRating { get; } + public string Series { get; } + public string Title { get; } + public string Length { get; } + public string Authors { get; } + public string Narrators { get; } + 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)); + #endregion + + #region Data Sorting + + private Dictionary> _memberValues { get; } + + /// + /// Create getters for all member object values by name + /// + private Dictionary> CreateMemberValueDictionary() => new() + { + { nameof(Title), () => GetSortName(Book.Title) }, + { nameof(Series), () => GetSortName(Book.SeriesNames) }, + { nameof(Length), () => Book.LengthInMinutes }, + { nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore }, + { nameof(PurchaseDate), () => LibraryBook.DateAdded }, + { nameof(ProductRating), () => Book.Rating.FirstScore }, + { nameof(Authors), () => Authors }, + { nameof(Narrators), () => Narrators }, + { nameof(Description), () => Description }, + { nameof(Category), () => Category }, + { nameof(Misc), () => Misc }, + { nameof(DisplayTags), () => DisplayTags }, + { nameof(Liberate), () => Liberate.Item1 } + }; + + // Instantiate comparers for every exposed member object type. + private static readonly Dictionary _memberTypeComparers = new() + { + { typeof(string), new ObjectComparer() }, + { typeof(int), new ObjectComparer() }, + { typeof(float), new ObjectComparer() }, + { typeof(DateTime), new ObjectComparer() }, + { typeof(LiberatedState), new ObjectComparer() }, + }; + + public virtual object GetMemberValue(string memberName) => _memberValues[memberName](); + public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType]; + + private static readonly string[] _sortPrefixIgnores = { "the", "a", "an" }; + private static string GetSortName(string unformattedName) { var sortName = unformattedName .Replace("|", "") @@ -89,110 +144,93 @@ namespace LibationWinForms .ToLowerInvariant() .Trim(); - if (sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " "))) + if (_sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " "))) sortName = sortName.Substring(sortName.IndexOf(" ") + 1).TrimStart(); return sortName; } - private string descriptionCache = null; - public string Description - { - get - { - // HtmlAgilityPack is expensive. cache results - if (descriptionCache is null) - { - if (book.Description is null) - descriptionCache = ""; - else - { - var doc = new HtmlAgilityPack.HtmlDocument(); - doc.LoadHtml(book.Description); - var noHtml = doc.DocumentNode.InnerText; - descriptionCache - = noHtml.Length < 63 - ? noHtml - : noHtml.Substring(0, 60) + "..."; - } - } + #endregion - return descriptionCache; - } + #region Static library display functions + + public static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState 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"), + _ => 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 => ("", ""), + _ => throw new Exception("Unexpected PDF state") + }; + + var mouseoverText = libState + pdfState; + + if (liberatedStatus == LiberatedState.NotDownloaded || + liberatedStatus == LiberatedState.PartialDownload || + pdfStatus == PdfState.NotDownloaded) + mouseoverText += "\r\nClick to complete"; + + var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}"); + + return (mouseoverText, buttonImage); } - public string Category => string.Join(" > ", book.CategoriesNames); - - // star ratings retain numeric value but display star text. this is needed because just using star text doesn't sort correctly: - // - star - // - star star - // - star 1/2 - - public string Product_Rating + /// + /// This information should not change during lifetime, so call only once. + /// + private static string GetDescriptionDisplay(Book book) { - get - { - displayValues[nameof(Product_Rating)] = starString(book.Rating); - return firstScore(book.Rating); - } + var doc = new HtmlAgilityPack.HtmlDocument(); + doc.LoadHtml(book.Description); + var noHtml = doc.DocumentNode.InnerText; + return + noHtml.Length < 63 ? + noHtml : + noHtml.Substring(0, 60) + "..."; } - public string Purchase_Date + /// + /// This information should not change during lifetime, so call only once. + /// Maximum of 5 text rows will fit in 80-pixel row height. + /// + private static string GetMiscDisplay(LibraryBook libraryBook) { - get - { - displayValues[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d"); - return libraryBook.DateAdded.ToString("yyyy-MM-dd HH:mm:ss"); - } + var details = new List(); + + var locale = ValueOrDefault(libraryBook.Book.Locale, "[unknown]"); + var acct = ValueOrDefault(libraryBook.Account, "[unknown]"); + + details.Add($"Account: {locale} - {acct}"); + + if (libraryBook.Book.HasPdf) + details.Add("Has PDF"); + if (libraryBook.Book.IsAbridged) + details.Add("Abridged"); + if (libraryBook.Book.DatePublished.HasValue) + details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}"); + // this goes last since it's most likely to have a line-break + if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher)) + details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}"); + + if (!details.Any()) + return "[details not imported]"; + + return string.Join("\r\n", details); } - public string My_Rating - { - get - { - displayValues[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating); - return firstScore(book.UserDefinedItem.Rating); - } - } + //Maybe add to Dinah StringExtensions? + private static string ValueOrDefault(string value, string defaultValue) + => string.IsNullOrWhiteSpace(value) ? defaultValue : value; - private string starString(Rating rating) - => (rating?.FirstScore != null && rating?.FirstScore > 0f) - ? rating?.ToStarString() - : ""; - private string firstScore(Rating rating) => rating?.FirstScore.ToString("0.0"); - - // max 5 text rows - public string Misc - { - get - { - var details = new List(); - - var locale - = string.IsNullOrWhiteSpace(book.Locale) - ? "[unknown]" - : book.Locale; - var acct - = string.IsNullOrWhiteSpace(libraryBook.Account) - ? "[unknown]" - : libraryBook.Account; - details.Add($"Account: {locale} - {acct}"); - - if (book.HasPdf) - details.Add("Has PDF"); - if (book.IsAbridged) - details.Add("Abridged"); - if (book.DatePublished.HasValue) - details.Add($"Date pub'd: {book.DatePublished.Value:MM/dd/yyyy}"); - // this goes last since it's most likely to have a line-break - if (!string.IsNullOrWhiteSpace(book.Publisher)) - details.Add($"Pub: {book.Publisher.Trim()}"); - - if (!details.Any()) - return "[details not imported]"; - - return string.Join("\r\n", details); - } - } - } + #endregion + } } diff --git a/LibationWinForms/IMemberComparable.cs b/LibationWinForms/IMemberComparable.cs new file mode 100644 index 00000000..bbc6471e --- /dev/null +++ b/LibationWinForms/IMemberComparable.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections; + +namespace LibationWinForms +{ + internal interface IMemberComparable + { + IComparer GetMemberComparer(Type memberType); + object GetMemberValue(string memberName); + } +} diff --git a/LibationWinForms/LibationWinForms.csproj b/LibationWinForms/LibationWinForms.csproj index 6ee0b7db..343425c6 100644 --- a/LibationWinForms/LibationWinForms.csproj +++ b/LibationWinForms/LibationWinForms.csproj @@ -1,4 +1,5 @@ - + + Library @@ -12,8 +13,8 @@ + - diff --git a/LibationWinForms/LiberateDataGridViewImageButtonColumn.cs b/LibationWinForms/LiberateDataGridViewImageButtonColumn.cs new file mode 100644 index 00000000..4802fa96 --- /dev/null +++ b/LibationWinForms/LiberateDataGridViewImageButtonColumn.cs @@ -0,0 +1,31 @@ +using ApplicationServices; +using System; +using System.Drawing; +using System.Windows.Forms; +using System.Linq; + +namespace LibationWinForms +{ + public class LiberateDataGridViewImageButtonColumn : DataGridViewImageButtonColumn + { + protected override DataGridViewImageButtonCell NewCell() + => new LiberateDataGridViewImageButtonCell(); + } + + internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell + { + protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts) + { + base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts); + + if (value is (LiberatedState liberatedState, PdfState pdfState)) + { + (string mouseoverText, Bitmap buttonImage) = GridEntry.GetLiberateDisplay(liberatedState, pdfState); + + DrawButtonImage(graphics, buttonImage, cellBounds); + + ToolTipText = mouseoverText; + } + } + } +} diff --git a/LibationWinForms/MemberComparer.cs b/LibationWinForms/MemberComparer.cs new file mode 100644 index 00000000..1bca2c82 --- /dev/null +++ b/LibationWinForms/MemberComparer.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace LibationWinForms +{ + internal class MemberComparer : IComparer where T : IMemberComparable + { + public ListSortDirection Direction { get; set; } = ListSortDirection.Ascending; + public string PropertyName { get; set; } + + public int Compare(T x, T y) + { + var val1 = x.GetMemberValue(PropertyName); + var val2 = y.GetMemberValue(PropertyName); + + return DirMult * x.GetMemberComparer(val1.GetType()).Compare(val1, val2); + } + + private int DirMult => Direction == ListSortDirection.Descending ? -1 : 1; + } +} diff --git a/LibationWinForms/MessageBoxAlertAdmin.cs b/LibationWinForms/MessageBoxAlertAdmin.cs index 8e97f09c..a1423a7e 100644 --- a/LibationWinForms/MessageBoxAlertAdmin.cs +++ b/LibationWinForms/MessageBoxAlertAdmin.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using LibationWinForms.Dialogs; +using System; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using LibationWinForms.Dialogs; namespace LibationWinForms { diff --git a/LibationWinForms/MessageBoxWarnIfVerboseLogging.cs b/LibationWinForms/MessageBoxWarnIfVerboseLogging.cs index 7b33a22c..0f47ef3c 100644 --- a/LibationWinForms/MessageBoxWarnIfVerboseLogging.cs +++ b/LibationWinForms/MessageBoxWarnIfVerboseLogging.cs @@ -1,7 +1,8 @@ using System; -using System.Windows.Forms; +using System.Linq; using Dinah.Core.Logging; using Serilog; +using System.Windows.Forms; namespace LibationWinForms { @@ -16,7 +17,7 @@ Warning: verbose logging is enabled. This should be used for debugging only. It creates many more logs and debug files, neither of which are as -strictly anonomous. +strictly anonymous. When you are finished debugging, it's highly recommended to set your debug MinimumLevel to Information and restart diff --git a/LibationWinForms/ObjectComparer[T].cs b/LibationWinForms/ObjectComparer[T].cs new file mode 100644 index 00000000..5a02b215 --- /dev/null +++ b/LibationWinForms/ObjectComparer[T].cs @@ -0,0 +1,10 @@ +using System; +using System.Collections; + +namespace LibationWinForms +{ + internal class ObjectComparer : IComparer where T : IComparable + { + public int Compare(object x, object y) => ((T)x).CompareTo(y); + } +} diff --git a/LibationWinForms/ProductsGrid.Designer.cs b/LibationWinForms/ProductsGrid.Designer.cs index a81c8abf..529a7783 100644 --- a/LibationWinForms/ProductsGrid.Designer.cs +++ b/LibationWinForms/ProductsGrid.Designer.cs @@ -28,146 +28,195 @@ /// private void InitializeComponent() { - this.components = new System.ComponentModel.Container(); - this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components); - this.gridEntryDataGridView = new System.Windows.Forms.DataGridView(); - this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn(); - this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - ((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit(); - this.SuspendLayout(); - // - // gridEntryBindingSource - // - this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry); - // - // gridEntryDataGridView - // - this.gridEntryDataGridView.AutoGenerateColumns = false; - this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { - this.dataGridViewImageColumn1, - this.dataGridViewTextBoxColumn1, - this.dataGridViewTextBoxColumn2, - this.dataGridViewTextBoxColumn3, - this.dataGridViewTextBoxColumn4, - this.dataGridViewTextBoxColumn5, - this.dataGridViewTextBoxColumn6, - this.dataGridViewTextBoxColumn7, - this.dataGridViewTextBoxColumn8, - this.dataGridViewTextBoxColumn9, - this.dataGridViewTextBoxColumn10, - this.dataGridViewTextBoxColumn11}); - this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource; - this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58); - this.gridEntryDataGridView.Name = "gridEntryDataGridView"; - this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220); - this.gridEntryDataGridView.TabIndex = 0; - // - // dataGridViewImageColumn1 - // - this.dataGridViewImageColumn1.DataPropertyName = "Cover"; - this.dataGridViewImageColumn1.HeaderText = "Cover"; - this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1"; - this.dataGridViewImageColumn1.ReadOnly = true; - // - // dataGridViewTextBoxColumn1 - // - this.dataGridViewTextBoxColumn1.DataPropertyName = "Title"; - this.dataGridViewTextBoxColumn1.HeaderText = "Title"; - this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1"; - this.dataGridViewTextBoxColumn1.ReadOnly = true; - // - // dataGridViewTextBoxColumn2 - // - this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors"; - this.dataGridViewTextBoxColumn2.HeaderText = "Authors"; - this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2"; - this.dataGridViewTextBoxColumn2.ReadOnly = true; - // - // dataGridViewTextBoxColumn3 - // - this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators"; - this.dataGridViewTextBoxColumn3.HeaderText = "Narrators"; - this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3"; - this.dataGridViewTextBoxColumn3.ReadOnly = true; - // - // dataGridViewTextBoxColumn4 - // - this.dataGridViewTextBoxColumn4.DataPropertyName = "Length"; - this.dataGridViewTextBoxColumn4.HeaderText = "Length"; - this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4"; - this.dataGridViewTextBoxColumn4.ReadOnly = true; - // - // dataGridViewTextBoxColumn5 - // - this.dataGridViewTextBoxColumn5.DataPropertyName = "Series"; - this.dataGridViewTextBoxColumn5.HeaderText = "Series"; - this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5"; - this.dataGridViewTextBoxColumn5.ReadOnly = true; - // - // dataGridViewTextBoxColumn6 - // - this.dataGridViewTextBoxColumn6.DataPropertyName = "Description"; - this.dataGridViewTextBoxColumn6.HeaderText = "Description"; - this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6"; - this.dataGridViewTextBoxColumn6.ReadOnly = true; - // - // dataGridViewTextBoxColumn7 - // - this.dataGridViewTextBoxColumn7.DataPropertyName = "Category"; - this.dataGridViewTextBoxColumn7.HeaderText = "Category"; - this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7"; - this.dataGridViewTextBoxColumn7.ReadOnly = true; - // - // dataGridViewTextBoxColumn8 - // - this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating"; - this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating"; - this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8"; - this.dataGridViewTextBoxColumn8.ReadOnly = true; - // - // dataGridViewTextBoxColumn9 - // - this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date"; - this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date"; - this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9"; - this.dataGridViewTextBoxColumn9.ReadOnly = true; - // - // dataGridViewTextBoxColumn10 - // - this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating"; - this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating"; - this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10"; - this.dataGridViewTextBoxColumn10.ReadOnly = true; - // - // dataGridViewTextBoxColumn11 - // - this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc"; - this.dataGridViewTextBoxColumn11.HeaderText = "Misc"; - this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11"; - this.dataGridViewTextBoxColumn11.ReadOnly = true; - // - // ProductsGrid - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.Controls.Add(this.gridEntryDataGridView); - this.Name = "ProductsGrid"; - this.Size = new System.Drawing.Size(434, 329); - ((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit(); - this.ResumeLayout(false); + this.components = new System.ComponentModel.Container(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); + this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components); + this.gridEntryDataGridView = new System.Windows.Forms.DataGridView(); + this.dataGridViewImageButtonBoxColumn1 = new LibationWinForms.LiberateDataGridViewImageButtonColumn(); + this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn(); + this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.dataGridViewImageButtonBoxColumn2 = new LibationWinForms.EditTagsDataGridViewImageButtonColumn(); + ((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit(); + this.SuspendLayout(); + // + // gridEntryBindingSource + // + this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry); + // + // gridEntryDataGridView + // + this.gridEntryDataGridView.AllowUserToAddRows = false; + this.gridEntryDataGridView.AllowUserToDeleteRows = false; + this.gridEntryDataGridView.AllowUserToResizeRows = false; + this.gridEntryDataGridView.AutoGenerateColumns = false; + this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.dataGridViewImageButtonBoxColumn1, + this.dataGridViewImageColumn1, + this.dataGridViewTextBoxColumn1, + this.dataGridViewTextBoxColumn2, + this.dataGridViewTextBoxColumn3, + this.dataGridViewTextBoxColumn4, + this.dataGridViewTextBoxColumn5, + this.dataGridViewTextBoxColumn6, + this.dataGridViewTextBoxColumn7, + this.dataGridViewTextBoxColumn8, + this.dataGridViewTextBoxColumn9, + this.dataGridViewTextBoxColumn10, + this.dataGridViewTextBoxColumn11, + this.dataGridViewImageButtonBoxColumn2}); + this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource; + dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window; + dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText; + dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight; + dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText; + dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; + this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1; + this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill; + this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0); + this.gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.gridEntryDataGridView.Name = "gridEntryDataGridView"; + this.gridEntryDataGridView.ReadOnly = true; + this.gridEntryDataGridView.RowHeadersVisible = false; + this.gridEntryDataGridView.RowTemplate.Height = 82; + this.gridEntryDataGridView.Size = new System.Drawing.Size(1505, 380); + this.gridEntryDataGridView.TabIndex = 0; + // + // dataGridViewImageButtonBoxColumn1 + // + this.dataGridViewImageButtonBoxColumn1.DataPropertyName = "Liberate"; + this.dataGridViewImageButtonBoxColumn1.HeaderText = "Liberate"; + this.dataGridViewImageButtonBoxColumn1.Name = "dataGridViewImageButtonBoxColumn1"; + this.dataGridViewImageButtonBoxColumn1.ReadOnly = true; + this.dataGridViewImageButtonBoxColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.dataGridViewImageButtonBoxColumn1.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic; + this.dataGridViewImageButtonBoxColumn1.Width = 70; + // + // dataGridViewImageColumn1 + // + this.dataGridViewImageColumn1.DataPropertyName = "Cover"; + this.dataGridViewImageColumn1.HeaderText = "Cover"; + this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1"; + this.dataGridViewImageColumn1.ReadOnly = true; + this.dataGridViewImageColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.dataGridViewImageColumn1.ToolTipText = "Cover Art"; + this.dataGridViewImageColumn1.Width = 80; + // + // dataGridViewTextBoxColumn1 + // + this.dataGridViewTextBoxColumn1.DataPropertyName = "Title"; + this.dataGridViewTextBoxColumn1.HeaderText = "Title"; + this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1"; + this.dataGridViewTextBoxColumn1.ReadOnly = true; + this.dataGridViewTextBoxColumn1.Width = 200; + // + // dataGridViewTextBoxColumn2 + // + this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors"; + this.dataGridViewTextBoxColumn2.HeaderText = "Authors"; + this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2"; + this.dataGridViewTextBoxColumn2.ReadOnly = true; + // + // dataGridViewTextBoxColumn3 + // + this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators"; + this.dataGridViewTextBoxColumn3.HeaderText = "Narrators"; + this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3"; + this.dataGridViewTextBoxColumn3.ReadOnly = true; + // + // dataGridViewTextBoxColumn4 + // + this.dataGridViewTextBoxColumn4.DataPropertyName = "Length"; + this.dataGridViewTextBoxColumn4.HeaderText = "Length"; + this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4"; + this.dataGridViewTextBoxColumn4.ReadOnly = true; + this.dataGridViewTextBoxColumn4.ToolTipText = "Recording Length"; + // + // dataGridViewTextBoxColumn5 + // + this.dataGridViewTextBoxColumn5.DataPropertyName = "Series"; + this.dataGridViewTextBoxColumn5.HeaderText = "Series"; + this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5"; + this.dataGridViewTextBoxColumn5.ReadOnly = true; + // + // dataGridViewTextBoxColumn6 + // + this.dataGridViewTextBoxColumn6.DataPropertyName = "Description"; + this.dataGridViewTextBoxColumn6.HeaderText = "Description"; + this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6"; + this.dataGridViewTextBoxColumn6.ReadOnly = true; + // + // dataGridViewTextBoxColumn7 + // + this.dataGridViewTextBoxColumn7.DataPropertyName = "Category"; + this.dataGridViewTextBoxColumn7.HeaderText = "Category"; + this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7"; + this.dataGridViewTextBoxColumn7.ReadOnly = true; + // + // ProductRating + // + this.dataGridViewTextBoxColumn8.DataPropertyName = "ProductRating"; + this.dataGridViewTextBoxColumn8.HeaderText = "Product Rating"; + this.dataGridViewTextBoxColumn8.Name = "ProductRating"; + this.dataGridViewTextBoxColumn8.ReadOnly = true; + this.dataGridViewTextBoxColumn8.Width = 108; + // + // PurchaseDate + // + this.dataGridViewTextBoxColumn9.DataPropertyName = "PurchaseDate"; + this.dataGridViewTextBoxColumn9.HeaderText = "Purchase Date"; + this.dataGridViewTextBoxColumn9.Name = "PurchaseDate"; + this.dataGridViewTextBoxColumn9.ReadOnly = true; + // + // MyRating + // + this.dataGridViewTextBoxColumn10.DataPropertyName = "MyRating"; + this.dataGridViewTextBoxColumn10.HeaderText = "My Rating"; + this.dataGridViewTextBoxColumn10.Name = "MyRating"; + this.dataGridViewTextBoxColumn10.ReadOnly = true; + this.dataGridViewTextBoxColumn10.Width = 108; + // + // dataGridViewTextBoxColumn11 + // + this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc"; + this.dataGridViewTextBoxColumn11.HeaderText = "Misc"; + this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11"; + this.dataGridViewTextBoxColumn11.ReadOnly = true; + this.dataGridViewTextBoxColumn11.Width = 135; + // + // dataGridViewImageButtonBoxColumn2 + // + this.dataGridViewImageButtonBoxColumn2.DataPropertyName = "DisplayTags"; + this.dataGridViewImageButtonBoxColumn2.HeaderText = "Edit Tags"; + this.dataGridViewImageButtonBoxColumn2.Name = "dataGridViewImageButtonBoxColumn2"; + this.dataGridViewImageButtonBoxColumn2.ReadOnly = true; + this.dataGridViewImageButtonBoxColumn2.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.dataGridViewImageButtonBoxColumn2.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic; + // + // ProductsGrid + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.gridEntryDataGridView); + this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.Name = "ProductsGrid"; + this.Size = new System.Drawing.Size(1505, 380); + ((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit(); + this.ResumeLayout(false); } @@ -175,17 +224,19 @@ private System.Windows.Forms.BindingSource gridEntryBindingSource; private System.Windows.Forms.DataGridView gridEntryDataGridView; - private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10; - private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11; - } + private LiberateDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn1; + private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10; + private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11; + private EditTagsDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn2; + } } diff --git a/LibationWinForms/ProductsGrid.cs b/LibationWinForms/ProductsGrid.cs index 5f811bd1..330e7ef1 100644 --- a/LibationWinForms/ProductsGrid.cs +++ b/LibationWinForms/ProductsGrid.cs @@ -1,418 +1,200 @@ -using System; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; -using ApplicationServices; +using ApplicationServices; using DataLayer; using Dinah.Core; -using Dinah.Core.Collections.Generic; -using Dinah.Core.DataBinding; using Dinah.Core.Windows.Forms; using LibationWinForms.Dialogs; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; namespace LibationWinForms { - // INSTRUCTIONS TO UPDATE DATA_GRID_VIEW - // - delete current DataGridView - // - view > other windows > data sources - // - refresh - // OR - // - Add New Data Source - // Object. Next - // LibationWinForms - // AudibleDTO - // GridEntry - // - go to Design view - // - click on Data Sources > ProductItem. drowdown: DataGridView - // - drag/drop ProductItem on design surface - public partial class ProductsGrid : UserControl - { - public event EventHandler VisibleCountChanged; - public event EventHandler BackupCountsChanged; + // INSTRUCTIONS TO UPDATE DATA_GRID_VIEW + // - delete current DataGridView + // - view > other windows > data sources + // - refresh + // OR + // - Add New Data Source + // Object. Next + // LibationWinForms + // AudibleDTO + // GridEntry + // - go to Design view + // - click on Data Sources > ProductItem. dropdown: DataGridView + // - drag/drop ProductItem on design surface + // AS OF AUGUST 2021 THIS DOES NOT WORK IN VS2019 WITH .NET-5 PROJECTS - private const string EDIT_TAGS = "Edit Tags"; - private const string LIBERATE = "Liberate"; + public partial class ProductsGrid : UserControl + { + public event EventHandler VisibleCountChanged; + public event EventHandler BackupCountsChanged; - // alias - private DataGridView dataGridView => gridEntryDataGridView; + // alias + private DataGridView _dataGridView => gridEntryDataGridView; public ProductsGrid() { InitializeComponent(); - formatDataGridView(); - addLiberateButtons(); - addEditTagsButtons(); - formatColumns(); - manageLiveImageUpdateSubscriptions(); + // sorting breaks filters. must reapply filters after sorting + _dataGridView.Sorted += (_, __) => Filter(); + _dataGridView.CellContentClick += DataGridView_CellContentClick; - enableDoubleBuffering(); + EnableDoubleBuffering(); } - - private void enableDoubleBuffering() + private void EnableDoubleBuffering() { - var propertyInfo = dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var propertyInfo = _dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - //var before = (bool)propertyInfo.GetValue(dataGridView); - propertyInfo.SetValue(dataGridView, true, null); - //var after = (bool)propertyInfo.GetValue(dataGridView); + propertyInfo.SetValue(_dataGridView, true, null); } - private void formatDataGridView() - { - dataGridView.Dock = DockStyle.Fill; - dataGridView.AllowUserToAddRows = false; - dataGridView.AllowUserToDeleteRows = false; - dataGridView.AutoGenerateColumns = false; - dataGridView.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; - dataGridView.DefaultCellStyle.WrapMode = DataGridViewTriState.True; - dataGridView.ReadOnly = true; - dataGridView.RowHeadersVisible = false; + #region Button controls - // adjust height for 80x80 pictures. - // this must be done before databinding. or can alter later by iterating through rows - dataGridView.RowTemplate.Height = 82; - dataGridView.CellFormatting += replaceFormatted; - dataGridView.CellFormatting += hiddenFormatting; - - // sorting breaks filters. must reapply filters after sorting - dataGridView.Sorted += (_, __) => filter(); - } - - #region format text cells. ie: not buttons - private void replaceFormatted(object sender, DataGridViewCellFormattingEventArgs e) - { - var col = ((DataGridView)sender).Columns[e.ColumnIndex]; - if (col is DataGridViewTextBoxColumn textCol && getGridEntry(e.RowIndex).TryDisplayValue(textCol.Name, out string value)) - { - // DO NOT DO THIS: getCell(e).Value = value; - // it's the wrong way and will infinitely call CellFormatting on each assign - - // this is the correct way. will actually set FormattedValue (and EditedFormattedValue) while leaving Value as-is for sorting - e.Value = value; - - getCell(e).ToolTipText = value; - } - } - - private void hiddenFormatting(object sender, DataGridViewCellFormattingEventArgs e) - { - var dgv = (DataGridView)sender; - // no action needed for buttons - if (e.RowIndex < 0 || dgv.Columns[e.ColumnIndex] is DataGridViewButtonColumn) - return; - - var isHidden = getGridEntry(e.RowIndex).TagsEnumerated.Contains("hidden"); - - getCell(e).Style - = isHidden - ? new DataGridViewCellStyle { ForeColor = Color.LightGray } - : dgv.DefaultCellStyle; - } - #endregion - - #region liberation buttons - private void addLiberateButtons() - { - dataGridView.Columns.Insert(0, new DataGridViewButtonColumn { HeaderText = LIBERATE }); - - dataGridView.CellPainting += liberate_Paint; - dataGridView.CellContentClick += liberate_Click; - } - - private void liberate_Paint(object sender, DataGridViewCellPaintingEventArgs e) - { - if (!isColumnValid(e, LIBERATE)) - return; - - var cell = getCell(e); - var gridEntry = getGridEntry(e.RowIndex); - var liberatedStatus = gridEntry.Liberated_Status; - var pdfStatus = gridEntry.Pdf_Status; - - // mouseover text - { - var libState = liberatedStatus switch - { - LiberatedState.Liberated => "Liberated", - LiberatedState.PartialDownload => "File has been at least\r\npartially downloaded", - LiberatedState.NotDownloaded => "Book NOT downloaded", - _ => throw new Exception("Unexpected liberation state") - }; - - var pdfState = pdfStatus switch - { - PdfState.Downloaded => "\r\nPDF downloaded", - PdfState.NotDownloaded => "\r\nPDF NOT downloaded", - PdfState.NoPdf => "", - _ => throw new Exception("Unexpected PDF state") - }; - - var text = libState + pdfState; - - if (liberatedStatus == LiberatedState.NotDownloaded || - liberatedStatus == LiberatedState.PartialDownload || - pdfStatus == PdfState.NotDownloaded) - text += "\r\nClick to complete"; - - //DEBUG//cell.Value = text; - cell.ToolTipText = text; - } - - // draw img - { - var image_lib - = liberatedStatus == LiberatedState.NotDownloaded ? "red" - : liberatedStatus == LiberatedState.PartialDownload ? "yellow" - : liberatedStatus == LiberatedState.Liberated ? "green" - : throw new Exception("Unexpected liberation state"); - var image_pdf - = pdfStatus == PdfState.NoPdf ? "" - : pdfStatus == PdfState.NotDownloaded ? "_pdf_no" - : pdfStatus == PdfState.Downloaded ? "_pdf_yes" - : throw new Exception("Unexpected PDF state"); - var image = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}"); - drawImage(e, image); - } - } - - private async void liberate_Click(object sender, DataGridViewCellEventArgs e) - { - if (!isColumnValid(e, LIBERATE)) - return; - - var libraryBook = getGridEntry(e.RowIndex).GetLibraryBook(); - - // liberated: open explorer to file - if (TransitionalFileLocator.Audio_Exists(libraryBook.Book)) - { - var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book); - if (!Go.To.File(filePath)) - MessageBox.Show($"File not found:\r\n{filePath}"); - return; - } - - // else: liberate - await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId)); - } - #endregion - - public void RefreshRow(string productId) - { - var rowId = getRowId((ge) => ge.AudibleProductId == productId); - - // update cells incl Liberate button text - dataGridView.InvalidateRow(rowId); - - // needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change - filter(); - - BackupCountsChanged?.Invoke(this, EventArgs.Empty); - } - - #region tag buttons - private void addEditTagsButtons() - { - dataGridView.Columns.Add(new DataGridViewButtonColumn { HeaderText = EDIT_TAGS }); - - dataGridView.CellPainting += editTags_Paint; - dataGridView.CellContentClick += editTags_Click; - } - - private void editTags_Paint(object sender, DataGridViewCellPaintingEventArgs e) - { - // DataGridView Image for Button Column: https://stackoverflow.com/a/36253883 - - if (!isColumnValid(e, EDIT_TAGS)) - return; - - var cell = getCell(e); - var gridEntry = getGridEntry(e.RowIndex); - - var displayTags = gridEntry.TagsEnumerated.ToList(); - - if (displayTags.Any()) - cell.Value = string.Join("\r\n", displayTags); - else - { - // if removing all tags: clear previous tag text - cell.Value = ""; - drawImage(e, Properties.Resources.edit_tags_25x25); - } - } - - private void editTags_Click(object sender, DataGridViewCellEventArgs e) - { - // handle grid button click: https://stackoverflow.com/a/13687844 - - var dgv = (DataGridView)sender; - - if (!isColumnValid(e, EDIT_TAGS)) - return; - - var liveGridEntry = getGridEntry(e.RowIndex); - - // EditTagsDialog should display better-formatted title - liveGridEntry.TryDisplayValue(nameof(liveGridEntry.Title), out string value); - - var bookDetailsForm = new BookDetailsDialog(value, liveGridEntry.Tags); - if (bookDetailsForm.ShowDialog() != DialogResult.OK) - return; - - var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.GetBook(), bookDetailsForm.NewTags); - if (qtyChanges == 0) - return; - - // force a re-draw, and re-apply filters - - // needed to update text colors - dgv.InvalidateRow(e.RowIndex); - - filter(); - } - #endregion - - private static void drawImage(DataGridViewCellPaintingEventArgs e, Bitmap image) - { - e.Paint(e.CellBounds, DataGridViewPaintParts.All); - - var w = image.Width; - var h = image.Height; - var x = e.CellBounds.Left + (e.CellBounds.Width - w) / 2; - var y = e.CellBounds.Top + (e.CellBounds.Height - h) / 2; - - e.Graphics.DrawImage(image, new Rectangle(x, y, w, h)); - e.Handled = true; - } - - private bool isColumnValid(DataGridViewCellEventArgs e, string colName) => isColumnValid(e.RowIndex, e.ColumnIndex, colName); - private bool isColumnValid(DataGridViewCellPaintingEventArgs e, string colName) => isColumnValid(e.RowIndex, e.ColumnIndex, colName); - private bool isColumnValid(int rowIndex, int colIndex, string colName) - { - var col = dataGridView.Columns[colIndex]; - return rowIndex >= 0 && col.Name == colName && col is DataGridViewButtonColumn; - } - - private void formatColumns() - { - for (var i = dataGridView.ColumnCount - 1; i >= 0; i--) - { - var col = dataGridView.Columns[i]; - - // initial HeaderText is the lookup name from GridEntry class. any formatting below won't change this - col.Name = col.HeaderText; - - if (!(col is DataGridViewImageColumn || col is DataGridViewButtonColumn)) - col.SortMode = DataGridViewColumnSortMode.Automatic; - - col.HeaderText = col.HeaderText.Replace("_", " "); - - col.Width = col.Name switch - { - LIBERATE => 70, - nameof(GridEntry.Cover) => 80, - nameof(GridEntry.Title) => col.Width * 2, - nameof(GridEntry.Misc) => (int)(col.Width * 1.35), - var n when n.In(nameof(GridEntry.My_Rating), nameof(GridEntry.Product_Rating)) => col.Width + 8, - _ => col.Width - }; - } - } - - #region live update newly downloaded and cached images - private void manageLiveImageUpdateSubscriptions() + private async void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e) { - FileManager.PictureStorage.PictureCached += crossThreadImageUpdate; - Disposed += (_, __) => FileManager.PictureStorage.PictureCached -= crossThreadImageUpdate; - } - - private void crossThreadImageUpdate(object _, string pictureId) - => dataGridView.UIThread(() => updateRowImage(pictureId)); - private void updateRowImage(string pictureId) - { - var rowId = getRowId((ge) => ge.PictureId == pictureId); - if (rowId > -1) - dataGridView.InvalidateRow(rowId); - } - #endregion - - private bool hasBeenDisplayed = false; - public void Display() - { - if (hasBeenDisplayed) - return; - hasBeenDisplayed = true; - - // - // transform into sorted GridEntry.s BEFORE binding - // - using var context = DbContexts.GetContext(); - var lib = context.GetLibrary_Flat_NoTracking(); - - // if no data. hide all columns. return - if (!lib.Any()) - { - for (var i = dataGridView.ColumnCount - 1; i >= 0; i--) - dataGridView.Columns.RemoveAt(i); - return; - } - - var orderedGridEntries = lib - .Select(lb => new GridEntry(lb)).ToList() - // default load order - .OrderByDescending(ge => ge.Purchase_Date) - //// more advanced example: sort by author, then series, then title - //.OrderBy(ge => ge.Authors) - // .ThenBy(ge => ge.Series) - // .ThenBy(ge => ge.Title) - .ToList(); - - // - // BIND - // - gridEntryBindingSource.DataSource = orderedGridEntries.ToSortableBindingList(); - - // - // FILTER - // - filter(); - - BackupCountsChanged?.Invoke(this, EventArgs.Empty); - } - - #region filter - string _filterSearchString; - private void filter() => Filter(_filterSearchString); - public void Filter(string searchString) - { - _filterSearchString = searchString; - - if (dataGridView.Rows.Count == 0) + // handle grid button click: https://stackoverflow.com/a/13687844 + if (e.RowIndex < 0 || _dataGridView.Columns[e.ColumnIndex] is not DataGridViewButtonColumn) return; - var searchResults = SearchEngineCommands.Search(searchString); - var productIds = searchResults.Docs.Select(d => d.ProductId).ToList(); + var liveGridEntry = getGridEntry(e.RowIndex); - // https://stackoverflow.com/a/18942430 - var currencyManager = (CurrencyManager)BindingContext[dataGridView.DataSource]; - currencyManager.SuspendBinding(); - { - for (var r = dataGridView.RowCount - 1; r >= 0; r--) - dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId); - } - currencyManager.ResumeBinding(); - VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible)); - } - #endregion + switch (_dataGridView.Columns[e.ColumnIndex].DataPropertyName) + { + case nameof(liveGridEntry.Liberate): + await Liberate_Click(liveGridEntry); + break; + case nameof(liveGridEntry.DisplayTags): + EditTags_Click(liveGridEntry); + break; + } + } - private int getRowId(Func func) => dataGridView.GetRowIdOfBoundItem(func); + private async Task Liberate_Click(GridEntry liveGridEntry) + { + var libraryBook = liveGridEntry.LibraryBook; - private GridEntry getGridEntry(int rowIndex) => dataGridView.GetBoundItem(rowIndex); + // liberated: open explorer to file + if (TransitionalFileLocator.Audio_Exists(libraryBook.Book)) + { + var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book); + if (!Go.To.File(filePath)) + MessageBox.Show($"File not found:\r\n{filePath}"); + return; + } - private DataGridViewCell getCell(DataGridViewCellFormattingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex); + // else: liberate + await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId)); + } - private DataGridViewCell getCell(DataGridViewCellPaintingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex); + private void EditTags_Click(GridEntry liveGridEntry) + { + var bookDetailsForm = new BookDetailsDialog(liveGridEntry.Title, liveGridEntry.LibraryBook.Book.UserDefinedItem.Tags); + if (bookDetailsForm.ShowDialog() != DialogResult.OK) + return; - private DataGridViewCell getCell(int rowIndex, int columnIndex) => dataGridView.Rows[rowIndex].Cells[columnIndex]; - } + var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags); + if (qtyChanges == 0) + return; + + //Re-apply filters + Filter(); + } + + #endregion + + #region UI display functions + + private bool hasBeenDisplayed = false; + public void Display() + { + if (hasBeenDisplayed) + return; + hasBeenDisplayed = true; + + // + // transform into sorted GridEntry.s BEFORE binding + // + using var context = DbContexts.GetContext(); + var lib = context.GetLibrary_Flat_NoTracking(); + + // if no data. hide all columns. return + if (!lib.Any()) + { + for (var i = _dataGridView.ColumnCount - 1; i >= 0; i--) + _dataGridView.Columns.RemoveAt(i); + return; + } + + var orderedGridEntries = lib + .Select(lb => new GridEntry(lb)).ToList() + // default load order + .OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate))) + //// more advanced example: sort by author, then series, then title + //.OrderBy(ge => ge.Authors) + // .ThenBy(ge => ge.Series) + // .ThenBy(ge => ge.Title) + .ToList(); + + // BIND + gridEntryBindingSource.DataSource = new SortableBindingList2(orderedGridEntries); + + // FILTER + Filter(); + + BackupCountsChanged?.Invoke(this, EventArgs.Empty); + } + + public void RefreshRow(string productId) + { + var rowIndex = getRowIndex((ge) => ge.AudibleProductId == productId); + + // update cells incl Liberate button text + _dataGridView.InvalidateRow(rowIndex); + + // 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 + + private string _filterSearchString; + private void Filter() => Filter(_filterSearchString); + public void Filter(string searchString) + { + _filterSearchString = searchString; + + if (_dataGridView.Rows.Count == 0) + return; + + var searchResults = SearchEngineCommands.Search(searchString); + var productIds = searchResults.Docs.Select(d => d.ProductId).ToList(); + + // https://stackoverflow.com/a/18942430 + var bindingContext = BindingContext[_dataGridView.DataSource]; + bindingContext.SuspendBinding(); + { + for (var r = _dataGridView.RowCount - 1; r >= 0; r--) + _dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId); + } + + //Causes repainting of the DataGridView + bindingContext.ResumeBinding(); + VisibleCountChanged?.Invoke(this, _dataGridView.AsEnumerable().Count(r => r.Visible)); + } + + #endregion + + #region DataGridView Macro + + private int getRowIndex(Func func) => _dataGridView.GetRowIdOfBoundItem(func); + private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem(rowIndex); + + #endregion + } } diff --git a/LibationWinForms/ProductsGrid.resx b/LibationWinForms/ProductsGrid.resx index d1166daf..6ee0745a 100644 --- a/LibationWinForms/ProductsGrid.resx +++ b/LibationWinForms/ProductsGrid.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/Properties/DataSources/LibationWinForm.RemovableGridEntry.datasource b/LibationWinForms/Properties/DataSources/LibationWinForm.RemovableGridEntry.datasource new file mode 100644 index 00000000..da01a0bf --- /dev/null +++ b/LibationWinForms/Properties/DataSources/LibationWinForm.RemovableGridEntry.datasource @@ -0,0 +1,10 @@ + + + + LibationWinForms.Dialogs.RemovableGridEntry, LibationWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/LibationWinForms/Properties/Resources.resx b/LibationWinForms/Properties/Resources.resx index 70761507..47825a4b 100644 --- a/LibationWinForms/Properties/Resources.resx +++ b/LibationWinForms/Properties/Resources.resx @@ -1,64 +1,5 @@  - diff --git a/LibationWinForms/SortableBindingList2[T].cs b/LibationWinForms/SortableBindingList2[T].cs new file mode 100644 index 00000000..c29d1f65 --- /dev/null +++ b/LibationWinForms/SortableBindingList2[T].cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.ComponentModel; + +namespace LibationWinForms +{ + internal class SortableBindingList2 : BindingList where T : IMemberComparable + { + private bool isSorted; + private ListSortDirection listSortDirection; + private PropertyDescriptor propertyDescriptor; + + public SortableBindingList2() : base(new List()) { } + public SortableBindingList2(IEnumerable enumeration) : base(new List(enumeration)) { } + + private MemberComparer Comparer { get; } = new(); + protected override bool SupportsSortingCore => true; + protected override bool SupportsSearchingCore => true; + protected override bool IsSortedCore => isSorted; + protected override PropertyDescriptor SortPropertyCore => propertyDescriptor; + protected override ListSortDirection SortDirectionCore => listSortDirection; + + protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction) + { + List itemsList = (List)Items; + + Comparer.PropertyName = property.Name; + Comparer.Direction = direction; + + //Array.Sort() and List.Sort() are unstable sorts. OrderBy is stable. + var sortedItems = itemsList.OrderBy((ge) => ge, Comparer).ToList(); + + itemsList.Clear(); + itemsList.AddRange(sortedItems); + + propertyDescriptor = property; + listSortDirection = direction; + isSorted = true; + + OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); + } + + protected override void RemoveSortCore() + { + isSorted = false; + propertyDescriptor = base.SortPropertyCore; + listSortDirection = base.SortDirectionCore; + + OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); + } + //NOTE: Libation does not currently use BindingSource.Find anywhere, + //so this override may be removed (along with SupportsSearchingCore) + protected override int FindCore(PropertyDescriptor property, object key) + { + int count = Count; + + System.Collections.IComparer valueComparer = null; + + for (int i = 0; i < count; ++i) + { + T element = this[i]; + var elemValue = element.GetMemberValue(property.Name); + valueComparer ??= element.GetMemberComparer(elemValue.GetType()); + + if (valueComparer.Compare(elemValue, key) == 0) + { + return i; + } + } + + return -1; + } + } +} diff --git a/LibationWinForms/SynchronizeInvoker.cs b/LibationWinForms/SynchronizeInvoker.cs new file mode 100644 index 00000000..c38c0499 --- /dev/null +++ b/LibationWinForms/SynchronizeInvoker.cs @@ -0,0 +1,122 @@ +using System; +using System.ComponentModel; +using System.Threading; + +namespace LibationWinForms +{ + public class SynchronizeInvoker : ISynchronizeInvoke + { + public bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId; + private int InstanceThreadId { get; set; } = Thread.CurrentThread.ManagedThreadId; + private SynchronizationContext SyncContext { get; } = SynchronizationContext.Current; + + public SynchronizeInvoker() + { + if (SyncContext is null) + throw new NullReferenceException($"Could not capture a current {nameof(SynchronizationContext)}"); + } + + public IAsyncResult BeginInvoke(Action action) => BeginInvoke(action, null); + public IAsyncResult BeginInvoke(Delegate method) => BeginInvoke(method, null); + public IAsyncResult BeginInvoke(Delegate method, object[] args) + { + var tme = new ThreadMethodEntry(method, args); + + if (InvokeRequired) + { + SyncContext.Post(OnSendOrPostCallback, tme); + } + else + { + tme.Complete(); + tme.CompletedSynchronously = true; + } + return tme; + } + + public object EndInvoke(IAsyncResult result) + { + if (result is not ThreadMethodEntry crossThread) + throw new ArgumentException($"{nameof(result)} was not returned by {nameof(SynchronizeInvoker)}.{nameof(BeginInvoke)}"); + + if (!crossThread.IsCompleted) + crossThread.AsyncWaitHandle.WaitOne(); + + return crossThread.ReturnValue; + } + + public object Invoke(Action action) => Invoke(action, null); + public object Invoke(Delegate method) => Invoke(method, null); + public object Invoke(Delegate method, object[] args) + { + var tme = new ThreadMethodEntry(method, args); + + if (InvokeRequired) + { + SyncContext.Send(OnSendOrPostCallback, tme); + } + else + { + tme.Complete(); + tme.CompletedSynchronously = true; + } + + return tme.ReturnValue; + } + + /// + /// This callback executes on the SynchronizationContext thread. + /// + private static void OnSendOrPostCallback(object asyncArgs) + { + var e = asyncArgs as ThreadMethodEntry; + e.Complete(); + } + + private class ThreadMethodEntry : IAsyncResult + { + public object AsyncState => null; + public bool CompletedSynchronously { get; internal set; } + public bool IsCompleted { get; private set; } + public object ReturnValue { get; private set; } + public WaitHandle AsyncWaitHandle => completedEvent; + + private Delegate method; + private object[] args; + private ManualResetEvent completedEvent; + + public ThreadMethodEntry(Delegate method, object[] args) + { + this.method = method; + this.args = args; + completedEvent = new ManualResetEvent(initialState: false); + } + + public void Complete() + { + try + { + switch (method) + { + case Action actiton: + actiton(); + break; + default: + ReturnValue = method.DynamicInvoke(args); + break; + } + } + finally + { + IsCompleted = true; + completedEvent.Set(); + } + } + + ~ThreadMethodEntry() + { + completedEvent.Close(); + } + } + } +} \ No newline at end of file diff --git a/WindowsDesktopUtilities/WinAudibleImageServer.cs b/WindowsDesktopUtilities/WinAudibleImageServer.cs deleted file mode 100644 index 1b3eeab2..00000000 --- a/WindowsDesktopUtilities/WinAudibleImageServer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using Dinah.Core.Drawing; -using FileManager; - -namespace WindowsDesktopUtilities -{ - public static class WinAudibleImageServer - { - private static Dictionary cache { get; } = new Dictionary(); - - public static Image GetImage(string pictureId, PictureSize size) - { - var def = new PictureDefinition(pictureId, size); - if (!cache.ContainsKey(def)) - { - (var isDefault, var bytes) = PictureStorage.GetPicture(def); - - var image = ImageReader.ToImage(bytes); - if (isDefault) - return image; - cache[def] = image; - } - return cache[def]; - } - } -} diff --git a/WindowsDesktopUtilities/WindowsDesktopUtilities.csproj b/WindowsDesktopUtilities/WindowsDesktopUtilities.csproj deleted file mode 100644 index 88b1cbf5..00000000 --- a/WindowsDesktopUtilities/WindowsDesktopUtilities.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Library - net5.0-windows - true - - - - - - - - - - \ No newline at end of file