Merge pull request #85 from Mbucari/master

"F*ck it, we'll do it live!"
This commit is contained in:
rmcrackan 2021-08-18 11:03:23 -04:00 committed by GitHub
commit fb7f57ab69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 3239 additions and 3055 deletions

View File

@ -1,32 +1,27 @@
using AAXClean; using AAXClean;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO; using Dinah.Core.IO;
using Dinah.Core.Logging; using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner; using Dinah.Core.StepRunner;
using System; using System;
using System.IO; using System.IO;
namespace AaxDecrypter namespace AaxDecrypter
{ {
public enum OutputFormat public enum OutputFormat { Mp4a, Mp3 }
{
Mp4a,
Mp3
}
public class AaxcDownloadConverter public class AaxcDownloadConverter
{ {
public event EventHandler<AppleTags> RetrievedTags; public event EventHandler<AppleTags> RetrievedTags;
public event EventHandler<byte[]> RetrievedCoverArt; public event EventHandler<byte[]> RetrievedCoverArt;
public event EventHandler<int> DecryptProgressUpdate; public event EventHandler<DownloadProgress> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining; public event EventHandler<TimeSpan> DecryptTimeRemaining;
public string AppName { get; set; } = nameof(AaxcDownloadConverter); public string AppName { get; set; } = nameof(AaxcDownloadConverter);
private string outputFileName { get; } private string outputFileName { get; }
private string cacheDir { get; } private string cacheDir { get; }
private DownloadLicense downloadLicense { get; } private DownloadLicense downloadLicense { get; }
private AaxFile aaxFile; private AaxFile aaxFile;
private byte[] coverArt;
private OutputFormat OutputFormat; private OutputFormat OutputFormat;
private StepSequence steps { get; } private StepSequence steps { get; }
@ -65,11 +60,15 @@ namespace AaxDecrypter
}; };
} }
/// <summary>
/// Setting cover art by this method will insert the art into the audiobook metadata
/// </summary>
public void SetCoverArt(byte[] coverArt) public void SetCoverArt(byte[] coverArt)
{ {
if (coverArt is null) return; if (coverArt is null) return;
this.coverArt = coverArt; aaxFile?.AppleTags.SetCoverArt(coverArt);
RetrievedCoverArt?.Invoke(this, coverArt); RetrievedCoverArt?.Invoke(this, coverArt);
} }
@ -98,7 +97,7 @@ namespace AaxDecrypter
try try
{ {
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState); 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. //The new url will be to the same file.
nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl)); nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
} }
@ -113,13 +112,11 @@ namespace AaxDecrypter
{ {
nfsPersister = NewNetworkFilePersister(); nfsPersister = NewNetworkFilePersister();
} }
nfsPersister.NetworkFileStream.BeginDownloading();
aaxFile = new AaxFile(nfsPersister.NetworkFileStream); aaxFile = new AaxFile(nfsPersister.NetworkFileStream);
coverArt = aaxFile.AppleTags.Cover;
RetrievedTags?.Invoke(this, aaxFile.AppleTags); RetrievedTags?.Invoke(this, aaxFile.AppleTags);
RetrievedCoverArt?.Invoke(this, coverArt); RetrievedCoverArt?.Invoke(this, aaxFile.AppleTags.Cover);
return !isCanceled; return !isCanceled;
} }
@ -136,8 +133,14 @@ namespace AaxDecrypter
public bool Step2_DownloadAndCombine() 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)) if (File.Exists(outputFileName))
FileExt.SafeDelete(outputFileName); FileExt.SafeDelete(outputFileName);
@ -147,7 +150,6 @@ namespace AaxDecrypter
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV); aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult = OutputFormat == OutputFormat.Mp4a ? aaxFile.ConvertToMp4a(outFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outFile); var decryptionResult = OutputFormat == OutputFormat.Mp4a ? aaxFile.ConvertToMp4a(outFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outFile);
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
@ -155,21 +157,9 @@ namespace AaxDecrypter
downloadLicense.ChapterInfo = aaxFile.Chapters; 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(); nfsPersister.Dispose();
DecryptProgressUpdate?.Invoke(this, 0); DecryptProgressUpdate?.Invoke(this, zeroProgress);
return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled; return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled;
} }
@ -185,7 +175,13 @@ namespace AaxDecrypter
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds; 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() public bool Step3_CreateCue()
@ -227,6 +223,7 @@ namespace AaxDecrypter
{ {
isCanceled = true; isCanceled = true;
aaxFile?.Cancel(); aaxFile?.Cancel();
aaxFile?.Dispose();
} }
} }
} }

View File

@ -6,7 +6,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
@ -27,7 +26,7 @@ namespace AaxDecrypter
public CookieCollection GetCookies() public CookieCollection GetCookies()
{ {
return base.GetCookies(Uri); return GetCookies(Uri);
} }
} }
@ -79,15 +78,14 @@ namespace AaxDecrypter
#endregion #endregion
#region Private Properties #region Private Properties
private HttpWebRequest HttpRequest { get; set; } private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; } private FileStream _writeFile { get; }
private FileStream _readFile { get; } private FileStream _readFile { get; }
private Stream _networkStream { get; set; } private Stream _networkStream { get; set; }
private bool hasBegunDownloading { get; set; } private bool hasBegunDownloading { get; set; }
private bool isCancelled { get; set; } private bool isCancelled { get; set; }
private bool finishedDownloading { get; set; } private EventWaitHandle downloadEnded { get; set; }
private Action downloadThreadCompleteCallback { get; set; } private EventWaitHandle downloadedPiece { get; set; }
#endregion #endregion
@ -147,7 +145,7 @@ namespace AaxDecrypter
private void Update() private void Update()
{ {
RequestHeaders = HttpRequest.Headers; RequestHeaders = HttpRequest.Headers;
Updated?.Invoke(this, new EventArgs()); Updated?.Invoke(this, EventArgs.Empty);
} }
/// <summary> /// <summary>
@ -160,8 +158,8 @@ namespace AaxDecrypter
if (uriToSameFile.Host != Uri.Host) 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}"); 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) if (hasBegunDownloading)
throw new Exception("Cannot change Uri during a download operation."); throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile; Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri); HttpRequest = WebRequest.CreateHttp(Uri);
@ -176,25 +174,27 @@ namespace AaxDecrypter
/// <summary> /// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. /// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary> /// </summary>
public void BeginDownloading() private void BeginDownloading()
{ {
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
if (ContentLength != 0 && WritePosition == ContentLength) if (ContentLength != 0 && WritePosition == ContentLength)
{ {
hasBegunDownloading = true; hasBegunDownloading = true;
finishedDownloading = true; downloadEnded.Set();
return; return;
} }
if (ContentLength != 0 && WritePosition > ContentLength) 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; var response = HttpRequest.GetResponse() as HttpWebResponse;
if (response.StatusCode != HttpStatusCode.PartialContent) 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) 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 //Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0- //to the complete file length if requesting Range: bytes=0-
@ -202,10 +202,12 @@ namespace AaxDecrypter
ContentLength = response.ContentLength; ContentLength = response.ContentLength;
_networkStream = response.GetResponseStream(); _networkStream = response.GetResponseStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background. //Download the file in the background.
Thread downloadThread = new Thread(() => DownloadFile()) { IsBackground = true }; new Thread(() => DownloadFile())
downloadThread.Start(); { IsBackground = true }
.Start();
hasBegunDownloading = true; hasBegunDownloading = true;
return; return;
@ -216,13 +218,13 @@ namespace AaxDecrypter
/// </summary> /// </summary>
private void DownloadFile() private void DownloadFile()
{ {
long downloadPosition = WritePosition; var downloadPosition = WritePosition;
long nextFlush = downloadPosition + DATA_FLUSH_SZ; var nextFlush = downloadPosition + DATA_FLUSH_SZ;
byte[] buff = new byte[DOWNLOAD_BUFF_SZ]; var buff = new byte[DOWNLOAD_BUFF_SZ];
do do
{ {
int bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead); _writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead; downloadPosition += bytesRead;
@ -233,6 +235,7 @@ namespace AaxDecrypter
WritePosition = downloadPosition; WritePosition = downloadPosition;
Update(); Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ; nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
} }
} while (downloadPosition < ContentLength && !isCancelled); } while (downloadPosition < ContentLength && !isCancelled);
@ -243,13 +246,12 @@ namespace AaxDecrypter
_networkStream.Close(); _networkStream.Close();
if (!isCancelled && WritePosition < ContentLength) 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) 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; downloadEnded.Set();
downloadThreadCompleteCallback?.Invoke();
} }
#endregion #endregion
@ -330,9 +332,7 @@ namespace AaxDecrypter
var result = new WebHeaderCollection(); var result = new WebHeaderCollection();
foreach (var kvp in jObj) foreach (var kvp in jObj)
{
result.Add(kvp.Key, kvp.Value.Value<string>()); result.Add(kvp.Key, kvp.Value.Value<string>());
}
return result; return result;
} }
@ -341,8 +341,8 @@ namespace AaxDecrypter
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{ {
JObject jObj = new JObject(); var jObj = new JObject();
Type type = value.GetType(); var type = value.GetType();
var headers = value as WebHeaderCollection; var headers = value as WebHeaderCollection;
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k])); var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
jObj.Add(jHeaders); jObj.Add(jHeaders);
@ -364,13 +364,21 @@ namespace AaxDecrypter
public override bool CanWrite => false; public override bool CanWrite => false;
[JsonIgnore] [JsonIgnore]
public override long Length => ContentLength; public override long Length
{
get
{
if (!hasBegunDownloading)
BeginDownloading();
return ContentLength;
}
}
[JsonIgnore] [JsonIgnore]
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); } public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
[JsonIgnore] [JsonIgnore]
public override bool CanTimeout => base.CanTimeout; public override bool CanTimeout => false;
[JsonIgnore] [JsonIgnore]
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; } public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
@ -387,63 +395,39 @@ namespace AaxDecrypter
if (!hasBegunDownloading) if (!hasBegunDownloading)
BeginDownloading(); BeginDownloading();
long toRead = Math.Min(count, Length - Position); var toRead = Math.Min(count, Length - Position);
long requiredPosition = Position + toRead; WaitToPosition(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);
return _readFile.Read(buffer, offset, count); return _readFile.Read(buffer, offset, count);
} }
public override long Seek(long offset, SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin)
{ {
long newPosition; var newPosition = origin switch
switch (origin)
{ {
case SeekOrigin.Current: SeekOrigin.Current => Position + offset,
newPosition = Position + offset; SeekOrigin.End => ContentLength + offset,
break; _ => offset,
case SeekOrigin.End: };
newPosition = ContentLength + offset;
break;
default:
newPosition = offset;
break;
}
ReadToPosition(newPosition);
_readFile.Position = newPosition; WaitToPosition(newPosition);
return newPosition; return _readFile.Position = newPosition;
} }
/// <summary> /// <summary>
/// Ensures that the file has downloaded to at least <paramref name="neededPosition"/>, then returns. /// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary> /// </summary>
/// <param name="neededPosition">The minimum required data length in <see cref="SaveFilePath"/>.</param> /// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void ReadToPosition(long neededPosition) private void WaitToPosition(long requiredPosition)
{ {
byte[] buff = new byte[DOWNLOAD_BUFF_SZ]; while (requiredPosition > WritePosition && !isCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
do
{
Read(buff, 0, DOWNLOAD_BUFF_SZ);
} while (neededPosition > WritePosition);
} }
public override void Close() public override void Close()
{ {
isCancelled = true; isCancelled = true;
downloadThreadCompleteCallback = CloseAction;
//ensure that close will run even if called after callback was fired. while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
if (finishedDownloading)
CloseAction();
}
private void CloseAction()
{
_readFile.Close(); _readFile.Close();
_writeFile.Close(); _writeFile.Close();
_networkStream?.Close(); _networkStream?.Close();
@ -451,5 +435,10 @@ namespace AaxDecrypter
} }
#endregion #endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
}
} }
} }

View File

@ -20,6 +20,57 @@ namespace ApplicationServices
public static class LibraryCommands public static class LibraryCommands
{ {
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, ILoginCallback> loginCallbackFactoryFunc, List<LibraryBook> 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<LibraryBook>();
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 #region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts) public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
{ {
@ -95,7 +146,7 @@ namespace ApplicationServices
Account = account?.MaskedLogEntry ?? "[null]" 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(); return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
} }

View File

@ -1,57 +0,0 @@
using System;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileManager;
namespace FileLiberator
{
/// <summary>
/// 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
/// </summary>
public class BackupBook : IProcessable
{
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> 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<StatusHandler> 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);
}
}
}
}

View File

@ -3,6 +3,7 @@ using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
using Dinah.Core.IO; using Dinah.Core.IO;
using Dinah.Core.Net.Http;
using FileManager; using FileManager;
using System; using System;
using System.IO; using System.IO;
@ -11,24 +12,25 @@ using System.Threading.Tasks;
namespace FileLiberator namespace FileLiberator
{ {
public class ConvertToMp3 : IDecryptable public class ConvertToMp3 : IAudioDecodable
{ {
public event EventHandler<string> DecryptBegin;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
public event EventHandler<int> UpdateProgress;
public event EventHandler<TimeSpan> UpdateRemainingTime;
public event EventHandler<string> DecryptCompleted;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> StatusUpdate;
public event EventHandler<Action<byte[]>> RequestCoverArt;
private Mp4File m4bBook; private Mp4File m4bBook;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageDiscovered;
public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> StreamingCompleted;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed;
private long fileSize;
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3"); private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
public void Cancel() => m4bBook?.Cancel(); public void Cancel() => m4bBook?.Cancel();
@ -43,19 +45,20 @@ namespace FileLiberator
{ {
Begin?.Invoke(this, libraryBook); Begin?.Invoke(this, libraryBook);
DecryptBegin?.Invoke(this, $"Begin converting {libraryBook} to mp3"); StreamingBegin?.Invoke(this, $"Begin converting {libraryBook} to mp3");
try try
{ {
var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book); var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
m4bBook = new Mp4File(m4bPath, FileAccess.Read); m4bBook = new Mp4File(m4bPath, FileAccess.Read);
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate; m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
fileSize = m4bBook.InputStream.Length;
TitleDiscovered?.Invoke(this, m4bBook.AppleTags.Title); TitleDiscovered?.Invoke(this, m4bBook.AppleTags.Title);
AuthorsDiscovered?.Invoke(this, m4bBook.AppleTags.FirstAuthor); AuthorsDiscovered?.Invoke(this, m4bBook.AppleTags.FirstAuthor);
NarratorsDiscovered?.Invoke(this, m4bBook.AppleTags.Narrator); 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()); using var mp3File = File.OpenWrite(Path.GetTempFileName());
@ -76,7 +79,7 @@ namespace FileLiberator
} }
finally 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); Completed?.Invoke(this, libraryBook);
} }
} }
@ -88,11 +91,17 @@ namespace FileLiberator
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed; double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining)) if (double.IsNormal(estTimeRemaining))
UpdateRemainingTime?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining)); StreamingTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds; 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
});
} }
} }
} }

View File

@ -8,26 +8,29 @@ using AudibleApi;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
using FileManager; using FileManager;
namespace FileLiberator namespace FileLiberator
{ {
public class DownloadDecryptBook : IDecryptable public class DownloadDecryptBook : IAudioDecodable
{ {
private AaxcDownloadConverter aaxcDownloader;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<Action<byte[]>> RequestCoverArt; public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> DecryptBegin;
public event EventHandler<string> TitleDiscovered; public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered; public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered; public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageFilepathDiscovered; public event EventHandler<byte[]> CoverImageDiscovered;
public event EventHandler<int> UpdateProgress; public event EventHandler<string> StreamingBegin;
public event EventHandler<TimeSpan> UpdateRemainingTime; public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> DecryptCompleted; public event EventHandler<string> StreamingCompleted;
public event EventHandler<LibraryBook> Completed; public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate; public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed;
private AaxcDownloadConverter aaxcDownloader;
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{ {
Begin?.Invoke(this, libraryBook); Begin?.Invoke(this, libraryBook);
@ -63,7 +66,7 @@ namespace FileLiberator
private async Task<string> aaxToM4bConverterDecryptAsync(string cacheDir, string destinationDir, LibraryBook libraryBook) private async Task<string> aaxToM4bConverterDecryptAsync(string cacheDir, string destinationDir, LibraryBook libraryBook)
{ {
DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}"); StreamingBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
try try
{ {
@ -78,7 +81,7 @@ namespace FileLiberator
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
contentLic?.Voucher?.Key, contentLic?.Voucher?.Key,
contentLic?.Voucher?.Iv, contentLic?.Voucher?.Iv,
"Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0" Resources.UserAgent
); );
if (Configuration.Instance.AllowLibationFixup) if (Configuration.Instance.AllowLibationFixup)
@ -103,8 +106,8 @@ namespace FileLiberator
aaxcDownloader = new AaxcDownloadConverter(outFileName, cacheDir, aaxcDecryptDlLic, format) { AppName = "Libation" }; aaxcDownloader = new AaxcDownloadConverter(outFileName, cacheDir, aaxcDecryptDlLic, format) { AppName = "Libation" };
aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress); aaxcDownloader.DecryptProgressUpdate += (s, progress) => StreamingProgressChanged?.Invoke(this, progress);
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining); aaxcDownloader.DecryptTimeRemaining += (s, remaining) => StreamingTimeRemaining?.Invoke(this, remaining);
aaxcDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; aaxcDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
aaxcDownloader.RetrievedTags += aaxcDownloader_RetrievedTags; aaxcDownloader.RetrievedTags += aaxcDownloader_RetrievedTags;
@ -119,10 +122,11 @@ namespace FileLiberator
} }
finally 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) if (e is null && Configuration.Instance.AllowLibationFixup)
@ -132,7 +136,7 @@ namespace FileLiberator
if (e is not null) if (e is not null)
{ {
CoverImageFilepathDiscovered?.Invoke(this, e); CoverImageDiscovered?.Invoke(this, e);
} }
} }

View File

@ -6,20 +6,21 @@ using Dinah.Core.Net.Http;
namespace FileLiberator namespace FileLiberator
{ {
// currently only used to download the .zip flies for upgrade // currently only used to download the .zip flies for upgrade
public class DownloadFile : IDownloadable public class DownloadFile : IStreamable
{ {
public event EventHandler<string> DownloadBegin; public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> DownloadProgressChanged; public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> DownloadCompleted; public event EventHandler<string> StreamingCompleted;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath) public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath)
{ {
var client = new HttpClient(); var client = new HttpClient();
var progress = new Progress<DownloadProgress>(); var progress = new Progress<DownloadProgress>();
progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e); progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e);
DownloadBegin?.Invoke(this, proposedDownloadFilePath); StreamingBegin?.Invoke(this, proposedDownloadFilePath);
try try
{ {
@ -28,7 +29,7 @@ namespace FileLiberator
} }
finally finally
{ {
DownloadCompleted?.Invoke(this, proposedDownloadFilePath); StreamingCompleted?.Invoke(this, proposedDownloadFilePath);
} }
} }
} }

View File

@ -6,16 +6,18 @@ using Dinah.Core.Net.Http;
namespace FileLiberator namespace FileLiberator
{ {
public abstract class DownloadableBase : IDownloadableProcessable public abstract class DownloadableBase : IProcessable
{ {
public event EventHandler<LibraryBook> Begin; public event EventHandler<LibraryBook> Begin;
public event EventHandler<LibraryBook> Completed; public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> DownloadBegin; public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> DownloadProgressChanged; public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> DownloadCompleted; public event EventHandler<string> StreamingCompleted;
public event EventHandler<string> StatusUpdate; public event EventHandler<string> StatusUpdate;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message); protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
public abstract bool Validate(LibraryBook libraryBook); public abstract bool Validate(LibraryBook libraryBook);
@ -44,9 +46,9 @@ namespace FileLiberator
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func) protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
{ {
var progress = new Progress<DownloadProgress>(); var progress = new Progress<DownloadProgress>();
progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e); progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e);
DownloadBegin?.Invoke(this, proposedDownloadFilePath); StreamingBegin?.Invoke(this, proposedDownloadFilePath);
try try
{ {
@ -57,7 +59,7 @@ namespace FileLiberator
} }
finally finally
{ {
DownloadCompleted?.Invoke(this, proposedDownloadFilePath); StreamingCompleted?.Invoke(this, proposedDownloadFilePath);
} }
} }
} }

View File

@ -0,0 +1,14 @@
using System;
namespace FileLiberator
{
public interface IAudioDecodable : IProcessable
{
event EventHandler<Action<byte[]>> RequestCoverArt;
event EventHandler<string> TitleDiscovered;
event EventHandler<string> AuthorsDiscovered;
event EventHandler<string> NarratorsDiscovered;
event EventHandler<byte[]> CoverImageDiscovered;
void Cancel();
}
}

View File

@ -1,20 +0,0 @@
using System;
namespace FileLiberator
{
public interface IDecryptable : IProcessable
{
event EventHandler<string> DecryptBegin;
event EventHandler<Action<byte[]>> RequestCoverArt;
event EventHandler<string> TitleDiscovered;
event EventHandler<string> AuthorsDiscovered;
event EventHandler<string> NarratorsDiscovered;
event EventHandler<byte[]> CoverImageFilepathDiscovered;
event EventHandler<int> UpdateProgress;
event EventHandler<TimeSpan> UpdateRemainingTime;
event EventHandler<string> DecryptCompleted;
void Cancel();
}
}

View File

@ -1,12 +0,0 @@
using System;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
public interface IDownloadable
{
event EventHandler<string> DownloadBegin;
event EventHandler<DownloadProgress> DownloadProgressChanged;
event EventHandler<string> DownloadCompleted;
}
}

View File

@ -1,4 +0,0 @@
namespace FileLiberator
{
public interface IDownloadableProcessable : IDownloadable, IProcessable { }
}

View File

@ -5,7 +5,7 @@ using Dinah.Core.ErrorHandling;
namespace FileLiberator namespace FileLiberator
{ {
public interface IProcessable public interface IProcessable : IStreamable
{ {
event EventHandler<LibraryBook> Begin; event EventHandler<LibraryBook> Begin;

View File

@ -23,17 +23,12 @@ namespace FileLiberator
.GetLibrary_Flat_NoTracking() .GetLibrary_Flat_NoTracking()
.Where(libraryBook => processable.Validate(libraryBook)); .Where(libraryBook => processable.Validate(libraryBook));
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook) public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
{ {
if (!processable.Validate(libraryBook)) if (validate && !processable.Validate(libraryBook))
return new StatusHandler { "Validation failed" }; return new StatusHandler { "Validation failed" };
return await processable.ProcessBookAsync_NoValidation(libraryBook); Serilog.Log.Logger.Information("Begin " + nameof(ProcessSingleAsync) + " {@DebugInfo}", new
}
public static async Task<StatusHandler> ProcessBookAsync_NoValidation(this IProcessable processable, LibraryBook libraryBook)
{
Serilog.Log.Logger.Information("Begin " + nameof(ProcessBookAsync_NoValidation) + " {@DebugInfo}", new
{ {
libraryBook.Book.Title, libraryBook.Book.Title,
libraryBook.Book.AudibleProductId, libraryBook.Book.AudibleProductId,

View File

@ -0,0 +1,13 @@
using System;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
public interface IStreamable
{
event EventHandler<string> StreamingBegin;
event EventHandler<DownloadProgress> StreamingProgressChanged;
event EventHandler<TimeSpan> StreamingTimeRemaining;
event EventHandler<string> StreamingCompleted;
}
}

View File

@ -34,7 +34,7 @@ namespace FileManager
} }
} }
private static BackgroundFileSystem BookDirectoryFiles { get; } = new BackgroundFileSystem(); private static BackgroundFileSystem BookDirectoryFiles { get; set; }
#endregion #endregion
#region instance #region instance
@ -47,6 +47,7 @@ namespace FileManager
{ {
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList(); extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}"); extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
} }
public void Refresh() public void Refresh()
@ -76,7 +77,15 @@ namespace FileManager
{ {
//If user changed the BooksDirectory, reinitialize. //If user changed the BooksDirectory, reinitialize.
if (storageDir != BookDirectoryFiles.RootDirectory) 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); firstOrNull = BookDirectoryFiles.FindFile(regexPattern, RegexOptions.IgnoreCase);
} }

View File

@ -18,12 +18,19 @@ namespace FileManager
private FileSystemWatcher fileSystemWatcher { get; set; } private FileSystemWatcher fileSystemWatcher { get; set; }
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; } private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; }
private Task backgroundScanner { get; set; } private Task backgroundScanner { get; set; }
private List<string> fsCache { get; set; } private List<string> 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) public string FindFile(string regexPattern, RegexOptions options)
{ {
if (fsCache is null) return null;
lock (fsCache) lock (fsCache)
{ {
return fsCache.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, options)); return fsCache.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, options));
@ -32,8 +39,6 @@ namespace FileManager
public void RefreshFiles() public void RefreshFiles()
{ {
if (fsCache is null) return;
lock (fsCache) lock (fsCache)
{ {
fsCache.Clear(); fsCache.Clear();
@ -41,19 +46,12 @@ namespace FileManager
} }
} }
public void Init(string rootDirectory, string searchPattern, SearchOption searchOptions) private void Init()
{ {
RootDirectory = rootDirectory; Stop();
SearchPattern = searchPattern;
SearchOption = searchOptions;
//Calling CompleteAdding() will cause background scanner to terminate. lock (fsCache)
directoryChangesEvents?.CompleteAdding(); fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
fsCache?.Clear();
directoryChangesEvents?.Dispose();
fileSystemWatcher?.Dispose();
fsCache = Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption).ToList();
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>(); directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory); fileSystemWatcher = new FileSystemWatcher(RootDirectory);
@ -64,28 +62,31 @@ namespace FileManager
fileSystemWatcher.IncludeSubdirectories = true; fileSystemWatcher.IncludeSubdirectories = true;
fileSystemWatcher.EnableRaisingEvents = true; fileSystemWatcher.EnableRaisingEvents = true;
//Wait for background scanner to terminate before reinitializing.
backgroundScanner?.Wait();
backgroundScanner = new Task(BackgroundScanner); backgroundScanner = new Task(BackgroundScanner);
backgroundScanner.Start(); backgroundScanner.Start();
} }
private void Stop()
{
//Stop raising events
fileSystemWatcher?.Dispose();
private void AddUniqueFiles(IEnumerable<string> newFiles) //Calling CompleteAdding() will cause background scanner to terminate.
{ directoryChangesEvents?.CompleteAdding();
foreach (var file in newFiles)
{ //Wait for background scanner to terminate before reinitializing.
AddUniqueFile(file); backgroundScanner?.Wait();
}
} //Dispose of directoryChangesEvents after backgroundScanner exists.
private void AddUniqueFile(string newFile) directoryChangesEvents?.Dispose();
{
if (!fsCache.Contains(newFile)) lock (fsCache)
fsCache.Add(newFile); fsCache.Clear();
} }
private void FileSystemWatcher_Error(object sender, ErrorEventArgs e) private void FileSystemWatcher_Error(object sender, ErrorEventArgs e)
{ {
Init(RootDirectory, SearchPattern, SearchOption); Stop();
Init();
} }
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e) private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
@ -97,12 +98,13 @@ namespace FileManager
private void BackgroundScanner() private void BackgroundScanner()
{ {
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1)) while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
{
lock (fsCache)
UpdateLocalCache(change); UpdateLocalCache(change);
} }
}
private void UpdateLocalCache(FileSystemEventArgs change) private void UpdateLocalCache(FileSystemEventArgs change)
{
lock (fsCache)
{ {
if (change.ChangeType == WatcherChangeTypes.Deleted) if (change.ChangeType == WatcherChangeTypes.Deleted)
{ {
@ -112,15 +114,12 @@ namespace FileManager
{ {
AddPath(change.FullPath); AddPath(change.FullPath);
} }
else if (change.ChangeType == WatcherChangeTypes.Renamed) else if (change.ChangeType == WatcherChangeTypes.Renamed && change is RenamedEventArgs renameChange)
{ {
var renameChange = change as RenamedEventArgs;
RemovePath(renameChange.OldFullPath); RemovePath(renameChange.OldFullPath);
AddPath(renameChange.FullPath); AddPath(renameChange.FullPath);
} }
} }
}
private void RemovePath(string path) private void RemovePath(string path)
{ {
@ -137,6 +136,21 @@ namespace FileManager
else else
AddUniqueFile(path); AddUniqueFile(path);
} }
private void AddUniqueFiles(IEnumerable<string> newFiles)
{
foreach (var file in newFiles)
{
AddUniqueFile(file);
}
}
private void AddUniqueFile(string newFile)
{
if (!fsCache.Contains(newFile))
fsCache.Add(newFile);
}
#endregion #endregion
~BackgroundFileSystem() => Stop();
} }
} }

View File

@ -1,12 +1,19 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks;
namespace FileManager 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 struct PictureDefinition
{ {
public string PictureId { get; } public string PictureId { get; }
@ -27,34 +34,60 @@ namespace FileManager
private static string getPath(PictureDefinition def) private static string getPath(PictureDefinition def)
=> Path.Combine(ImagesDirectory, $"{def.PictureId}{def.Size}.jpg"); => Path.Combine(ImagesDirectory, $"{def.PictureId}{def.Size}.jpg");
private static System.Timers.Timer timer { get; }
static PictureStorage() static PictureStorage()
{ {
timer = new System.Timers.Timer(700) new Task(BackgroundDownloader, TaskCreationOptions.LongRunning)
{ .Start();
AutoReset = true,
Enabled = true
};
timer.Elapsed += (_, __) => timerDownload();
} }
public static event EventHandler<string> PictureCached; public static event EventHandler<PictureCachedEventArgs> PictureCached;
private static BlockingCollection<PictureDefinition> DownloadQueue { get; } = new BlockingCollection<PictureDefinition>();
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>(); private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def) 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); var path = getPath(def);
cache[def]
= File.Exists(path) if (File.Exists(path))
? File.ReadAllBytes(path) {
: null; cache[def] = File.ReadAllBytes(path);
} return (false, cache[def]);
return (cache[def] == null, cache[def] ?? getDefaultImage(def.Size)); }
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];
}
} }
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes) public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes)
=> defaultImages[pictureSize] = bytes; => defaultImages[pictureSize] = bytes;
private static byte[] getDefaultImage(PictureSize size) private static byte[] getDefaultImage(PictureSize size)
@ -62,45 +95,26 @@ namespace FileManager
? defaultImages[size] ? defaultImages[size]
: new byte[0]; : new byte[0];
// necessary to avoid IO errors. ReadAllBytes and WriteAllBytes can conflict in some cases, esp when debugging static void BackgroundDownloader()
private static bool isProcessing;
private static void timerDownload()
{ {
// must live outside try-catch, else 'finally' can reset another thread's lock while (!DownloadQueue.IsCompleted)
if (isProcessing)
return;
try
{ {
isProcessing = true; if (!DownloadQueue.TryTake(out var def, System.Threading.Timeout.InfiniteTimeSpan))
continue;
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;
var bytes = downloadBytes(def); var bytes = downloadBytes(def);
saveFile(def, bytes); saveFile(def, bytes);
lock (cache)
cache[def] = bytes; cache[def] = bytes;
PictureCached?.Invoke(nameof(PictureStorage), def.PictureId); PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes });
}
finally
{
isProcessing = false;
} }
} }
private static HttpClient imageDownloadClient { get; } = new HttpClient(); private static HttpClient imageDownloadClient { get; } = new HttpClient();
private static byte[] downloadBytes(PictureDefinition def) 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; return imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result;
} }

View File

@ -47,18 +47,18 @@ namespace InternalUtilities
// 2 retries == 3 total // 2 retries == 3 total
.RetryAsync(2); .RetryAsync(2);
public static Task<List<Item>> GetLibraryValidatedAsync(Api api) public static Task<List<Item>> 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 // 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: // 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 }); // 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 // 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<List<Item>> getItemsAsync(Api api) private static async Task<List<Item>> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups)
{ {
var items = await api.GetAllLibraryItemsAsync(); var items = await api.GetAllLibraryItemsAsync(responseGroups);
// remove episode parents // remove episode parents
items.RemoveAll(i => i.IsEpisodes); items.RemoveAll(i => i.IsEpisodes);

View File

@ -74,8 +74,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "Appl
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}" 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}.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.ActiveCfg = Release|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = 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 {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} {282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF} {B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1} {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} {F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53} {8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53} {C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
@ -13,7 +13,11 @@
<!-- <PublishSingleFile>true</PublishSingleFile> --> <!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.4.9.0</Version> <Version>5.4.9.280</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE;DEBUG</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using AudibleApi;
using AudibleApi.Authorization; using AudibleApi.Authorization;
using DataLayer; using DataLayer;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -13,7 +12,6 @@ using FileManager;
using InternalUtilities; using InternalUtilities;
using LibationWinForms; using LibationWinForms;
using LibationWinForms.Dialogs; using LibationWinForms.Dialogs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Serilog; using Serilog;
@ -59,8 +57,10 @@ namespace LibationLauncher
ensureSerilogConfig(config); ensureSerilogConfig(config);
configureLogging(config); configureLogging(config);
logStartupState(config); logStartupState(config);
checkForUpdate(config);
#if !DEBUG
checkForUpdate(config);
#endif
Application.Run(new Form1()); Application.Run(new Form1());
} }

View File

@ -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) });
}
}

View File

@ -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
}
}

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,6 +1,6 @@
namespace LibationWinForms.BookLiberation namespace LibationWinForms.BookLiberation
{ {
partial class DecryptForm partial class AudioDecodeForm
{ {
/// <summary> /// <summary>
/// Required designer variable. /// Required designer variable.

View File

@ -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<byte[]> 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<byte[]> 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");
}
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -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
}
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -1,6 +1,7 @@
using System; using Dinah.Core.Windows.Forms;
using System;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core.Windows.Forms;
namespace LibationWinForms.BookLiberation namespace LibationWinForms.BookLiberation
{ {

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -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
/// <summary>
/// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
/// </summary>
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThread(() => Close());
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(() => Dispose());
/// <summary>
/// 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.
/// </summary>
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<byte[]> 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
}
}

View File

@ -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");
}
}

View File

@ -1,4 +1,7 @@
namespace LibationWinForms.BookLiberation using DataLayer;
using System;
namespace LibationWinForms.BookLiberation
{ {
partial class DownloadForm partial class DownloadForm
{ {
@ -95,6 +98,7 @@
} }
#endregion #endregion
private System.Windows.Forms.Label filenameLbl; private System.Windows.Forms.Label filenameLbl;

View File

@ -1,10 +1,13 @@
using System; using Dinah.Core.Net.Http;
using System.Windows.Forms;
using Dinah.Core.Windows.Forms; using Dinah.Core.Windows.Forms;
using LibationWinForms.BookLiberation.BaseForms;
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.BookLiberation namespace LibationWinForms.BookLiberation
{ {
public partial class DownloadForm : Form public partial class DownloadForm : LiberationBaseForm
{ {
public DownloadForm() public DownloadForm()
{ {
@ -14,23 +17,27 @@ namespace LibationWinForms.BookLiberation
filenameLbl.Text = ""; filenameLbl.Text = "";
} }
// thread-safe UI updates
public void UpdateFilename(string title) => filenameLbl.UIThread(() => filenameLbl.Text = title);
public void DownloadProgressChanged(long BytesReceived, long? TotalBytesToReceive) #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 // this won't happen with download file. it will happen with download string
if (!TotalBytesToReceive.HasValue || TotalBytesToReceive.Value <= 0) if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
return; 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 d = double.Parse(downloadProgress.BytesReceived.ToString()) / double.Parse(downloadProgress.TotalBytesToReceive.Value.ToString()) * 100.0;
var i = int.Parse(Math.Truncate(d).ToString()); var i = int.Parse(Math.Truncate(d).ToString());
progressBar1.UIThread(() => progressBar1.Value = i); progressBar1.UIThread(() => progressBar1.Value = i);
lastDownloadProgress = DateTime.Now; lastDownloadProgress = DateTime.Now;
} }
#endregion
#region timer #region timer
private Timer timer { get; } = new Timer { Interval = 1000 }; private Timer timer { get; } = new Timer { Interval = 1000 };

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -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}");
}
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -1,12 +1,11 @@
using System; using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationWinForms.BookLiberation.BaseForms;
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Windows.Forms;
using FileLiberator;
namespace LibationWinForms.BookLiberation namespace LibationWinForms.BookLiberation
{ {
@ -53,350 +52,114 @@ namespace LibationWinForms.BookLiberation
{ {
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null) public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null)
{ {
Serilog.Log.Logger.Information("Begin backup single {@DebugInfo}", new { libraryBook?.Book?.AudibleProductId }); Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
var backupBook = getWiredUpBackupBook(completedAction); var logMe = LogMe.RegisterForm();
var backupBook = CreateBackupBook(completedAction, logMe);
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook);
// continue even if libraryBook is null. we'll display even that in the processing box // continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync(); await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
unsubscribeEvents();
} }
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null) public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
{ {
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync)); Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var backupBook = getWiredUpBackupBook(completedAction);
var automatedBackupsForm = new AutomatedBackupsForm(); var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook, automatedBackupsForm); var backupBook = CreateBackupBook(completedAction, logMe);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync(); await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
unsubscribeEvents();
} }
public static async Task ConvertAllBooksAsync() public static async Task ConvertAllBooksAsync()
{ {
Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync)); Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync));
var convertBook = new ConvertToMp3();
convertBook.Begin += (_, l) => wireUpEvents(convertBook, l, "Converting");
var automatedBackupsForm = new AutomatedBackupsForm(); var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm); var logMe = LogMe.RegisterForm(automatedBackupsForm);
void statusUpdate(object _, string str) => logMe.Info("- " + str); var convertBook = CreateProcessable<ConvertToMp3, AudioConvertForm>(logMe);
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(); await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
convertBook.Begin -= convertBookBegin;
convertBook.StatusUpdate -= statusUpdate;
convertBook.Completed -= convertBookCompleted;
}
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> 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<LibraryBook> completedAction = null) public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
{ {
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync)); Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var downloadPdf = getWiredUpDownloadPdf(completedAction); var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe, completedAction);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync(); await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
} }
private static DownloadPdf getWiredUpDownloadPdf(EventHandler<LibraryBook> completedAction) private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe)
{ {
var downloadPdf = new DownloadPdf(); var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
downloadPdf.Begin += (_, __) => wireUpEvents(downloadPdf); //Chain pdf download on DownloadDecryptBook.Completed
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
{
await downloadPdf.TryProcessAsync(e);
completedAction(sender, e);
}
if (completedAction != null) var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
downloadPdf.Completed += completedAction; return downloadDecryptBook;
return downloadPdf;
} }
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false) public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
{ {
var downloadDialog = new DownloadForm(); Serilog.Log.Logger.Information($"Begin {nameof(DownloadFile)} for {url}");
downloadDialog.UpdateFilename(destination);
downloadDialog.Show();
new System.Threading.Thread(() => void onDownloadFileStreamingCompleted(object sender, string savedFile)
{ {
var downloadFile = new DownloadFile(); Serilog.Log.Logger.Information($"Completed {nameof(DownloadFile)} for {url}. Saved to {savedFile}");
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.UIThread(() =>
downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive)
);
downloadFile.DownloadCompleted += (_, __) => downloadDialog.UIThread(() =>
{
downloadDialog.Close();
if (showDownloadCompletedDialog) if (showDownloadCompletedDialog)
MessageBox.Show("File downloaded"); MessageBox.Show($"File downloaded to:{Environment.NewLine}{Environment.NewLine}{savedFile}");
});
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 var downloadFile = new DownloadFile();
private static void wireUpEvents(IDownloadableProcessable downloadable) var downloadForm = new DownloadForm();
{ downloadForm.RegisterFileLiberator(downloadFile);
#region create form downloadFile.StreamingCompleted += onDownloadFileStreamingCompleted;
var downloadDialog = new DownloadForm();
#endregion
// extra complexity for wiring up download form: async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination);
// case 1: download is needed new Task(runDownload).Start();
// 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 /// <summary>
void fileDownloadCompleted(object _, string __) => downloadDialog.Close(); /// Create a new <see cref="IProcessable"/> and links it to a new <see cref="LiberationBaseForm"/>.
/// </summary>
void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress) /// <typeparam name="TProcessable">The <see cref="IProcessable"/> derived type to create.</typeparam>
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive); /// <typeparam name="TForm">The <see cref="LiberationBaseForm"/> derived Form to create on <see cref="IProcessable.Begin"/>, Show on <see cref="IStreamable.StreamingBegin"/>, Close on <see cref="IStreamable.StreamingCompleted"/>, and Dispose on <see cref="IProcessable.Completed"/> </typeparam>
/// <param name="logMe">The logger</param>
void unsubscribe(object _ = null, EventArgs __ = null) /// <param name="completedAction">An additional event handler to handle <see cref="IProcessable.Completed"/></param>
/// <returns>A new <see cref="IProcessable"/> of type <typeparamref name="TProcessable"/></returns>
private static TProcessable CreateProcessable<TProcessable, TForm>(LogMe logMe, EventHandler<LibraryBook> completedAction = null)
where TForm : LiberationBaseForm, new()
where TProcessable : IProcessable, new()
{ {
downloadable.DownloadBegin -= downloadBegin; var strProc = new TProcessable();
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 strProc.Begin += (sender, libraryBook) =>
// 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) var processForm = new TForm();
downloadDialog.Dispose(); processForm.RegisterFileLiberator(strProc, logMe);
} processForm.OnBegin(sender, libraryBook);
#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<byte[]> 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 strProc.Completed += completedAction;
decryptBook.DecryptBegin += decryptBegin;
decryptBook.TitleDiscovered += titleDiscovered; return strProc;
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 internal abstract class BackupRunner
{ {
protected LogMe LogMe { get; } protected LogMe LogMe { get; }
protected IProcessable Processable { get; } protected IProcessable Processable { get; }
@ -410,9 +173,9 @@ namespace LibationWinForms.BookLiberation
} }
protected abstract Task RunAsync(); protected abstract Task RunAsync();
protected abstract string SkipDialogText { get; } protected abstract string SkipDialogText { get; }
protected abstract MessageBoxButtons SkipDialogButtons { get; } protected abstract MessageBoxButtons SkipDialogButtons { get; }
protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; }
protected abstract DialogResult CreateSkipFileResult { get; } protected abstract DialogResult CreateSkipFileResult { get; }
public async Task RunBackupAsync() public async Task RunBackupAsync()
@ -432,13 +195,13 @@ namespace LibationWinForms.BookLiberation
LogMe.Info("DONE"); LogMe.Info("DONE");
} }
protected async Task<bool> ProcessOneAsync(Func<LibraryBook, Task<StatusHandler>> func, LibraryBook libraryBook) protected async Task<bool> ProcessOneAsync(LibraryBook libraryBook, bool validate)
{ {
string logMessage; string logMessage;
try try
{ {
var statusHandler = await func(libraryBook); var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
if (statusHandler.IsSuccess) if (statusHandler.IsSuccess)
return true; return true;
@ -476,7 +239,7 @@ $@" Title: {libraryBook.Book.Title}
details = "[Error retrieving details]"; 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) if (dialogResult == DialogResult.Abort)
return false; return false;
@ -495,7 +258,8 @@ Created new 'skip' file
return true; return true;
} }
} }
class BackupSingle : BackupRunner
internal class BackupSingle : BackupRunner
{ {
private LibraryBook _libraryBook { get; } private LibraryBook _libraryBook { get; }
@ -508,6 +272,7 @@ 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. - Click NO to skip the book this time only. We'll try again later.
".Trim(); ".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo; protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2;
protected override DialogResult CreateSkipFileResult => DialogResult.Yes; protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook) public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
@ -519,10 +284,11 @@ An error occurred while trying to process this book. Skip this book permanently?
protected override async Task RunAsync() protected override async Task RunAsync()
{ {
if (_libraryBook is not null) if (_libraryBook is not null)
await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook); await ProcessOneAsync(_libraryBook, validate: true);
} }
} }
class BackupLoop : BackupRunner
internal class BackupLoop : BackupRunner
{ {
protected override string SkipDialogText => @" protected override string SkipDialogText => @"
An error occurred while trying to process this book. An error occurred while trying to process this book.
@ -535,6 +301,7 @@ An error occurred while trying to process this book.
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.) - IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim(); ".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore; protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore; protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm) public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
@ -545,7 +312,7 @@ An error occurred while trying to process this book.
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here // support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
foreach (var libraryBook in Processable.GetValidLibraryBooks()) foreach (var libraryBook in Processable.GetValidLibraryBooks())
{ {
var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook); var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
if (!keepGoing) if (!keepGoing)
return; return;

View File

@ -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));
}
}
}

View File

@ -1,21 +1,21 @@
using System; using AudibleApi;
using InternalUtilities;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using AudibleApi;
using InternalUtilities;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
public partial class AccountsDialog : Form public partial class AccountsDialog : Form
{ {
const string COL_Delete = nameof(DeleteAccount); private const string COL_Delete = nameof(DeleteAccount);
const string COL_LibraryScan = nameof(LibraryScan); private const string COL_LibraryScan = nameof(LibraryScan);
const string COL_AccountId = nameof(AccountId); private const string COL_AccountId = nameof(AccountId);
const string COL_AccountName = nameof(AccountName); private const string COL_AccountName = nameof(AccountName);
const string COL_Locale = nameof(Locale); private const string COL_Locale = nameof(Locale);
Form1 _parent { get; } private Form1 _parent { get; }
public AccountsDialog(Form1 parent) public AccountsDialog(Form1 parent)
{ {
@ -100,7 +100,7 @@ namespace LibationWinForms.Dialogs
this.Close(); this.Close();
} }
class AccountDto private class AccountDto
{ {
public string AccountId { get; set; } public string AccountId { get; set; }
public string AccountName { get; set; } public string AccountName { get; set; }

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,9 +1,8 @@
using System; using FileManager;
using System.Collections.Generic; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core;
using FileManager;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,9 +1,9 @@
using System; using Dinah.Core;
using FileManager;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core;
using FileManager;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
@ -13,7 +13,7 @@ namespace LibationWinForms.Dialogs
{ {
public string Description { get; } public string Description { get; }
public Configuration.KnownDirectories Value { get; } public Configuration.KnownDirectories Value { get; }
private DirectorySelectControl _parentControl; private readonly DirectorySelectControl _parentControl;
public string FullPath => _parentControl.AddSubDirectoryToPath(Configuration.GetKnownDirectoryPath(Value)); public string FullPath => _parentControl.AddSubDirectoryToPath(Configuration.GetKnownDirectoryPath(Value));

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,22 +1,21 @@
using System; using FileManager;
using System;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using FileManager;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
public partial class EditQuickFilters : Form public partial class EditQuickFilters : Form
{ {
const string BLACK_UP_POINTING_TRIANGLE = "\u25B2"; private const string BLACK_UP_POINTING_TRIANGLE = "\u25B2";
const string BLACK_DOWN_POINTING_TRIANGLE = "\u25BC"; 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); private Form1 _parent { get; }
const string COL_Delete = nameof(Delete);
const string COL_Filter = nameof(Filter);
const string COL_MoveUp = nameof(MoveUp);
const string COL_MoveDown = nameof(MoveDown);
Form1 _parent { get; }
public EditQuickFilters(Form1 parent) public EditQuickFilters(Form1 parent)
{ {

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,8 +1,9 @@
using System; using ApplicationServices;
using System.Windows.Forms;
using ApplicationServices;
using InternalUtilities; using InternalUtilities;
using LibationWinForms.Login; using LibationWinForms.Login;
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,6 +1,7 @@
using System; using FileManager;
using System;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using FileManager;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login namespace LibationWinForms.Dialogs.Login

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,7 +1,8 @@
using System; using Dinah.Core;
using System.Windows.Forms;
using Dinah.Core;
using InternalUtilities; using InternalUtilities;
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login namespace LibationWinForms.Dialogs.Login
{ {

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Windows.Forms; using System.Windows.Forms;

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,11 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login namespace LibationWinForms.Dialogs.Login
@ -14,7 +8,7 @@ namespace LibationWinForms.Dialogs.Login
{ {
private RadioButton[] radioButtons { get; } private RadioButton[] radioButtons { get; }
AudibleApi.MfaConfig _mfaConfig { get; } private AudibleApi.MfaConfig _mfaConfig { get; }
public MfaDialog(AudibleApi.MfaConfig mfaConfig) public MfaDialog(AudibleApi.MfaConfig mfaConfig)
{ {
@ -32,7 +26,8 @@ namespace LibationWinForms.Dialogs.Login
setRadioButton(1, this.radioButton2); setRadioButton(1, this.radioButton2);
setRadioButton(2, this.radioButton3); setRadioButton(2, this.radioButton3);
Serilog.Log.Logger.Information("{@DebugInfo}", new { Serilog.Log.Logger.Information("{@DebugInfo}", new
{
paramButtonCount = mfaConfig.Buttons.Count, paramButtonCount = mfaConfig.Buttons.Count,
visibleRadioButtonCount = radioButtons.Count(rb => rb.Visible) visibleRadioButtonCount = radioButtons.Count(rb => rb.Visible)
}); });
@ -65,7 +60,8 @@ namespace LibationWinForms.Dialogs.Login
{ {
var selected = radioButtons.FirstOrDefault(rb => rb.Checked); 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_visible = radioButton1.Visible,
rb1_checked = radioButton1.Checked, rb1_checked = radioButton1.Checked,

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,7 +1,8 @@
using System; using AudibleApi;
using AudibleApi;
using InternalUtilities; using InternalUtilities;
using LibationWinForms.Dialogs.Login; using LibationWinForms.Dialogs.Login;
using System;
using System.Linq;
namespace LibationWinForms.Login namespace LibationWinForms.Login
{ {

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login namespace LibationWinForms.Dialogs.Login

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,7 +1,8 @@
using System; using Dinah.Core;
using System;
using System.Linq;
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -0,0 +1,189 @@

namespace LibationWinForms.Dialogs
{
partial class RemoveBooksDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

View File

@ -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<LibraryBook> _libraryBooks;
private readonly SortableBindingList2<RemovableGridEntry> _removableGridEntries;
private readonly string _labelFormat;
private int SelectedCount => SelectedEntries?.Count() ?? 0;
private IEnumerable<RemovableGridEntry> 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<RemovableGridEntry>(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<bool>();
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);
}
}
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -1,10 +1,8 @@
using System; using InternalUtilities;
using System.Collections.Generic; using System;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Collections.Generic;
using System.Windows.Forms; using System.Windows.Forms;
using InternalUtilities;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
@ -12,7 +10,7 @@ namespace LibationWinForms.Dialogs
{ {
public List<Account> CheckedAccounts { get; } = new List<Account>(); public List<Account> CheckedAccounts { get; } = new List<Account>();
Form1 _parent { get; } private Form1 _parent { get; }
public ScanAccountsDialog(Form1 parent) public ScanAccountsDialog(Form1 parent)
{ {
@ -21,7 +19,7 @@ namespace LibationWinForms.Dialogs
InitializeComponent(); InitializeComponent();
} }
class listItem private class listItem
{ {
public Account Account { get; set; } public Account Account { get; set; }
public string Text { get; set; } public string Text { get; set; }

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,15 +1,16 @@
using System; using Dinah.Core;
using FileManager;
using System;
using System.Linq;
using System.IO; using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core;
using FileManager;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
public partial class SettingsDialog : Form public partial class SettingsDialog : Form
{ {
Configuration config { get; } = Configuration.Instance; private Configuration config { get; } = Configuration.Instance;
Func<string, string> desc { get; } = Configuration.GetDescription; private Func<string, string> desc { get; } = Configuration.GetDescription;
public SettingsDialog() => InitializeComponent(); public SettingsDialog() => InitializeComponent();

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs

View File

@ -1,4 +1,5 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -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);
}
}
}
}

View File

@ -39,6 +39,9 @@
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = 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.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@ -128,7 +131,8 @@
this.noAccountsYetAddAccountToolStripMenuItem, this.noAccountsYetAddAccountToolStripMenuItem,
this.scanLibraryToolStripMenuItem, this.scanLibraryToolStripMenuItem,
this.scanLibraryOfAllAccountsToolStripMenuItem, this.scanLibraryOfAllAccountsToolStripMenuItem,
this.scanLibraryOfSomeAccountsToolStripMenuItem}); this.scanLibraryOfSomeAccountsToolStripMenuItem,
this.removeLibraryBooksToolStripMenuItem});
this.importToolStripMenuItem.Name = "importToolStripMenuItem"; this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20); this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import"; this.importToolStripMenuItem.Text = "&Import";
@ -161,6 +165,29 @@
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts..."; this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click); 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 // liberateToolStripMenuItem
// //
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
@ -368,5 +395,8 @@
private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem convertAllM4bToMp3ToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem convertAllM4bToMp3ToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeLibraryBooksToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeAllAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeSomeAccountsToolStripMenuItem;
} }
} }

View File

@ -1,9 +1,4 @@
using System; using ApplicationServices;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.Drawing; using Dinah.Core.Drawing;
@ -11,6 +6,11 @@ using Dinah.Core.Windows.Forms;
using FileManager; using FileManager;
using InternalUtilities; using InternalUtilities;
using LibationWinForms.Dialogs; using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms namespace LibationWinForms
{ {
@ -48,13 +48,6 @@ namespace LibationWinForms
this.Load += (_, __) => RestoreSizeAndLocation(); this.Load += (_, __) => RestoreSizeAndLocation();
this.Load += (_, __) => RefreshImportMenu(); this.Load += (_, __) => RefreshImportMenu();
// start background service
this.Load += (_, __) => startBackgroundImageDownloader();
}
private static void startBackgroundImageDownloader()
{
// load default/missing cover images. this will also initiate the background image downloader
var format = System.Drawing.Imaging.ImageFormat.Jpeg; var format = System.Drawing.Imaging.ImageFormat.Jpeg;
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format)); 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._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
@ -70,6 +63,7 @@ namespace LibationWinForms
// also applies filter. ONLY call AFTER loading grid // also applies filter. ONLY call AFTER loading grid
loadInitialQuickFilterState(); loadInitialQuickFilterState();
} }
private void Form1_FormClosing(object sender, FormClosingEventArgs e) private void Form1_FormClosing(object sender, FormClosingEventArgs e)
@ -148,7 +142,7 @@ namespace LibationWinForms
} }
#region reload grid #region reload grid
bool isProcessingGridSelect = false; private bool isProcessingGridSelect = false;
private void reloadGrid() private void reloadGrid()
{ {
// suppressed filter while init'ing UI // suppressed filter while init'ing UI
@ -161,7 +155,7 @@ namespace LibationWinForms
doFilter(lastGoodFilter); doFilter(lastGoodFilter);
} }
ProductsGrid currProductsGrid; private ProductsGrid currProductsGrid;
private void setGrid() private void setGrid()
{ {
SuspendLayout(); SuspendLayout();
@ -264,7 +258,7 @@ namespace LibationWinForms
} }
private void filterBtn_Click(object sender, EventArgs e) => doFilter(); private void filterBtn_Click(object sender, EventArgs e) => doFilter();
string lastGoodFilter = ""; private string lastGoodFilter = "";
private void doFilter(string filterString) private void doFilter(string filterString)
{ {
this.filterSearchTb.Text = filterString; this.filterSearchTb.Text = filterString;
@ -300,6 +294,16 @@ namespace LibationWinForms
scanLibraryToolStripMenuItem.Visible = count == 1; scanLibraryToolStripMenuItem.Visible = count == 1;
scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1; scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1;
scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1; scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1;
removeLibraryBooksToolStripMenuItem.Visible = count != 0;
if (count == 1)
{
removeLibraryBooksToolStripMenuItem.Click += removeThisAccountToolStripMenuItem_Click;
}
removeSomeAccountsToolStripMenuItem.Visible = count > 1;
removeAllAccountsToolStripMenuItem.Visible = count > 1;
} }
private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e) private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e)
@ -335,6 +339,42 @@ namespace LibationWinForms
scanLibraries(scanAccountsDialog.CheckedAccounts); 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<Account> accounts) => scanLibraries(accounts.ToArray()); private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
private void scanLibraries(params Account[] accounts) private void scanLibraries(params Account[] accounts)
{ {
@ -422,7 +462,7 @@ namespace LibationWinForms
QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked; QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked;
} }
object quickFilterTag { get; } = new object(); private object quickFilterTag { get; } = new object();
public void UpdateFilterDropDown() public void UpdateFilterDropDown()
{ {
// remove old // remove old
@ -455,5 +495,6 @@ namespace LibationWinForms
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog(); private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
#endregion #endregion
} }
} }

View File

@ -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.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using ApplicationServices;
using DataLayer;
namespace LibationWinForms namespace LibationWinForms
{ {
internal class GridEntry internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{ {
private LibraryBook libraryBook { get; } #region implementation properties
private Book book => libraryBook.Book;
public Book GetBook() => book;
public LibraryBook GetLibraryBook() => libraryBook;
public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook;
// hide from public fields from Data Source GUI with [Browsable(false)] // hide from public fields from Data Source GUI with [Browsable(false)]
[Browsable(false)] [Browsable(false)]
public string AudibleProductId => book.AudibleProductId; public string AudibleProductId => Book.AudibleProductId;
[Browsable(false)] [Browsable(false)]
public string Tags => book.UserDefinedItem.Tags; public LibraryBook LibraryBook { get; }
[Browsable(false)]
public IEnumerable<string> 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);
// displayValues is what gets displayed #endregion
// 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<string, string> displayValues { get; } = new Dictionary<string, string>();
public bool TryDisplayValue(string key, out string value) => displayValues.TryGetValue(key, out value);
public Image Cover => private Book Book => LibraryBook.Book;
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(book.PictureId, FileManager.PictureSize._80x80); private Image _cover;
public string Title public GridEntry(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
_memberValues = CreateMemberValueDictionary();
//Get cover art. If it's default, subscribe to PictureCached
{
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
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;
}
}
#region Data Source properties
public Image Cover
{ {
get get
{ {
displayValues[nameof(Title)] = book.Title; return _cover;
return getSortName(book.Title);
} }
} private set
public string Authors => book.AuthorNames;
public string Narrators => book.NarratorNames;
public int Length
{ {
get _cover = value;
{ NotifyPropertyChanged();
displayValues[nameof(Length)]
= book.LengthInMinutes == 0
? ""
: $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min";
return book.LengthInMinutes;
} }
} }
public string Series public string ProductRating { get; }
{ public string PurchaseDate { get; }
get public string MyRating { get; }
{ public string Series { get; }
displayValues[nameof(Series)] = book.SeriesNames; public string Title { get; }
return getSortName(book.SeriesNames); 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
private static string[] sortPrefixIgnores { get; } = new[] { "the", "a", "an" }; #region Data Sorting
private static string getSortName(string unformattedName)
private Dictionary<string, Func<object>> _memberValues { get; }
/// <summary>
/// Create getters for all member object values by name
/// </summary>
private Dictionary<string, Func<object>> 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<Type, IComparer> _memberTypeComparers = new()
{
{ typeof(string), new ObjectComparer<string>() },
{ typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberatedState), new ObjectComparer<LiberatedState>() },
};
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 var sortName = unformattedName
.Replace("|", "") .Replace("|", "")
@ -89,110 +144,93 @@ namespace LibationWinForms
.ToLowerInvariant() .ToLowerInvariant()
.Trim(); .Trim();
if (sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " "))) if (_sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " ")))
sortName = sortName.Substring(sortName.IndexOf(" ") + 1).TrimStart(); sortName = sortName.Substring(sortName.IndexOf(" ") + 1).TrimStart();
return sortName; return sortName;
} }
private string descriptionCache = null; #endregion
public string Description
#region Static library display functions
public static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus)
{ {
get (string libState, string image_lib) = liberatedStatus switch
{ {
// HtmlAgilityPack is expensive. cache results LiberatedState.Liberated => ("Liberated", "green"),
if (descriptionCache is null) 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
{ {
if (book.Description is null) PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"),
descriptionCache = ""; PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"),
else 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);
}
/// <summary>
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
/// </summary>
private static string GetDescriptionDisplay(Book book)
{ {
var doc = new HtmlAgilityPack.HtmlDocument(); var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(book.Description); doc.LoadHtml(book.Description);
var noHtml = doc.DocumentNode.InnerText; var noHtml = doc.DocumentNode.InnerText;
descriptionCache return
= noHtml.Length < 63 noHtml.Length < 63 ?
? noHtml noHtml :
: noHtml.Substring(0, 60) + "..."; noHtml.Substring(0, 60) + "...";
}
} }
return descriptionCache; /// <summary>
} /// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
} /// Maximum of 5 text rows will fit in 80-pixel row height.
/// </summary>
public string Category => string.Join(" > ", book.CategoriesNames); private static string GetMiscDisplay(LibraryBook libraryBook)
// 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
{
get
{
displayValues[nameof(Product_Rating)] = starString(book.Rating);
return firstScore(book.Rating);
}
}
public string Purchase_Date
{
get
{
displayValues[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d");
return libraryBook.DateAdded.ToString("yyyy-MM-dd HH:mm:ss");
}
}
public string My_Rating
{
get
{
displayValues[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating);
return firstScore(book.UserDefinedItem.Rating);
}
}
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<string>(); var details = new List<string>();
var locale var locale = ValueOrDefault(libraryBook.Book.Locale, "[unknown]");
= string.IsNullOrWhiteSpace(book.Locale) var acct = ValueOrDefault(libraryBook.Account, "[unknown]");
? "[unknown]"
: book.Locale;
var acct
= string.IsNullOrWhiteSpace(libraryBook.Account)
? "[unknown]"
: libraryBook.Account;
details.Add($"Account: {locale} - {acct}"); details.Add($"Account: {locale} - {acct}");
if (book.HasPdf) if (libraryBook.Book.HasPdf)
details.Add("Has PDF"); details.Add("Has PDF");
if (book.IsAbridged) if (libraryBook.Book.IsAbridged)
details.Add("Abridged"); details.Add("Abridged");
if (book.DatePublished.HasValue) if (libraryBook.Book.DatePublished.HasValue)
details.Add($"Date pub'd: {book.DatePublished.Value:MM/dd/yyyy}"); 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 // this goes last since it's most likely to have a line-break
if (!string.IsNullOrWhiteSpace(book.Publisher)) if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
details.Add($"Pub: {book.Publisher.Trim()}"); details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
if (!details.Any()) if (!details.Any())
return "[details not imported]"; return "[details not imported]";
return string.Join("\r\n", details); return string.Join("\r\n", details);
} }
}
//Maybe add to Dinah StringExtensions?
private static string ValueOrDefault(string value, string defaultValue)
=> string.IsNullOrWhiteSpace(value) ? defaultValue : value;
#endregion
} }
} }

View File

@ -0,0 +1,11 @@
using System;
using System.Collections;
namespace LibationWinForms
{
internal interface IMemberComparable
{
IComparer GetMemberComparer(Type memberType);
object GetMemberValue(string memberName);
}
}

View File

@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
@ -12,8 +13,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" /> <ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
<ProjectReference Include="..\WindowsDesktopUtilities\WindowsDesktopUtilities.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.ComponentModel;
namespace LibationWinForms
{
internal class MemberComparer<T> : IComparer<T> 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;
}
}

View File

@ -1,9 +1,6 @@
using System; using LibationWinForms.Dialogs;
using System.Collections.Generic; using System;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LibationWinForms.Dialogs;
namespace LibationWinForms namespace LibationWinForms
{ {

View File

@ -1,7 +1,8 @@
using System; using System;
using System.Windows.Forms; using System.Linq;
using Dinah.Core.Logging; using Dinah.Core.Logging;
using Serilog; using Serilog;
using System.Windows.Forms;
namespace LibationWinForms namespace LibationWinForms
{ {
@ -16,7 +17,7 @@ Warning: verbose logging is enabled.
This should be used for debugging only. It creates many This should be used for debugging only. It creates many
more logs and debug files, neither of which are as more logs and debug files, neither of which are as
strictly anonomous. strictly anonymous.
When you are finished debugging, it's highly recommended When you are finished debugging, it's highly recommended
to set your debug MinimumLevel to Information and restart to set your debug MinimumLevel to Information and restart

View File

@ -0,0 +1,10 @@
using System;
using System.Collections;
namespace LibationWinForms
{
internal class ObjectComparer<T> : IComparer where T : IComparable
{
public int Compare(object x, object y) => ((T)x).CompareTo(y);
}
}

View File

@ -29,8 +29,10 @@
private void InitializeComponent() private void InitializeComponent()
{ {
this.components = new System.ComponentModel.Container(); 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.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView(); this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.dataGridViewImageButtonBoxColumn1 = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn(); this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
@ -43,6 +45,7 @@
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn11 = 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.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout(); this.SuspendLayout();
@ -53,9 +56,13 @@
// //
// gridEntryDataGridView // gridEntryDataGridView
// //
this.gridEntryDataGridView.AllowUserToAddRows = false;
this.gridEntryDataGridView.AllowUserToDeleteRows = false;
this.gridEntryDataGridView.AllowUserToResizeRows = false;
this.gridEntryDataGridView.AutoGenerateColumns = false; this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.dataGridViewImageButtonBoxColumn1,
this.dataGridViewImageColumn1, this.dataGridViewImageColumn1,
this.dataGridViewTextBoxColumn1, this.dataGridViewTextBoxColumn1,
this.dataGridViewTextBoxColumn2, this.dataGridViewTextBoxColumn2,
@ -67,19 +74,46 @@
this.dataGridViewTextBoxColumn8, this.dataGridViewTextBoxColumn8,
this.dataGridViewTextBoxColumn9, this.dataGridViewTextBoxColumn9,
this.dataGridViewTextBoxColumn10, this.dataGridViewTextBoxColumn10,
this.dataGridViewTextBoxColumn11}); this.dataGridViewTextBoxColumn11,
this.dataGridViewImageButtonBoxColumn2});
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource; this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58); 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.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220); 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; 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 // dataGridViewImageColumn1
// //
this.dataGridViewImageColumn1.DataPropertyName = "Cover"; this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover"; this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1"; this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true; this.dataGridViewImageColumn1.ReadOnly = true;
this.dataGridViewImageColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.dataGridViewImageColumn1.ToolTipText = "Cover Art";
this.dataGridViewImageColumn1.Width = 80;
// //
// dataGridViewTextBoxColumn1 // dataGridViewTextBoxColumn1
// //
@ -87,6 +121,7 @@
this.dataGridViewTextBoxColumn1.HeaderText = "Title"; this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1"; this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true; this.dataGridViewTextBoxColumn1.ReadOnly = true;
this.dataGridViewTextBoxColumn1.Width = 200;
// //
// dataGridViewTextBoxColumn2 // dataGridViewTextBoxColumn2
// //
@ -108,6 +143,7 @@
this.dataGridViewTextBoxColumn4.HeaderText = "Length"; this.dataGridViewTextBoxColumn4.HeaderText = "Length";
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4"; this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
this.dataGridViewTextBoxColumn4.ReadOnly = true; this.dataGridViewTextBoxColumn4.ReadOnly = true;
this.dataGridViewTextBoxColumn4.ToolTipText = "Recording Length";
// //
// dataGridViewTextBoxColumn5 // dataGridViewTextBoxColumn5
// //
@ -130,26 +166,28 @@
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7"; this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
this.dataGridViewTextBoxColumn7.ReadOnly = true; this.dataGridViewTextBoxColumn7.ReadOnly = true;
// //
// dataGridViewTextBoxColumn8 // ProductRating
// //
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating"; this.dataGridViewTextBoxColumn8.DataPropertyName = "ProductRating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating"; this.dataGridViewTextBoxColumn8.HeaderText = "Product Rating";
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8"; this.dataGridViewTextBoxColumn8.Name = "ProductRating";
this.dataGridViewTextBoxColumn8.ReadOnly = true; this.dataGridViewTextBoxColumn8.ReadOnly = true;
this.dataGridViewTextBoxColumn8.Width = 108;
// //
// dataGridViewTextBoxColumn9 // PurchaseDate
// //
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date"; this.dataGridViewTextBoxColumn9.DataPropertyName = "PurchaseDate";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date"; this.dataGridViewTextBoxColumn9.HeaderText = "Purchase Date";
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9"; this.dataGridViewTextBoxColumn9.Name = "PurchaseDate";
this.dataGridViewTextBoxColumn9.ReadOnly = true; this.dataGridViewTextBoxColumn9.ReadOnly = true;
// //
// dataGridViewTextBoxColumn10 // MyRating
// //
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating"; this.dataGridViewTextBoxColumn10.DataPropertyName = "MyRating";
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating"; this.dataGridViewTextBoxColumn10.HeaderText = "My Rating";
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10"; this.dataGridViewTextBoxColumn10.Name = "MyRating";
this.dataGridViewTextBoxColumn10.ReadOnly = true; this.dataGridViewTextBoxColumn10.ReadOnly = true;
this.dataGridViewTextBoxColumn10.Width = 108;
// //
// dataGridViewTextBoxColumn11 // dataGridViewTextBoxColumn11
// //
@ -157,14 +195,25 @@
this.dataGridViewTextBoxColumn11.HeaderText = "Misc"; this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11"; this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true; 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 // ProductsGrid
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView); this.Controls.Add(this.gridEntryDataGridView);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "ProductsGrid"; this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(434, 329); this.Size = new System.Drawing.Size(1505, 380);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false); this.ResumeLayout(false);
@ -175,6 +224,7 @@
private System.Windows.Forms.BindingSource gridEntryBindingSource; private System.Windows.Forms.BindingSource gridEntryBindingSource;
private System.Windows.Forms.DataGridView gridEntryDataGridView; private System.Windows.Forms.DataGridView gridEntryDataGridView;
private LiberateDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn1;
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1; private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1; private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2; private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
@ -187,5 +237,6 @@
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9; private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10; private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11; private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
private EditTagsDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn2;
} }
} }

View File

@ -1,14 +1,12 @@
using System; using ApplicationServices;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.Collections.Generic;
using Dinah.Core.DataBinding;
using Dinah.Core.Windows.Forms; using Dinah.Core.Windows.Forms;
using LibationWinForms.Dialogs; using LibationWinForms.Dialogs;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms namespace LibationWinForms
{ {
@ -23,165 +21,59 @@ namespace LibationWinForms
// AudibleDTO // AudibleDTO
// GridEntry // GridEntry
// - go to Design view // - go to Design view
// - click on Data Sources > ProductItem. drowdown: DataGridView // - click on Data Sources > ProductItem. dropdown: DataGridView
// - drag/drop ProductItem on design surface // - drag/drop ProductItem on design surface
// AS OF AUGUST 2021 THIS DOES NOT WORK IN VS2019 WITH .NET-5 PROJECTS
public partial class ProductsGrid : UserControl public partial class ProductsGrid : UserControl
{ {
public event EventHandler<int> VisibleCountChanged; public event EventHandler<int> VisibleCountChanged;
public event EventHandler BackupCountsChanged; public event EventHandler BackupCountsChanged;
private const string EDIT_TAGS = "Edit Tags";
private const string LIBERATE = "Liberate";
// alias // alias
private DataGridView dataGridView => gridEntryDataGridView; private DataGridView _dataGridView => gridEntryDataGridView;
public ProductsGrid() public ProductsGrid()
{ {
InitializeComponent(); InitializeComponent();
formatDataGridView();
addLiberateButtons();
addEditTagsButtons();
formatColumns();
manageLiveImageUpdateSubscriptions();
enableDoubleBuffering();
}
private void enableDoubleBuffering()
{
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);
}
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;
// 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 // sorting breaks filters. must reapply filters after sorting
dataGridView.Sorted += (_, __) => filter(); _dataGridView.Sorted += (_, __) => Filter();
_dataGridView.CellContentClick += DataGridView_CellContentClick;
EnableDoubleBuffering();
}
private void EnableDoubleBuffering()
{
var propertyInfo = _dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
propertyInfo.SetValue(_dataGridView, true, null);
} }
#region format text cells. ie: not buttons #region Button controls
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 private async void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
e.Value = value;
getCell(e).ToolTipText = value;
}
}
private void hiddenFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{ {
var dgv = (DataGridView)sender; // handle grid button click: https://stackoverflow.com/a/13687844
// no action needed for buttons if (e.RowIndex < 0 || _dataGridView.Columns[e.ColumnIndex] is not DataGridViewButtonColumn)
if (e.RowIndex < 0 || dgv.Columns[e.ColumnIndex] is DataGridViewButtonColumn)
return; return;
var isHidden = getGridEntry(e.RowIndex).TagsEnumerated.Contains("hidden"); var liveGridEntry = getGridEntry(e.RowIndex);
getCell(e).Style switch (_dataGridView.Columns[e.ColumnIndex].DataPropertyName)
= isHidden
? new DataGridViewCellStyle { ForeColor = Color.LightGray }
: dgv.DefaultCellStyle;
}
#endregion
#region liberation buttons
private void addLiberateButtons()
{ {
dataGridView.Columns.Insert(0, new DataGridViewButtonColumn { HeaderText = LIBERATE }); case nameof(liveGridEntry.Liberate):
await Liberate_Click(liveGridEntry);
dataGridView.CellPainting += liberate_Paint; break;
dataGridView.CellContentClick += liberate_Click; case nameof(liveGridEntry.DisplayTags):
} EditTags_Click(liveGridEntry);
break;
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) private async Task Liberate_Click(GridEntry liveGridEntry)
{ {
if (!isColumnValid(e, LIBERATE)) var libraryBook = liveGridEntry.LibraryBook;
return;
var libraryBook = getGridEntry(e.RowIndex).GetLibraryBook();
// liberated: open explorer to file // liberated: open explorer to file
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book)) if (TransitionalFileLocator.Audio_Exists(libraryBook.Book))
@ -195,146 +87,24 @@ namespace LibationWinForms
// else: liberate // else: liberate
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId)); await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
} }
#endregion
public void RefreshRow(string productId) private void EditTags_Click(GridEntry liveGridEntry)
{ {
var rowId = getRowId((ge) => ge.AudibleProductId == productId); var bookDetailsForm = new BookDetailsDialog(liveGridEntry.Title, liveGridEntry.LibraryBook.Book.UserDefinedItem.Tags);
// 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) if (bookDetailsForm.ShowDialog() != DialogResult.OK)
return; return;
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.GetBook(), bookDetailsForm.NewTags); var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags);
if (qtyChanges == 0) if (qtyChanges == 0)
return; return;
// force a re-draw, and re-apply filters //Re-apply filters
Filter();
// needed to update text colors
dgv.InvalidateRow(e.RowIndex);
filter();
} }
#endregion #endregion
private static void drawImage(DataGridViewCellPaintingEventArgs e, Bitmap image) #region UI display functions
{
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()
{
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; private bool hasBeenDisplayed = false;
public void Display() public void Display()
@ -352,67 +122,79 @@ namespace LibationWinForms
// if no data. hide all columns. return // if no data. hide all columns. return
if (!lib.Any()) if (!lib.Any())
{ {
for (var i = dataGridView.ColumnCount - 1; i >= 0; i--) for (var i = _dataGridView.ColumnCount - 1; i >= 0; i--)
dataGridView.Columns.RemoveAt(i); _dataGridView.Columns.RemoveAt(i);
return; return;
} }
var orderedGridEntries = lib var orderedGridEntries = lib
.Select(lb => new GridEntry(lb)).ToList() .Select(lb => new GridEntry(lb)).ToList()
// default load order // default load order
.OrderByDescending(ge => ge.Purchase_Date) .OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
//// more advanced example: sort by author, then series, then title //// more advanced example: sort by author, then series, then title
//.OrderBy(ge => ge.Authors) //.OrderBy(ge => ge.Authors)
// .ThenBy(ge => ge.Series) // .ThenBy(ge => ge.Series)
// .ThenBy(ge => ge.Title) // .ThenBy(ge => ge.Title)
.ToList(); .ToList();
//
// BIND // BIND
// gridEntryBindingSource.DataSource = new SortableBindingList2<GridEntry>(orderedGridEntries);
gridEntryBindingSource.DataSource = orderedGridEntries.ToSortableBindingList();
//
// FILTER // FILTER
// Filter();
filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty); BackupCountsChanged?.Invoke(this, EventArgs.Empty);
} }
#region filter public void RefreshRow(string productId)
string _filterSearchString; {
private void filter() => Filter(_filterSearchString); 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) public void Filter(string searchString)
{ {
_filterSearchString = searchString; _filterSearchString = searchString;
if (dataGridView.Rows.Count == 0) if (_dataGridView.Rows.Count == 0)
return; return;
var searchResults = SearchEngineCommands.Search(searchString); var searchResults = SearchEngineCommands.Search(searchString);
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList(); var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
// https://stackoverflow.com/a/18942430 // https://stackoverflow.com/a/18942430
var currencyManager = (CurrencyManager)BindingContext[dataGridView.DataSource]; var bindingContext = BindingContext[_dataGridView.DataSource];
currencyManager.SuspendBinding(); bindingContext.SuspendBinding();
{ {
for (var r = dataGridView.RowCount - 1; r >= 0; r--) for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId); _dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
} }
currencyManager.ResumeBinding();
VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible)); //Causes repainting of the DataGridView
bindingContext.ResumeBinding();
VisibleCountChanged?.Invoke(this, _dataGridView.AsEnumerable().Count(r => r.Visible));
} }
#endregion #endregion
private int getRowId(Func<GridEntry, bool> func) => dataGridView.GetRowIdOfBoundItem(func); #region DataGridView Macro
private GridEntry getGridEntry(int rowIndex) => dataGridView.GetBoundItem<GridEntry>(rowIndex); private int getRowIndex(Func<GridEntry, bool> func) => _dataGridView.GetRowIdOfBoundItem(func);
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
private DataGridViewCell getCell(DataGridViewCellFormattingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex); #endregion
private DataGridViewCell getCell(DataGridViewCellPaintingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex);
private DataGridViewCell getCell(int rowIndex, int columnIndex) => dataGridView.Rows[rowIndex].Cells[columnIndex];
} }
} }

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="RemovableGridEntry" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>LibationWinForms.Dialogs.RemovableGridEntry, LibationWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -0,0 +1,75 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
namespace LibationWinForms
{
internal class SortableBindingList2<T> : BindingList<T> where T : IMemberComparable
{
private bool isSorted;
private ListSortDirection listSortDirection;
private PropertyDescriptor propertyDescriptor;
public SortableBindingList2() : base(new List<T>()) { }
public SortableBindingList2(IEnumerable<T> enumeration) : base(new List<T>(enumeration)) { }
private MemberComparer<T> 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<T> itemsList = (List<T>)Items;
Comparer.PropertyName = property.Name;
Comparer.Direction = direction;
//Array.Sort() and List<T>.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;
}
}
}

View File

@ -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;
}
/// <summary>
/// This callback executes on the SynchronizationContext thread.
/// </summary>
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();
}
}
}
}

View File

@ -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<PictureDefinition, Image> cache { get; } = new Dictionary<PictureDefinition, Image>();
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];
}
}
}

View File

@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon />
<StartupObject />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>
</Project>