Merge pull request #85 from Mbucari/master
"F*ck it, we'll do it live!"
This commit is contained in:
commit
fb7f57ab69
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
{
|
||||||
|
SeekOrigin.Current => Position + offset,
|
||||||
|
SeekOrigin.End => ContentLength + offset,
|
||||||
|
_ => offset,
|
||||||
|
};
|
||||||
|
|
||||||
switch (origin)
|
WaitToPosition(newPosition);
|
||||||
{
|
return _readFile.Position = newPosition;
|
||||||
case SeekOrigin.Current:
|
|
||||||
newPosition = Position + offset;
|
|
||||||
break;
|
|
||||||
case SeekOrigin.End:
|
|
||||||
newPosition = ContentLength + offset;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
newPosition = offset;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ReadToPosition(newPosition);
|
|
||||||
|
|
||||||
_readFile.Position = newPosition;
|
|
||||||
return newPosition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,25 +12,26 @@ 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;
|
||||||
|
|
||||||
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
|
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");
|
||||||
|
|
||||||
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,27 +8,30 @@ 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
|
||||||
{
|
{
|
||||||
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> 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> Completed;
|
|
||||||
public event EventHandler<string> StatusUpdate;
|
|
||||||
|
|
||||||
private AaxcDownloadConverter aaxcDownloader;
|
private AaxcDownloadConverter aaxcDownloader;
|
||||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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,11 +122,12 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
FileLiberator/IAudioDecodable.cs
Normal file
14
FileLiberator/IAudioDecodable.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
namespace FileLiberator
|
|
||||||
{
|
|
||||||
public interface IDownloadableProcessable : IDownloadable, IProcessable { }
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
13
FileLiberator/IStreamable.cs
Normal file
13
FileLiberator/IStreamable.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,28 +98,26 @@ namespace FileManager
|
|||||||
private void BackgroundScanner()
|
private void BackgroundScanner()
|
||||||
{
|
{
|
||||||
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
||||||
UpdateLocalCache(change);
|
{
|
||||||
|
lock (fsCache)
|
||||||
|
UpdateLocalCache(change);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateLocalCache(FileSystemEventArgs change)
|
private void UpdateLocalCache(FileSystemEventArgs change)
|
||||||
{
|
{
|
||||||
lock (fsCache)
|
if (change.ChangeType == WatcherChangeTypes.Deleted)
|
||||||
{
|
{
|
||||||
if (change.ChangeType == WatcherChangeTypes.Deleted)
|
RemovePath(change.FullPath);
|
||||||
{
|
}
|
||||||
RemovePath(change.FullPath);
|
else if (change.ChangeType == WatcherChangeTypes.Created)
|
||||||
}
|
{
|
||||||
else if (change.ChangeType == WatcherChangeTypes.Created)
|
AddPath(change.FullPath);
|
||||||
{
|
}
|
||||||
AddPath(change.FullPath);
|
else if (change.ChangeType == WatcherChangeTypes.Renamed && change is RenamedEventArgs renameChange)
|
||||||
}
|
{
|
||||||
else if (change.ChangeType == WatcherChangeTypes.Renamed)
|
RemovePath(renameChange.OldFullPath);
|
||||||
{
|
AddPath(renameChange.FullPath);
|
||||||
var renameChange = change as RenamedEventArgs;
|
|
||||||
|
|
||||||
RemovePath(renameChange.OldFullPath);
|
|
||||||
AddPath(renameChange.FullPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadQueue.Add(def);
|
||||||
|
return (true, getDefaultImage(def.Size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] GetPictureSynchronously(PictureDefinition def)
|
||||||
|
{
|
||||||
|
lock (cache)
|
||||||
|
{
|
||||||
|
if (!cache.ContainsKey(def) || cache[def] == null)
|
||||||
|
{
|
||||||
|
var path = getPath(def);
|
||||||
|
byte[] bytes;
|
||||||
|
|
||||||
|
if (File.Exists(path))
|
||||||
|
bytes = File.ReadAllBytes(path);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bytes = downloadBytes(def);
|
||||||
|
saveFile(def, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
cache[def] = bytes;
|
||||||
|
}
|
||||||
|
return cache[def];
|
||||||
}
|
}
|
||||||
return (cache[def] == null, cache[def] ?? getDefaultImage(def.Size));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<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);
|
||||||
cache[def] = bytes;
|
lock (cache)
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
LibationWinForms/AsyncNotifyPropertyChanged.cs
Normal file
16
LibationWinForms/AsyncNotifyPropertyChanged.cs
Normal 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) });
|
||||||
|
}
|
||||||
|
}
|
||||||
24
LibationWinForms/BookLiberation/AudioConvertForm.cs
Normal file
24
LibationWinForms/BookLiberation/AudioConvertForm.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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">
|
||||||
@ -1,6 +1,6 @@
|
|||||||
namespace LibationWinForms.BookLiberation
|
namespace LibationWinForms.BookLiberation
|
||||||
{
|
{
|
||||||
partial class DecryptForm
|
partial class AudioDecodeForm
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required designer variable.
|
/// Required designer variable.
|
||||||
94
LibationWinForms/BookLiberation/AudioDecodeForm.cs
Normal file
94
LibationWinForms/BookLiberation/AudioDecodeForm.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
61
LibationWinForms/BookLiberation/AudioDecodeForm.resx
Normal file
61
LibationWinForms/BookLiberation/AudioDecodeForm.resx
Normal 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>
|
||||||
24
LibationWinForms/BookLiberation/AudioDecryptForm.cs
Normal file
24
LibationWinForms/BookLiberation/AudioDecryptForm.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
61
LibationWinForms/BookLiberation/AudioDecryptForm.resx
Normal file
61
LibationWinForms/BookLiberation/AudioDecryptForm.resx
Normal 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>
|
||||||
@ -1,34 +1,35 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
public partial class AutomatedBackupsForm : Form
|
public partial class AutomatedBackupsForm : Form
|
||||||
{
|
{
|
||||||
public bool KeepGoingChecked => keepGoingCb.Checked;
|
public bool KeepGoingChecked => keepGoingCb.Checked;
|
||||||
|
|
||||||
public bool KeepGoing => keepGoingCb.Enabled && keepGoingCb.Checked;
|
public bool KeepGoing => keepGoingCb.Enabled && keepGoingCb.Checked;
|
||||||
|
|
||||||
public AutomatedBackupsForm()
|
public AutomatedBackupsForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteLine(string text)
|
public void WriteLine(string text)
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
|
logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FinalizeUI()
|
public void FinalizeUI()
|
||||||
{
|
{
|
||||||
keepGoingCb.Enabled = false;
|
keepGoingCb.Enabled = false;
|
||||||
|
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
logTb.AppendText("");
|
logTb.AppendText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;
|
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
160
LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs
Normal file
160
LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,7 @@
|
|||||||
namespace LibationWinForms.BookLiberation
|
using DataLayer;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LibationWinForms.BookLiberation
|
||||||
{
|
{
|
||||||
partial class DownloadForm
|
partial class DownloadForm
|
||||||
{
|
{
|
||||||
@ -95,9 +98,10 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private System.Windows.Forms.Label filenameLbl;
|
#endregion
|
||||||
|
|
||||||
|
private System.Windows.Forms.Label filenameLbl;
|
||||||
private System.Windows.Forms.ProgressBar progressBar1;
|
private System.Windows.Forms.ProgressBar progressBar1;
|
||||||
private System.Windows.Forms.Label progressLbl;
|
private System.Windows.Forms.Label progressLbl;
|
||||||
private System.Windows.Forms.Label lastUpdateLbl;
|
private System.Windows.Forms.Label lastUpdateLbl;
|
||||||
|
|||||||
@ -1,59 +1,66 @@
|
|||||||
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()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
progressLbl.Text = "";
|
progressLbl.Text = "";
|
||||||
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)
|
||||||
// this won't happen with download file. it will happen with download string
|
{
|
||||||
if (!TotalBytesToReceive.HasValue || TotalBytesToReceive.Value <= 0)
|
filenameLbl.UIThread(() => filenameLbl.Text = beginString);
|
||||||
return;
|
}
|
||||||
|
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
||||||
|
{
|
||||||
|
// this won't happen with download file. it will happen with download string
|
||||||
|
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
progressLbl.UIThread(() => progressLbl.Text = $"{BytesReceived:#,##0} of {TotalBytesToReceive.Value:#,##0}");
|
progressLbl.UIThread(() => progressLbl.Text = $"{downloadProgress.BytesReceived:#,##0} of {downloadProgress.TotalBytesToReceive.Value:#,##0}");
|
||||||
|
|
||||||
var d = double.Parse(BytesReceived.ToString()) / double.Parse(TotalBytesToReceive.Value.ToString()) * 100.0;
|
var 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 };
|
||||||
private void DownloadForm_Load(object sender, EventArgs e)
|
private void DownloadForm_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
timer.Tick += new EventHandler(timer_Tick);
|
timer.Tick += new EventHandler(timer_Tick);
|
||||||
timer.Start();
|
timer.Start();
|
||||||
}
|
}
|
||||||
private DateTime lastDownloadProgress = DateTime.Now;
|
private DateTime lastDownloadProgress = DateTime.Now;
|
||||||
private void timer_Tick(object sender, EventArgs e)
|
private void timer_Tick(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// if no update in the last 30 seconds, display frozen label
|
// if no update in the last 30 seconds, display frozen label
|
||||||
lastUpdateLbl.UIThread(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now);
|
lastUpdateLbl.UIThread(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now);
|
||||||
if (lastUpdateLbl.Visible)
|
if (lastUpdateLbl.Visible)
|
||||||
{
|
{
|
||||||
var diff = DateTime.Now - lastDownloadProgress;
|
var diff = DateTime.Now - lastDownloadProgress;
|
||||||
var min = (int)diff.TotalMinutes;
|
var min = (int)diff.TotalMinutes;
|
||||||
var minText = min > 0 ? $"{min}min " : "";
|
var minText = min > 0 ? $"{min}min " : "";
|
||||||
|
|
||||||
lastUpdateLbl.UIThread(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago");
|
lastUpdateLbl.UIThread(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private void DownloadForm_FormClosing(object sender, FormClosingEventArgs e) => timer.Stop();
|
private void DownloadForm_FormClosing(object sender, FormClosingEventArgs e) => timer.Stop();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
10
LibationWinForms/BookLiberation/PdfDownloadForm.cs
Normal file
10
LibationWinForms/BookLiberation/PdfDownloadForm.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
61
LibationWinForms/BookLiberation/PdfDownloadForm.resx
Normal file
61
LibationWinForms/BookLiberation/PdfDownloadForm.resx
Normal 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>
|
||||||
@ -1,505 +1,269 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
// decouple serilog and form. include convenience factory method
|
// decouple serilog and form. include convenience factory method
|
||||||
public class LogMe
|
public class LogMe
|
||||||
{
|
{
|
||||||
public event EventHandler<string> LogInfo;
|
public event EventHandler<string> LogInfo;
|
||||||
public event EventHandler<string> LogErrorString;
|
public event EventHandler<string> LogErrorString;
|
||||||
public event EventHandler<(Exception, string)> LogError;
|
public event EventHandler<(Exception, string)> LogError;
|
||||||
|
|
||||||
private LogMe()
|
private LogMe()
|
||||||
{
|
{
|
||||||
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
|
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
|
||||||
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||||
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogMe RegisterForm(AutomatedBackupsForm form = null)
|
public static LogMe RegisterForm(AutomatedBackupsForm form = null)
|
||||||
{
|
{
|
||||||
var logMe = new LogMe();
|
var logMe = new LogMe();
|
||||||
|
|
||||||
if (form is null)
|
if (form is null)
|
||||||
return logMe;
|
return logMe;
|
||||||
|
|
||||||
logMe.LogInfo += (_, text) => form?.WriteLine(text);
|
logMe.LogInfo += (_, text) => form?.WriteLine(text);
|
||||||
|
|
||||||
logMe.LogErrorString += (_, text) => form?.WriteLine(text);
|
logMe.LogErrorString += (_, text) => form?.WriteLine(text);
|
||||||
|
|
||||||
logMe.LogError += (_, tuple) =>
|
logMe.LogError += (_, tuple) =>
|
||||||
{
|
{
|
||||||
form?.WriteLine(tuple.Item2 ?? "Automated backup: error");
|
form?.WriteLine(tuple.Item2 ?? "Automated backup: error");
|
||||||
form?.WriteLine("ERROR: " + tuple.Item1.Message);
|
form?.WriteLine("ERROR: " + tuple.Item1.Message);
|
||||||
};
|
};
|
||||||
|
|
||||||
return logMe;
|
return logMe;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Info(string text) => LogInfo?.Invoke(this, text);
|
public void Info(string text) => LogInfo?.Invoke(this, text);
|
||||||
public void Error(string text) => LogErrorString?.Invoke(this, text);
|
public void Error(string text) => LogErrorString?.Invoke(this, text);
|
||||||
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
|
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProcessorAutomationController
|
public static class ProcessorAutomationController
|
||||||
{
|
{
|
||||||
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)
|
||||||
}
|
{
|
||||||
|
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
|
||||||
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
|
|
||||||
{
|
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
|
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||||
|
var backupBook = CreateBackupBook(completedAction, logMe);
|
||||||
var backupBook = getWiredUpBackupBook(completedAction);
|
|
||||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
|
||||||
|
}
|
||||||
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook, automatedBackupsForm);
|
|
||||||
|
public static async Task ConvertAllBooksAsync()
|
||||||
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
|
{
|
||||||
|
Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync));
|
||||||
unsubscribeEvents();
|
|
||||||
}
|
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||||
|
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||||
public static async Task ConvertAllBooksAsync()
|
|
||||||
{
|
var convertBook = CreateProcessable<ConvertToMp3, AudioConvertForm>(logMe);
|
||||||
Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync));
|
|
||||||
|
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
|
||||||
var convertBook = new ConvertToMp3();
|
}
|
||||||
convertBook.Begin += (_, l) => wireUpEvents(convertBook, l, "Converting");
|
|
||||||
|
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
|
||||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
{
|
||||||
|
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
|
||||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
|
||||||
|
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||||
void statusUpdate(object _, string str) => logMe.Info("- " + str);
|
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||||
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}");
|
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe, completedAction);
|
||||||
convertBook.Begin += convertBookBegin;
|
|
||||||
convertBook.StatusUpdate += statusUpdate;
|
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
|
||||||
convertBook.Completed += convertBookCompleted;
|
}
|
||||||
|
|
||||||
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
|
private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe)
|
||||||
|
{
|
||||||
convertBook.Begin -= convertBookBegin;
|
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
|
||||||
convertBook.StatusUpdate -= statusUpdate;
|
|
||||||
convertBook.Completed -= convertBookCompleted;
|
//Chain pdf download on DownloadDecryptBook.Completed
|
||||||
}
|
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
|
||||||
|
{
|
||||||
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> completedAction)
|
await downloadPdf.TryProcessAsync(e);
|
||||||
{
|
completedAction(sender, e);
|
||||||
var backupBook = new BackupBook();
|
}
|
||||||
|
|
||||||
backupBook.DownloadDecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DownloadDecryptBook, l);
|
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
|
||||||
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
|
return downloadDecryptBook;
|
||||||
|
}
|
||||||
if (completedAction != null)
|
|
||||||
{
|
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
|
||||||
backupBook.DownloadDecryptBook.Completed += completedAction;
|
{
|
||||||
backupBook.DownloadPdf.Completed += completedAction;
|
Serilog.Log.Logger.Information($"Begin {nameof(DownloadFile)} for {url}");
|
||||||
}
|
|
||||||
|
void onDownloadFileStreamingCompleted(object sender, string savedFile)
|
||||||
return backupBook;
|
{
|
||||||
}
|
Serilog.Log.Logger.Information($"Completed {nameof(DownloadFile)} for {url}. Saved to {savedFile}");
|
||||||
|
|
||||||
private static (Action unsubscribeEvents, LogMe) attachToBackupsForm(BackupBook backupBook, AutomatedBackupsForm automatedBackupsForm = null)
|
if (showDownloadCompletedDialog)
|
||||||
{
|
MessageBox.Show($"File downloaded to:{Environment.NewLine}{Environment.NewLine}{savedFile}");
|
||||||
#region create logger
|
}
|
||||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
|
||||||
#endregion
|
var downloadFile = new DownloadFile();
|
||||||
|
var downloadForm = new DownloadForm();
|
||||||
#region define how model actions will affect form behavior
|
downloadForm.RegisterFileLiberator(downloadFile);
|
||||||
void statusUpdate(object _, string str) => logMe.Info("- " + str);
|
downloadFile.StreamingCompleted += onDownloadFileStreamingCompleted;
|
||||||
void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}");
|
|
||||||
// extra line after book is completely finished
|
async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination);
|
||||||
void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
new Task(runDownload).Start();
|
||||||
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}");
|
/// <summary>
|
||||||
#endregion
|
/// Create a new <see cref="IProcessable"/> and links it to a new <see cref="LiberationBaseForm"/>.
|
||||||
|
/// </summary>
|
||||||
#region subscribe new form to model's events
|
/// <typeparam name="TProcessable">The <see cref="IProcessable"/> derived type to create.</typeparam>
|
||||||
backupBook.DownloadDecryptBook.Begin += decryptBookBegin;
|
/// <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>
|
||||||
backupBook.DownloadDecryptBook.StatusUpdate += statusUpdate;
|
/// <param name="logMe">The logger</param>
|
||||||
backupBook.DownloadDecryptBook.Completed += decryptBookCompleted;
|
/// <param name="completedAction">An additional event handler to handle <see cref="IProcessable.Completed"/></param>
|
||||||
backupBook.DownloadPdf.Begin += downloadPdfBegin;
|
/// <returns>A new <see cref="IProcessable"/> of type <typeparamref name="TProcessable"/></returns>
|
||||||
backupBook.DownloadPdf.StatusUpdate += statusUpdate;
|
private static TProcessable CreateProcessable<TProcessable, TForm>(LogMe logMe, EventHandler<LibraryBook> completedAction = null)
|
||||||
backupBook.DownloadPdf.Completed += downloadPdfCompleted;
|
where TForm : LiberationBaseForm, new()
|
||||||
#endregion
|
where TProcessable : IProcessable, new()
|
||||||
|
{
|
||||||
#region when form closes, unsubscribe from model's events
|
var strProc = new TProcessable();
|
||||||
// unsubscribe so disposed forms aren't still trying to receive notifications
|
|
||||||
Action unsubscribe = () =>
|
strProc.Begin += (sender, libraryBook) =>
|
||||||
{
|
{
|
||||||
backupBook.DownloadDecryptBook.Begin -= decryptBookBegin;
|
var processForm = new TForm();
|
||||||
backupBook.DownloadDecryptBook.StatusUpdate -= statusUpdate;
|
processForm.RegisterFileLiberator(strProc, logMe);
|
||||||
backupBook.DownloadDecryptBook.Completed -= decryptBookCompleted;
|
processForm.OnBegin(sender, libraryBook);
|
||||||
backupBook.DownloadPdf.Begin -= downloadPdfBegin;
|
};
|
||||||
backupBook.DownloadPdf.StatusUpdate -= statusUpdate;
|
|
||||||
backupBook.DownloadPdf.Completed -= downloadPdfCompleted;
|
strProc.Completed += completedAction;
|
||||||
};
|
|
||||||
#endregion
|
return strProc;
|
||||||
|
}
|
||||||
return (unsubscribe, logMe);
|
}
|
||||||
}
|
|
||||||
|
internal abstract class BackupRunner
|
||||||
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
|
{
|
||||||
{
|
protected LogMe LogMe { get; }
|
||||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
|
protected IProcessable Processable { get; }
|
||||||
|
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
|
||||||
var downloadPdf = getWiredUpDownloadPdf(completedAction);
|
|
||||||
|
protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm = null)
|
||||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
|
{
|
||||||
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
|
LogMe = logMe;
|
||||||
}
|
Processable = processable;
|
||||||
|
AutomatedBackupsForm = automatedBackupsForm;
|
||||||
private static DownloadPdf getWiredUpDownloadPdf(EventHandler<LibraryBook> completedAction)
|
}
|
||||||
{
|
|
||||||
var downloadPdf = new DownloadPdf();
|
protected abstract Task RunAsync();
|
||||||
|
protected abstract string SkipDialogText { get; }
|
||||||
downloadPdf.Begin += (_, __) => wireUpEvents(downloadPdf);
|
protected abstract MessageBoxButtons SkipDialogButtons { get; }
|
||||||
|
protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; }
|
||||||
if (completedAction != null)
|
protected abstract DialogResult CreateSkipFileResult { get; }
|
||||||
downloadPdf.Completed += completedAction;
|
|
||||||
|
public async Task RunBackupAsync()
|
||||||
return downloadPdf;
|
{
|
||||||
}
|
AutomatedBackupsForm?.Show();
|
||||||
|
|
||||||
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
|
try
|
||||||
{
|
{
|
||||||
var downloadDialog = new DownloadForm();
|
await RunAsync();
|
||||||
downloadDialog.UpdateFilename(destination);
|
}
|
||||||
downloadDialog.Show();
|
catch (Exception ex)
|
||||||
|
{
|
||||||
new System.Threading.Thread(() =>
|
LogMe.Error(ex);
|
||||||
{
|
}
|
||||||
var downloadFile = new DownloadFile();
|
|
||||||
|
AutomatedBackupsForm?.FinalizeUI();
|
||||||
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.UIThread(() =>
|
LogMe.Info("DONE");
|
||||||
downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive)
|
}
|
||||||
);
|
|
||||||
downloadFile.DownloadCompleted += (_, __) => downloadDialog.UIThread(() =>
|
protected async Task<bool> ProcessOneAsync(LibraryBook libraryBook, bool validate)
|
||||||
{
|
{
|
||||||
downloadDialog.Close();
|
string logMessage;
|
||||||
if (showDownloadCompletedDialog)
|
|
||||||
MessageBox.Show("File downloaded");
|
try
|
||||||
});
|
{
|
||||||
|
var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
|
||||||
downloadFile.PerformDownloadFileAsync(url, destination).GetAwaiter().GetResult();
|
|
||||||
})
|
if (statusHandler.IsSuccess)
|
||||||
{ IsBackground = true }
|
return true;
|
||||||
.Start();
|
|
||||||
}
|
foreach (var errorMessage in statusHandler.Errors)
|
||||||
|
LogMe.Error(errorMessage);
|
||||||
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
|
|
||||||
private static void wireUpEvents(IDownloadableProcessable downloadable)
|
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
{
|
}
|
||||||
#region create form
|
catch (Exception ex)
|
||||||
var downloadDialog = new DownloadForm();
|
{
|
||||||
#endregion
|
LogMe.Error(ex);
|
||||||
|
|
||||||
// extra complexity for wiring up download form:
|
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
|
||||||
// case 1: download is needed
|
}
|
||||||
// dialog created. subscribe to events
|
|
||||||
// downloadable.DownloadBegin fires. shows dialog
|
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
||||||
// downloadable.DownloadCompleted fires. closes dialog. which fires FormClosing, FormClosed, Disposed
|
|
||||||
// Disposed unsubscribe from events
|
string details;
|
||||||
// case 2: download is not needed
|
try
|
||||||
// dialog created. subscribe to events
|
{
|
||||||
// dialog is never shown nor closed
|
static string trunc(string str)
|
||||||
// downloadable.Completed fires. disposes dialog and unsubscribes from events
|
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
|
||||||
|
: (str.Length > 50) ? $"{str.Truncate(47)}..."
|
||||||
#region define how model actions will affect form behavior
|
: str;
|
||||||
void downloadBegin(object _, string str)
|
|
||||||
{
|
details =
|
||||||
downloadDialog.UpdateFilename(str);
|
|
||||||
downloadDialog.Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// close form on DOWNLOAD completed, not final Completed. Else for BackupBook this form won't close until DECRYPT is also complete
|
|
||||||
void fileDownloadCompleted(object _, string __) => downloadDialog.Close();
|
|
||||||
|
|
||||||
void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress)
|
|
||||||
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive);
|
|
||||||
|
|
||||||
void unsubscribe(object _ = null, EventArgs __ = null)
|
|
||||||
{
|
|
||||||
downloadable.DownloadBegin -= downloadBegin;
|
|
||||||
downloadable.DownloadCompleted -= fileDownloadCompleted;
|
|
||||||
downloadable.DownloadProgressChanged -= downloadProgressChanged;
|
|
||||||
downloadable.Completed -= dialogDispose;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unless we dispose, if the form is created but un-used/never-shown then weird UI stuff can happen
|
|
||||||
// also, since event unsubscribe occurs on FormClosing and an unused form is never closed, then the events will never be unsubscribed
|
|
||||||
void dialogDispose(object _, object __)
|
|
||||||
{
|
|
||||||
if (!downloadDialog.IsDisposed)
|
|
||||||
downloadDialog.Dispose();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region subscribe new form to model's events
|
|
||||||
downloadable.DownloadBegin += downloadBegin;
|
|
||||||
downloadable.DownloadCompleted += fileDownloadCompleted;
|
|
||||||
downloadable.DownloadProgressChanged += downloadProgressChanged;
|
|
||||||
downloadable.Completed += dialogDispose;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region when form closes, unsubscribe from model's events
|
|
||||||
// unsubscribe so disposed forms aren't still trying to receive notifications
|
|
||||||
// FormClosing is more UI safe but won't fire unless the form is shown and closed
|
|
||||||
// if form was shown, Disposed will fire for FormClosing, FormClosed, and Disposed
|
|
||||||
// if not shown, it will still fire for Disposed
|
|
||||||
downloadDialog.Disposed += unsubscribe;
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
|
|
||||||
private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook, string actionName = "Decrypting")
|
|
||||||
{
|
|
||||||
#region create form
|
|
||||||
var decryptDialog = new DecryptForm();
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Set initially displayed book properties from library info.
|
|
||||||
decryptDialog.SetTitle(actionName, libraryBook.Book.Title);
|
|
||||||
decryptDialog.SetAuthorNames(string.Join(", ", libraryBook.Book.Authors));
|
|
||||||
decryptDialog.SetNarratorNames(string.Join(", ", libraryBook.Book.NarratorNames));
|
|
||||||
decryptDialog.SetCoverImage(
|
|
||||||
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(
|
|
||||||
libraryBook.Book.PictureId,
|
|
||||||
FileManager.PictureSize._80x80
|
|
||||||
));
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region define how model actions will affect form behavior
|
|
||||||
void decryptBegin(object _, string __) => decryptDialog.Show();
|
|
||||||
|
|
||||||
void titleDiscovered(object _, string title) => decryptDialog.SetTitle(actionName, title);
|
|
||||||
void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors);
|
|
||||||
void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators);
|
|
||||||
void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(Dinah.Core.Drawing.ImageReader.ToImage(coverBytes));
|
|
||||||
void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage);
|
|
||||||
void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining);
|
|
||||||
void decryptCompleted(object _, string __) => decryptDialog.Close();
|
|
||||||
|
|
||||||
void requestCoverArt(object _, Action<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
|
|
||||||
decryptBook.DecryptBegin += decryptBegin;
|
|
||||||
|
|
||||||
decryptBook.TitleDiscovered += titleDiscovered;
|
|
||||||
decryptBook.AuthorsDiscovered += authorsDiscovered;
|
|
||||||
decryptBook.NarratorsDiscovered += narratorsDiscovered;
|
|
||||||
decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered;
|
|
||||||
decryptBook.UpdateProgress += updateProgress;
|
|
||||||
decryptBook.UpdateRemainingTime += updateRemainingTime;
|
|
||||||
decryptBook.RequestCoverArt += requestCoverArt;
|
|
||||||
|
|
||||||
decryptBook.DecryptCompleted += decryptCompleted;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region when form closes, unsubscribe from model's events
|
|
||||||
// unsubscribe so disposed forms aren't still trying to receive notifications
|
|
||||||
decryptDialog.FormClosing += (_, __) =>
|
|
||||||
{
|
|
||||||
decryptBook.DecryptBegin -= decryptBegin;
|
|
||||||
|
|
||||||
decryptBook.TitleDiscovered -= titleDiscovered;
|
|
||||||
decryptBook.AuthorsDiscovered -= authorsDiscovered;
|
|
||||||
decryptBook.NarratorsDiscovered -= narratorsDiscovered;
|
|
||||||
decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered;
|
|
||||||
decryptBook.UpdateProgress -= updateProgress;
|
|
||||||
decryptBook.UpdateRemainingTime -= updateRemainingTime;
|
|
||||||
decryptBook.RequestCoverArt -= requestCoverArt;
|
|
||||||
|
|
||||||
decryptBook.DecryptCompleted -= decryptCompleted;
|
|
||||||
decryptBook.Cancel();
|
|
||||||
};
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
|
|
||||||
{
|
|
||||||
#region create form and logger
|
|
||||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
|
||||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region define how model actions will affect form behavior
|
|
||||||
void begin(object _, LibraryBook libraryBook) => logMe.Info($"Begin: {libraryBook.Book}");
|
|
||||||
void statusUpdate(object _, string str) => logMe.Info("- " + str);
|
|
||||||
// extra line after book is completely finished
|
|
||||||
void completed(object _, LibraryBook libraryBook) => logMe.Info($"Completed: {libraryBook.Book}{Environment.NewLine}");
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region subscribe new form to model's events
|
|
||||||
downloadable.Begin += begin;
|
|
||||||
downloadable.StatusUpdate += statusUpdate;
|
|
||||||
downloadable.Completed += completed;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region when form closes, unsubscribe from model's events
|
|
||||||
// unsubscribe so disposed forms aren't still trying to receive notifications
|
|
||||||
automatedBackupsForm.FormClosing += (_, __) =>
|
|
||||||
{
|
|
||||||
downloadable.Begin -= begin;
|
|
||||||
downloadable.StatusUpdate -= statusUpdate;
|
|
||||||
downloadable.Completed -= completed;
|
|
||||||
};
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
return (automatedBackupsForm, logMe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BackupRunner
|
|
||||||
{
|
|
||||||
protected LogMe LogMe { get; }
|
|
||||||
protected IProcessable Processable { get; }
|
|
||||||
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
|
|
||||||
|
|
||||||
protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm = null)
|
|
||||||
{
|
|
||||||
LogMe = logMe;
|
|
||||||
Processable = processable;
|
|
||||||
AutomatedBackupsForm = automatedBackupsForm;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract Task RunAsync();
|
|
||||||
|
|
||||||
protected abstract string SkipDialogText { get; }
|
|
||||||
protected abstract MessageBoxButtons SkipDialogButtons { get; }
|
|
||||||
protected abstract DialogResult CreateSkipFileResult { get; }
|
|
||||||
|
|
||||||
public async Task RunBackupAsync()
|
|
||||||
{
|
|
||||||
AutomatedBackupsForm?.Show();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RunAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LogMe.Error(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
AutomatedBackupsForm?.FinalizeUI();
|
|
||||||
LogMe.Info("DONE");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task<bool> ProcessOneAsync(Func<LibraryBook, Task<StatusHandler>> func, LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
string logMessage;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var statusHandler = await func(libraryBook);
|
|
||||||
|
|
||||||
if (statusHandler.IsSuccess)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
foreach (var errorMessage in statusHandler.Errors)
|
|
||||||
LogMe.Error(errorMessage);
|
|
||||||
|
|
||||||
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LogMe.Error(ex);
|
|
||||||
|
|
||||||
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
|
|
||||||
}
|
|
||||||
|
|
||||||
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
|
||||||
|
|
||||||
string details;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
static string trunc(string str)
|
|
||||||
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
|
|
||||||
: (str.Length > 50) ? $"{str.Truncate(47)}..."
|
|
||||||
: str;
|
|
||||||
|
|
||||||
details =
|
|
||||||
$@" Title: {libraryBook.Book.Title}
|
$@" Title: {libraryBook.Book.Title}
|
||||||
ID: {libraryBook.Book.AudibleProductId}
|
ID: {libraryBook.Book.AudibleProductId}
|
||||||
Author: {trunc(libraryBook.Book.AuthorNames)}
|
Author: {trunc(libraryBook.Book.AuthorNames)}
|
||||||
Narr: {trunc(libraryBook.Book.NarratorNames)}";
|
Narr: {trunc(libraryBook.Book.NarratorNames)}";
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
|
||||||
if (dialogResult == CreateSkipFileResult)
|
if (dialogResult == CreateSkipFileResult)
|
||||||
{
|
{
|
||||||
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null);
|
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null);
|
||||||
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
|
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
|
||||||
LogMe.Info($@"
|
LogMe.Info($@"
|
||||||
Created new 'skip' file
|
Created new 'skip' file
|
||||||
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
|
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
|
||||||
{path}
|
{path}
|
||||||
".Trim());
|
".Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class BackupSingle : BackupRunner
|
|
||||||
{
|
|
||||||
private LibraryBook _libraryBook { get; }
|
|
||||||
|
|
||||||
protected override string SkipDialogText => @"
|
internal class BackupSingle : BackupRunner
|
||||||
|
{
|
||||||
|
private LibraryBook _libraryBook { get; }
|
||||||
|
|
||||||
|
protected override string SkipDialogText => @"
|
||||||
An error occurred while trying to process this book. Skip this book permanently?
|
An error occurred while trying to process this book. Skip this book permanently?
|
||||||
{0}
|
{0}
|
||||||
|
|
||||||
@ -507,24 +271,26 @@ An error occurred while trying to process this book. Skip this book permanently?
|
|||||||
|
|
||||||
- Click NO to skip the book this time only. We'll try again later.
|
- 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 DialogResult CreateSkipFileResult => DialogResult.Yes;
|
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2;
|
||||||
|
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
|
||||||
|
|
||||||
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
|
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
|
||||||
: base(logMe, processable)
|
: base(logMe, processable)
|
||||||
{
|
{
|
||||||
_libraryBook = libraryBook;
|
_libraryBook = libraryBook;
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
{0}
|
{0}
|
||||||
|
|
||||||
@ -534,33 +300,34 @@ An error occurred while trying to process this book.
|
|||||||
|
|
||||||
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
|
- 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 DialogResult CreateSkipFileResult => DialogResult.Ignore;
|
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
|
||||||
|
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
|
||||||
|
|
||||||
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
|
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
|
||||||
: base(logMe, processable, automatedBackupsForm) { }
|
: base(logMe, processable, automatedBackupsForm) { }
|
||||||
|
|
||||||
protected override async Task RunAsync()
|
protected override async Task RunAsync()
|
||||||
{
|
{
|
||||||
// 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;
|
||||||
|
|
||||||
if (AutomatedBackupsForm.IsDisposed)
|
if (AutomatedBackupsForm.IsDisposed)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (!AutomatedBackupsForm.KeepGoing)
|
if (!AutomatedBackupsForm.KeepGoing)
|
||||||
{
|
{
|
||||||
if (!AutomatedBackupsForm.KeepGoingChecked)
|
if (!AutomatedBackupsForm.KeepGoingChecked)
|
||||||
LogMe.Info("'Keep going' is unchecked");
|
LogMe.Info("'Keep going' is unchecked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LogMe.Info("Done. All books have been processed");
|
LogMe.Info("Done. All books have been processed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
LibationWinForms/DataGridViewImageButtonColumn.cs
Normal file
50
LibationWinForms/DataGridViewImageButtonColumn.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
namespace LibationWinForms.Dialogs
|
namespace LibationWinForms.Dialogs
|
||||||
{
|
{
|
||||||
public partial class BookDetailsDialog : Form
|
public partial class BookDetailsDialog : Form
|
||||||
{
|
{
|
||||||
public string NewTags { get; private set; }
|
public string NewTags { get; private set; }
|
||||||
|
|
||||||
public BookDetailsDialog()
|
public BookDetailsDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
public BookDetailsDialog(string title, string rawTags) : this()
|
public BookDetailsDialog(string title, string rawTags) : this()
|
||||||
{
|
{
|
||||||
this.Text = $"Edit Tags - {title}";
|
this.Text = $"Edit Tags - {title}";
|
||||||
|
|
||||||
this.newTagsTb.Text = rawTags;
|
this.newTagsTb.Text = rawTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveBtn_Click(object sender, EventArgs e)
|
private void SaveBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
NewTags = this.newTagsTb.Text;
|
NewTags = this.newTagsTb.Text;
|
||||||
DialogResult = DialogResult.OK;
|
DialogResult = DialogResult.OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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));
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -1,101 +1,100 @@
|
|||||||
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)
|
||||||
|
{
|
||||||
|
_parent = parent;
|
||||||
|
|
||||||
public EditQuickFilters(Form1 parent)
|
InitializeComponent();
|
||||||
{
|
|
||||||
_parent = parent;
|
|
||||||
|
|
||||||
InitializeComponent();
|
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||||
|
|
||||||
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
populateGridValues();
|
||||||
|
}
|
||||||
|
|
||||||
populateGridValues();
|
private void populateGridValues()
|
||||||
}
|
{
|
||||||
|
var filters = QuickFilters.Filters;
|
||||||
|
if (!filters.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
private void populateGridValues()
|
foreach (var filter in filters)
|
||||||
{
|
dataGridView1.Rows.Add(filter, "X", filter, BLACK_UP_POINTING_TRIANGLE, BLACK_DOWN_POINTING_TRIANGLE);
|
||||||
var filters = QuickFilters.Filters;
|
}
|
||||||
if (!filters.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var filter in filters)
|
private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e)
|
||||||
dataGridView1.Rows.Add(filter, "X", filter, BLACK_UP_POINTING_TRIANGLE, BLACK_DOWN_POINTING_TRIANGLE);
|
{
|
||||||
}
|
e.Row.Cells[COL_Delete].Value = "X";
|
||||||
|
e.Row.Cells[COL_MoveUp].Value = BLACK_UP_POINTING_TRIANGLE;
|
||||||
|
e.Row.Cells[COL_MoveDown].Value = BLACK_DOWN_POINTING_TRIANGLE;
|
||||||
|
}
|
||||||
|
|
||||||
private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e)
|
private void saveBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
e.Row.Cells[COL_Delete].Value = "X";
|
var list = dataGridView1.Rows
|
||||||
e.Row.Cells[COL_MoveUp].Value = BLACK_UP_POINTING_TRIANGLE;
|
.OfType<DataGridViewRow>()
|
||||||
e.Row.Cells[COL_MoveDown].Value = BLACK_DOWN_POINTING_TRIANGLE;
|
.Select(r => r.Cells[COL_Filter].Value?.ToString())
|
||||||
}
|
.ToList();
|
||||||
|
QuickFilters.ReplaceAll(list);
|
||||||
|
|
||||||
private void saveBtn_Click(object sender, EventArgs e)
|
_parent.UpdateFilterDropDown();
|
||||||
{
|
this.DialogResult = DialogResult.OK;
|
||||||
var list = dataGridView1.Rows
|
this.Close();
|
||||||
.OfType<DataGridViewRow>()
|
}
|
||||||
.Select(r => r.Cells[COL_Filter].Value?.ToString())
|
|
||||||
.ToList();
|
|
||||||
QuickFilters.ReplaceAll(list);
|
|
||||||
|
|
||||||
_parent.UpdateFilterDropDown();
|
private void cancelBtn_Click(object sender, EventArgs e)
|
||||||
this.DialogResult = DialogResult.OK;
|
{
|
||||||
this.Close();
|
this.DialogResult = DialogResult.Cancel;
|
||||||
}
|
this.Close();
|
||||||
|
}
|
||||||
|
|
||||||
private void cancelBtn_Click(object sender, EventArgs e)
|
private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
|
||||||
{
|
{
|
||||||
this.DialogResult = DialogResult.Cancel;
|
var dgv = (DataGridView)sender;
|
||||||
this.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
|
var col = dgv.Columns[e.ColumnIndex];
|
||||||
{
|
if (col is DataGridViewButtonColumn && e.RowIndex >= 0)
|
||||||
var dgv = (DataGridView)sender;
|
{
|
||||||
|
var row = dgv.Rows[e.RowIndex];
|
||||||
var col = dgv.Columns[e.ColumnIndex];
|
switch (col.Name)
|
||||||
if (col is DataGridViewButtonColumn && e.RowIndex >= 0)
|
{
|
||||||
{
|
case COL_Delete:
|
||||||
var row = dgv.Rows[e.RowIndex];
|
// if final/edit row: do nothing
|
||||||
switch (col.Name)
|
if (e.RowIndex < dgv.RowCount - 1)
|
||||||
{
|
dgv.Rows.Remove(row);
|
||||||
case COL_Delete:
|
break;
|
||||||
// if final/edit row: do nothing
|
case COL_MoveUp:
|
||||||
if (e.RowIndex < dgv.RowCount - 1)
|
// if top: do nothing
|
||||||
dgv.Rows.Remove(row);
|
if (e.RowIndex < 1)
|
||||||
break;
|
break;
|
||||||
case COL_MoveUp:
|
dgv.Rows.Remove(row);
|
||||||
// if top: do nothing
|
dgv.Rows.Insert(e.RowIndex - 1, row);
|
||||||
if (e.RowIndex < 1)
|
break;
|
||||||
break;
|
case COL_MoveDown:
|
||||||
dgv.Rows.Remove(row);
|
// if final/edit row or bottom filter row: do nothing
|
||||||
dgv.Rows.Insert(e.RowIndex - 1, row);
|
if (e.RowIndex >= dgv.RowCount - 2)
|
||||||
break;
|
break;
|
||||||
case COL_MoveDown:
|
dgv.Rows.Remove(row);
|
||||||
// if final/edit row or bottom filter row: do nothing
|
dgv.Rows.Insert(e.RowIndex + 1, row);
|
||||||
if (e.RowIndex >= dgv.RowCount - 2)
|
break;
|
||||||
break;
|
}
|
||||||
dgv.Rows.Remove(row);
|
}
|
||||||
dgv.Rows.Insert(e.RowIndex + 1, row);
|
}
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
189
LibationWinForms/Dialogs/RemoveBooksDialog.Designer.cs
generated
Normal file
189
LibationWinForms/Dialogs/RemoveBooksDialog.Designer.cs
generated
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
LibationWinForms/Dialogs/RemoveBooksDialog.cs
Normal file
170
LibationWinForms/Dialogs/RemoveBooksDialog.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
LibationWinForms/Dialogs/RemoveBooksDialog.resx
Normal file
61
LibationWinForms/Dialogs/RemoveBooksDialog.resx
Normal 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>
|
||||||
@ -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; }
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
namespace LibationWinForms.Dialogs
|
namespace LibationWinForms.Dialogs
|
||||||
{
|
{
|
||||||
public partial class SearchSyntaxDialog : Form
|
public partial class SearchSyntaxDialog : Form
|
||||||
{
|
{
|
||||||
public SearchSyntaxDialog()
|
public SearchSyntaxDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
label2.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields());
|
label2.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields());
|
||||||
label3.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields());
|
label3.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields());
|
||||||
label4.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
|
label4.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
|
||||||
label5.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());
|
label5.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseBtn_Click(object sender, EventArgs e) => this.Close();
|
private void CloseBtn_Click(object sender, EventArgs e) => this.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
@ -57,13 +58,13 @@ namespace LibationWinForms.Dialogs
|
|||||||
inProgressSelectControl.SelectDirectory(config.InProgress);
|
inProgressSelectControl.SelectDirectory(config.InProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
convertLosslessRb.Enabled = allowLibationFixupCbox.Checked;
|
convertLosslessRb.Enabled = allowLibationFixupCbox.Checked;
|
||||||
convertLossyRb.Enabled = allowLibationFixupCbox.Checked;
|
convertLossyRb.Enabled = allowLibationFixupCbox.Checked;
|
||||||
|
|
||||||
if (!allowLibationFixupCbox.Checked)
|
if (!allowLibationFixupCbox.Checked)
|
||||||
{
|
{
|
||||||
convertLosslessRb.Checked = true;
|
convertLosslessRb.Checked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
39
LibationWinForms/EditTagsDataGridViewImageButtonColumn.cs
Normal file
39
LibationWinForms/EditTagsDataGridViewImageButtonColumn.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
LibationWinForms/Form1.Designer.cs
generated
34
LibationWinForms/Form1.Designer.cs
generated
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,332 +6,377 @@ 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
|
||||||
{
|
{
|
||||||
public partial class Form1 : Form
|
public partial class Form1 : Form
|
||||||
{
|
{
|
||||||
private string backupsCountsLbl_Format { get; }
|
private string backupsCountsLbl_Format { get; }
|
||||||
private string pdfsCountsLbl_Format { get; }
|
private string pdfsCountsLbl_Format { get; }
|
||||||
private string visibleCountLbl_Format { get; }
|
private string visibleCountLbl_Format { get; }
|
||||||
|
|
||||||
private string beginBookBackupsToolStripMenuItem_format { get; }
|
private string beginBookBackupsToolStripMenuItem_format { get; }
|
||||||
private string beginPdfBackupsToolStripMenuItem_format { get; }
|
private string beginPdfBackupsToolStripMenuItem_format { get; }
|
||||||
|
|
||||||
public Form1()
|
public Form1()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
// back up string formats
|
// back up string formats
|
||||||
backupsCountsLbl_Format = backupsCountsLbl.Text;
|
backupsCountsLbl_Format = backupsCountsLbl.Text;
|
||||||
pdfsCountsLbl_Format = pdfsCountsLbl.Text;
|
pdfsCountsLbl_Format = pdfsCountsLbl.Text;
|
||||||
visibleCountLbl_Format = visibleCountLbl.Text;
|
visibleCountLbl_Format = visibleCountLbl.Text;
|
||||||
|
|
||||||
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
|
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
|
||||||
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
|
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
|
||||||
|
|
||||||
// after backing up formats: can set default/temp visible text
|
// after backing up formats: can set default/temp visible text
|
||||||
backupsCountsLbl.Text = "[Calculating backed up book quantities]";
|
backupsCountsLbl.Text = "[Calculating backed up book quantities]";
|
||||||
pdfsCountsLbl.Text = "[Calculating backed up PDFs]";
|
pdfsCountsLbl.Text = "[Calculating backed up PDFs]";
|
||||||
setVisibleCount(null, 0);
|
setVisibleCount(null, 0);
|
||||||
|
|
||||||
if (this.DesignMode)
|
if (this.DesignMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// independent UI updates
|
// independent UI updates
|
||||||
this.Load += setBackupCountsAsync;
|
this.Load += setBackupCountsAsync;
|
||||||
this.Load += (_, __) => RestoreSizeAndLocation();
|
this.Load += (_, __) => RestoreSizeAndLocation();
|
||||||
this.Load += (_, __) => RefreshImportMenu();
|
this.Load += (_, __) => RefreshImportMenu();
|
||||||
|
|
||||||
// start background service
|
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||||
this.Load += (_, __) => startBackgroundImageDownloader();
|
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||||
}
|
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||||
|
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||||
|
}
|
||||||
|
|
||||||
private static void startBackgroundImageDownloader()
|
private void Form1_Load(object sender, EventArgs e)
|
||||||
{
|
|
||||||
// load default/missing cover images. this will also initiate the background image downloader
|
|
||||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
|
||||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
|
||||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
|
||||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Form1_Load(object sender, EventArgs e)
|
|
||||||
{
|
{
|
||||||
if (this.DesignMode)
|
if (this.DesignMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
reloadGrid();
|
reloadGrid();
|
||||||
|
|
||||||
// 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)
|
}
|
||||||
{
|
|
||||||
SaveSizeAndLocation();
|
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
|
||||||
}
|
{
|
||||||
|
SaveSizeAndLocation();
|
||||||
|
}
|
||||||
|
|
||||||
private void RestoreSizeAndLocation()
|
private void RestoreSizeAndLocation()
|
||||||
{
|
{
|
||||||
var config = Configuration.Instance;
|
var config = Configuration.Instance;
|
||||||
|
|
||||||
var width = config.MainFormWidth;
|
var width = config.MainFormWidth;
|
||||||
var height = config.MainFormHeight;
|
var height = config.MainFormHeight;
|
||||||
|
|
||||||
// too small -- something must have gone wrong. use defaults
|
// too small -- something must have gone wrong. use defaults
|
||||||
if (width < 25 || height < 25)
|
if (width < 25 || height < 25)
|
||||||
{
|
{
|
||||||
width = 1023;
|
width = 1023;
|
||||||
height = 578;
|
height = 578;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||||
if (width > Screen.PrimaryScreen.WorkingArea.Width)
|
if (width > Screen.PrimaryScreen.WorkingArea.Width)
|
||||||
width = Screen.PrimaryScreen.WorkingArea.Width;
|
width = Screen.PrimaryScreen.WorkingArea.Width;
|
||||||
if (height > Screen.PrimaryScreen.WorkingArea.Height)
|
if (height > Screen.PrimaryScreen.WorkingArea.Height)
|
||||||
height = Screen.PrimaryScreen.WorkingArea.Height;
|
height = Screen.PrimaryScreen.WorkingArea.Height;
|
||||||
|
|
||||||
var x = config.MainFormX;
|
var x = config.MainFormX;
|
||||||
var y = config.MainFormY;
|
var y = config.MainFormY;
|
||||||
|
|
||||||
var rect = new System.Drawing.Rectangle(x, y, width, height);
|
var rect = new System.Drawing.Rectangle(x, y, width, height);
|
||||||
|
|
||||||
// is proposed rect on a screen?
|
// is proposed rect on a screen?
|
||||||
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
|
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
|
||||||
{
|
{
|
||||||
this.StartPosition = FormStartPosition.Manual;
|
this.StartPosition = FormStartPosition.Manual;
|
||||||
this.DesktopBounds = rect;
|
this.DesktopBounds = rect;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.StartPosition = FormStartPosition.WindowsDefaultLocation;
|
this.StartPosition = FormStartPosition.WindowsDefaultLocation;
|
||||||
this.Size = rect.Size;
|
this.Size = rect.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
|
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
|
||||||
this.WindowState = config.MainFormIsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
|
this.WindowState = config.MainFormIsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveSizeAndLocation()
|
private void SaveSizeAndLocation()
|
||||||
{
|
{
|
||||||
System.Drawing.Point location;
|
System.Drawing.Point location;
|
||||||
System.Drawing.Size size;
|
System.Drawing.Size size;
|
||||||
|
|
||||||
// save location and size if the state is normal
|
// save location and size if the state is normal
|
||||||
if (this.WindowState == FormWindowState.Normal)
|
if (this.WindowState == FormWindowState.Normal)
|
||||||
{
|
{
|
||||||
location = this.Location;
|
location = this.Location;
|
||||||
size = this.Size;
|
size = this.Size;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// save the RestoreBounds if the form is minimized or maximized
|
// save the RestoreBounds if the form is minimized or maximized
|
||||||
location = this.RestoreBounds.Location;
|
location = this.RestoreBounds.Location;
|
||||||
size = this.RestoreBounds.Size;
|
size = this.RestoreBounds.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = Configuration.Instance;
|
var config = Configuration.Instance;
|
||||||
|
|
||||||
config.MainFormX = location.X;
|
config.MainFormX = location.X;
|
||||||
config.MainFormY = location.Y;
|
config.MainFormY = location.Y;
|
||||||
|
|
||||||
config.MainFormWidth = size.Width;
|
config.MainFormWidth = size.Width;
|
||||||
config.MainFormHeight = size.Height;
|
config.MainFormHeight = size.Height;
|
||||||
|
|
||||||
config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized;
|
config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized;
|
||||||
}
|
}
|
||||||
|
|
||||||
#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
|
||||||
var prev_isProcessingGridSelect = isProcessingGridSelect;
|
var prev_isProcessingGridSelect = isProcessingGridSelect;
|
||||||
isProcessingGridSelect = true;
|
isProcessingGridSelect = true;
|
||||||
setGrid();
|
setGrid();
|
||||||
isProcessingGridSelect = prev_isProcessingGridSelect;
|
isProcessingGridSelect = prev_isProcessingGridSelect;
|
||||||
|
|
||||||
// UI init complete. now we can apply filter
|
// UI init complete. now we can apply filter
|
||||||
doFilter(lastGoodFilter);
|
doFilter(lastGoodFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProductsGrid currProductsGrid;
|
private ProductsGrid currProductsGrid;
|
||||||
private void setGrid()
|
private void setGrid()
|
||||||
{
|
{
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
{
|
{
|
||||||
if (currProductsGrid != null)
|
if (currProductsGrid != null)
|
||||||
{
|
{
|
||||||
gridPanel.Controls.Remove(currProductsGrid);
|
gridPanel.Controls.Remove(currProductsGrid);
|
||||||
currProductsGrid.VisibleCountChanged -= setVisibleCount;
|
currProductsGrid.VisibleCountChanged -= setVisibleCount;
|
||||||
currProductsGrid.BackupCountsChanged -= setBackupCountsAsync;
|
currProductsGrid.BackupCountsChanged -= setBackupCountsAsync;
|
||||||
currProductsGrid.Dispose();
|
currProductsGrid.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
|
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
|
||||||
currProductsGrid.VisibleCountChanged += setVisibleCount;
|
currProductsGrid.VisibleCountChanged += setVisibleCount;
|
||||||
currProductsGrid.BackupCountsChanged += setBackupCountsAsync;
|
currProductsGrid.BackupCountsChanged += setBackupCountsAsync;
|
||||||
gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid));
|
gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid));
|
||||||
currProductsGrid.Display();
|
currProductsGrid.Display();
|
||||||
}
|
}
|
||||||
ResumeLayout();
|
ResumeLayout();
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region bottom: qty books visible
|
#region bottom: qty books visible
|
||||||
private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty);
|
private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region bottom: backup counts
|
#region bottom: backup counts
|
||||||
private async void setBackupCountsAsync(object _, object __)
|
private async void setBackupCountsAsync(object _, object __)
|
||||||
{
|
{
|
||||||
LibraryCommands.LibraryStats libraryStats = null;
|
LibraryCommands.LibraryStats libraryStats = null;
|
||||||
await Task.Run(() => libraryStats = LibraryCommands.GetCounts());
|
await Task.Run(() => libraryStats = LibraryCommands.GetCounts());
|
||||||
|
|
||||||
setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress);
|
setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress);
|
||||||
setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded);
|
setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded);
|
||||||
}
|
}
|
||||||
private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress)
|
private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress)
|
||||||
{
|
{
|
||||||
// enable/disable export
|
// enable/disable export
|
||||||
var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress);
|
var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress);
|
||||||
exportLibraryToolStripMenuItem.Enabled = hasResults;
|
exportLibraryToolStripMenuItem.Enabled = hasResults;
|
||||||
|
|
||||||
// update bottom numbers
|
// update bottom numbers
|
||||||
var pending = booksNoProgress + booksDownloadedOnly;
|
var pending = booksNoProgress + booksDownloadedOnly;
|
||||||
var statusStripText
|
var statusStripText
|
||||||
= !hasResults ? "No books. Begin by importing your library"
|
= !hasResults ? "No books. Begin by importing your library"
|
||||||
: pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp)
|
: pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp)
|
||||||
: $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
|
: $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
|
||||||
|
|
||||||
// update menu item
|
// update menu item
|
||||||
var menuItemText
|
var menuItemText
|
||||||
= pending > 0
|
= pending > 0
|
||||||
? $"{pending} remaining"
|
? $"{pending} remaining"
|
||||||
: "All books have been liberated";
|
: "All books have been liberated";
|
||||||
|
|
||||||
// update UI
|
// update UI
|
||||||
statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText);
|
statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText);
|
||||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
|
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
|
||||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
|
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
|
||||||
}
|
}
|
||||||
private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded)
|
private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded)
|
||||||
{
|
{
|
||||||
// update bottom numbers
|
// update bottom numbers
|
||||||
var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||||
var statusStripText
|
var statusStripText
|
||||||
= !hasResults ? ""
|
= !hasResults ? ""
|
||||||
: pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded)
|
: pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded)
|
||||||
: $"| All {pdfsDownloaded} PDFs downloaded";
|
: $"| All {pdfsDownloaded} PDFs downloaded";
|
||||||
|
|
||||||
// update menu item
|
// update menu item
|
||||||
var menuItemText
|
var menuItemText
|
||||||
= pdfsNotDownloaded > 0
|
= pdfsNotDownloaded > 0
|
||||||
? $"{pdfsNotDownloaded} remaining"
|
? $"{pdfsNotDownloaded} remaining"
|
||||||
: "All PDFs have been downloaded";
|
: "All PDFs have been downloaded";
|
||||||
|
|
||||||
// update UI
|
// update UI
|
||||||
statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText);
|
statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText);
|
||||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0);
|
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0);
|
||||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
|
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region filter
|
#region filter
|
||||||
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
|
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
|
||||||
|
|
||||||
private void AddFilterBtn_Click(object sender, EventArgs e)
|
private void AddFilterBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
QuickFilters.Add(this.filterSearchTb.Text);
|
QuickFilters.Add(this.filterSearchTb.Text);
|
||||||
UpdateFilterDropDown();
|
UpdateFilterDropDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e)
|
private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.KeyChar == (char)Keys.Return)
|
if (e.KeyChar == (char)Keys.Return)
|
||||||
{
|
{
|
||||||
doFilter();
|
doFilter();
|
||||||
|
|
||||||
// silence the 'ding'
|
// silence the 'ding'
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
doFilter();
|
doFilter();
|
||||||
}
|
}
|
||||||
private void doFilter()
|
private void doFilter()
|
||||||
{
|
{
|
||||||
if (isProcessingGridSelect || currProductsGrid == null)
|
if (isProcessingGridSelect || currProductsGrid == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
currProductsGrid.Filter(filterSearchTb.Text);
|
currProductsGrid.Filter(filterSearchTb.Text);
|
||||||
lastGoodFilter = filterSearchTb.Text;
|
lastGoodFilter = filterSearchTb.Text;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
|
||||||
// re-apply last good filter
|
// re-apply last good filter
|
||||||
doFilter(lastGoodFilter);
|
doFilter(lastGoodFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Import menu
|
#region Import menu
|
||||||
public void RefreshImportMenu()
|
public void RefreshImportMenu()
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
var count = persister.AccountsSettings.Accounts.Count;
|
var count = persister.AccountsSettings.Accounts.Count;
|
||||||
|
|
||||||
noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0;
|
noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0;
|
||||||
scanLibraryToolStripMenuItem.Visible = count == 1;
|
scanLibraryToolStripMenuItem.Visible = count == 1;
|
||||||
scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1;
|
scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1;
|
||||||
scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1;
|
scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1;
|
||||||
}
|
|
||||||
|
|
||||||
private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e)
|
removeLibraryBooksToolStripMenuItem.Visible = count != 0;
|
||||||
{
|
|
||||||
MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
|
|
||||||
new AccountsDialog(this).ShowDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
if (count == 1)
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
removeLibraryBooksToolStripMenuItem.Click += removeThisAccountToolStripMenuItem_Click;
|
||||||
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
}
|
||||||
scanLibraries(firstAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
removeSomeAccountsToolStripMenuItem.Visible = count > 1;
|
||||||
{
|
removeAllAccountsToolStripMenuItem.Visible = count > 1;
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
}
|
||||||
var allAccounts = persister.AccountsSettings.GetAll();
|
|
||||||
scanLibraries(allAccounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
using var scanAccountsDialog = new ScanAccountsDialog(this);
|
MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
|
||||||
|
new AccountsDialog(this).ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
|
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
return;
|
{
|
||||||
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
|
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
||||||
|
scanLibraries(firstAccount);
|
||||||
|
}
|
||||||
|
|
||||||
if (!scanAccountsDialog.CheckedAccounts.Any())
|
private void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
return;
|
{
|
||||||
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
|
var allAccounts = persister.AccountsSettings.GetAll();
|
||||||
|
scanLibraries(allAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
scanLibraries(scanAccountsDialog.CheckedAccounts);
|
private void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
}
|
{
|
||||||
|
using var scanAccountsDialog = new ScanAccountsDialog(this);
|
||||||
|
|
||||||
private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
|
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
|
||||||
private void scanLibraries(params Account[] accounts)
|
return;
|
||||||
|
|
||||||
|
if (!scanAccountsDialog.CheckedAccounts.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
scanLibraries(scanAccountsDialog.CheckedAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeThisAccountToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
|
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
||||||
|
scanLibrariesRemovedBooks(firstAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
|
var allAccounts = persister.AccountsSettings.GetAll();
|
||||||
|
scanLibrariesRemovedBooks(allAccounts.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using var scanAccountsDialog = new ScanAccountsDialog(this);
|
||||||
|
|
||||||
|
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!scanAccountsDialog.CheckedAccounts.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanLibrariesRemovedBooks(params Account[] accounts)
|
||||||
|
{
|
||||||
|
using var dialog = new RemoveBooksDialog(accounts);
|
||||||
|
dialog.ShowDialog();
|
||||||
|
|
||||||
|
if (dialog.BooksRemoved)
|
||||||
|
reloadGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
|
||||||
|
private void scanLibraries(params Account[] accounts)
|
||||||
{
|
{
|
||||||
using var dialog = new IndexLibraryDialog(accounts);
|
using var dialog = new IndexLibraryDialog(accounts);
|
||||||
dialog.ShowDialog();
|
dialog.ShowDialog();
|
||||||
@ -348,112 +388,113 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
if (totalProcessed > 0)
|
if (totalProcessed > 0)
|
||||||
reloadGrid();
|
reloadGrid();
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region liberate menu
|
#region liberate menu
|
||||||
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow);
|
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow);
|
||||||
|
|
||||||
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow);
|
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow);
|
||||||
|
|
||||||
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
|
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
=> await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
|
=> await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
|
||||||
|
|
||||||
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
|
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Export menu
|
#region Export menu
|
||||||
private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var saveFileDialog = new SaveFileDialog
|
var saveFileDialog = new SaveFileDialog
|
||||||
{
|
{
|
||||||
Title = "Where to export Library",
|
Title = "Where to export Library",
|
||||||
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
|
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (saveFileDialog.ShowDialog() != DialogResult.OK)
|
if (saveFileDialog.ShowDialog() != DialogResult.OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// FilterIndex is 1-based, NOT 0-based
|
// FilterIndex is 1-based, NOT 0-based
|
||||||
switch (saveFileDialog.FilterIndex)
|
switch (saveFileDialog.FilterIndex)
|
||||||
{
|
{
|
||||||
case 1: // xlsx
|
case 1: // xlsx
|
||||||
default:
|
default:
|
||||||
LibraryExporter.ToXlsx(saveFileDialog.FileName);
|
LibraryExporter.ToXlsx(saveFileDialog.FileName);
|
||||||
break;
|
break;
|
||||||
case 2: // csv
|
case 2: // csv
|
||||||
LibraryExporter.ToCsv(saveFileDialog.FileName);
|
LibraryExporter.ToCsv(saveFileDialog.FileName);
|
||||||
break;
|
break;
|
||||||
case 3: // json
|
case 3: // json
|
||||||
LibraryExporter.ToJson(saveFileDialog.FileName);
|
LibraryExporter.ToJson(saveFileDialog.FileName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
|
MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBoxAlertAdmin.Show("Error attempting to export your library.", "Error exporting", ex);
|
MessageBoxAlertAdmin.Show("Error attempting to export your library.", "Error exporting", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region quick filters menu
|
#region quick filters menu
|
||||||
private void loadInitialQuickFilterState()
|
private void loadInitialQuickFilterState()
|
||||||
{
|
{
|
||||||
// set inital state. do once only
|
// set inital state. do once only
|
||||||
firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault;
|
firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault;
|
||||||
|
|
||||||
// load default filter. do once only
|
// load default filter. do once only
|
||||||
if (QuickFilters.UseDefault)
|
if (QuickFilters.UseDefault)
|
||||||
doFilter(QuickFilters.Filters.FirstOrDefault());
|
doFilter(QuickFilters.Filters.FirstOrDefault());
|
||||||
|
|
||||||
// do after every save
|
// do after every save
|
||||||
UpdateFilterDropDown();
|
UpdateFilterDropDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
|
private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked;
|
firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked;
|
||||||
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
|
||||||
for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--)
|
for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var menuItem = quickFiltersToolStripMenuItem.DropDownItems[i];
|
var menuItem = quickFiltersToolStripMenuItem.DropDownItems[i];
|
||||||
if (menuItem.Tag == quickFilterTag)
|
if (menuItem.Tag == quickFilterTag)
|
||||||
quickFiltersToolStripMenuItem.DropDownItems.Remove(menuItem);
|
quickFiltersToolStripMenuItem.DropDownItems.Remove(menuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-populate
|
// re-populate
|
||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var filter in QuickFilters.Filters)
|
foreach (var filter in QuickFilters.Filters)
|
||||||
{
|
{
|
||||||
var menuItem = new ToolStripMenuItem
|
var menuItem = new ToolStripMenuItem
|
||||||
{
|
{
|
||||||
Tag = quickFilterTag,
|
Tag = quickFilterTag,
|
||||||
Text = $"&{++index}: {filter}"
|
Text = $"&{++index}: {filter}"
|
||||||
};
|
};
|
||||||
menuItem.Click += (_, __) => doFilter(filter);
|
menuItem.Click += (_, __) => doFilter(filter);
|
||||||
quickFiltersToolStripMenuItem.DropDownItems.Add(menuItem);
|
quickFiltersToolStripMenuItem.DropDownItems.Add(menuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog();
|
private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region settings menu
|
#region settings menu
|
||||||
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog();
|
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog();
|
||||||
|
|
||||||
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
|
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
get
|
LibraryBook = libraryBook;
|
||||||
|
_memberValues = CreateMemberValueDictionary();
|
||||||
|
|
||||||
|
//Get cover art. If it's default, subscribe to PictureCached
|
||||||
{
|
{
|
||||||
displayValues[nameof(Title)] = book.Title;
|
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
|
||||||
return getSortName(book.Title);
|
|
||||||
|
if (isDefault)
|
||||||
|
FileManager.PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||||
|
|
||||||
|
//Mutable property. Set the field so PropertyChanged isn't fired.
|
||||||
|
_cover = ImageReader.ToImage(picture);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Immutable properties
|
||||||
|
{
|
||||||
|
Title = Book.Title;
|
||||||
|
Series = Book.SeriesNames;
|
||||||
|
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
|
||||||
|
MyRating = ValueOrDefault(Book.UserDefinedItem.Rating?.ToStarString(), "");
|
||||||
|
PurchaseDate = libraryBook.DateAdded.ToString("d");
|
||||||
|
ProductRating = ValueOrDefault(Book.Rating?.ToStarString(), "");
|
||||||
|
Authors = Book.AuthorNames;
|
||||||
|
Narrators = Book.NarratorNames;
|
||||||
|
Category = string.Join(" > ", Book.CategoriesNames);
|
||||||
|
Misc = GetMiscDisplay(libraryBook);
|
||||||
|
Description = GetDescriptionDisplay(Book);
|
||||||
|
}
|
||||||
|
|
||||||
|
//DisplayTags and Liberate properties are live.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Definition.PictureId == Book.PictureId)
|
||||||
|
{
|
||||||
|
Cover = ImageReader.ToImage(e.Picture);
|
||||||
|
FileManager.PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Authors => book.AuthorNames;
|
#region Data Source properties
|
||||||
public string Narrators => book.NarratorNames;
|
|
||||||
|
|
||||||
public int Length
|
public Image Cover
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
displayValues[nameof(Length)]
|
return _cover;
|
||||||
= book.LengthInMinutes == 0
|
|
||||||
? ""
|
|
||||||
: $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min";
|
|
||||||
|
|
||||||
return book.LengthInMinutes;
|
|
||||||
}
|
}
|
||||||
}
|
private set
|
||||||
|
|
||||||
public string Series
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
{
|
||||||
displayValues[nameof(Series)] = book.SeriesNames;
|
_cover = value;
|
||||||
return getSortName(book.SeriesNames);
|
NotifyPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string[] sortPrefixIgnores { get; } = new[] { "the", "a", "an" };
|
public string ProductRating { get; }
|
||||||
private static string getSortName(string unformattedName)
|
public string PurchaseDate { get; }
|
||||||
|
public string MyRating { get; }
|
||||||
|
public string Series { get; }
|
||||||
|
public string Title { get; }
|
||||||
|
public string Length { get; }
|
||||||
|
public string Authors { get; }
|
||||||
|
public string Narrators { get; }
|
||||||
|
public string Category { get; }
|
||||||
|
public string Misc { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||||
|
public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book));
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Data Sorting
|
||||||
|
|
||||||
|
private Dictionary<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
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
// HtmlAgilityPack is expensive. cache results
|
|
||||||
if (descriptionCache is null)
|
|
||||||
{
|
|
||||||
if (book.Description is null)
|
|
||||||
descriptionCache = "";
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var doc = new HtmlAgilityPack.HtmlDocument();
|
|
||||||
doc.LoadHtml(book.Description);
|
|
||||||
var noHtml = doc.DocumentNode.InnerText;
|
|
||||||
descriptionCache
|
|
||||||
= noHtml.Length < 63
|
|
||||||
? noHtml
|
|
||||||
: noHtml.Substring(0, 60) + "...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return descriptionCache;
|
#region Static library display functions
|
||||||
}
|
|
||||||
|
public static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus)
|
||||||
|
{
|
||||||
|
(string libState, string image_lib) = liberatedStatus switch
|
||||||
|
{
|
||||||
|
LiberatedState.Liberated => ("Liberated", "green"),
|
||||||
|
LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
|
||||||
|
LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"),
|
||||||
|
_ => throw new Exception("Unexpected liberation state")
|
||||||
|
};
|
||||||
|
|
||||||
|
(string pdfState, string image_pdf) = pdfStatus switch
|
||||||
|
{
|
||||||
|
PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"),
|
||||||
|
PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"),
|
||||||
|
PdfState.NoPdf => ("", ""),
|
||||||
|
_ => throw new Exception("Unexpected PDF state")
|
||||||
|
};
|
||||||
|
|
||||||
|
var mouseoverText = libState + pdfState;
|
||||||
|
|
||||||
|
if (liberatedStatus == LiberatedState.NotDownloaded ||
|
||||||
|
liberatedStatus == LiberatedState.PartialDownload ||
|
||||||
|
pdfStatus == PdfState.NotDownloaded)
|
||||||
|
mouseoverText += "\r\nClick to complete";
|
||||||
|
|
||||||
|
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
|
||||||
|
|
||||||
|
return (mouseoverText, buttonImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Category => string.Join(" > ", book.CategoriesNames);
|
/// <summary>
|
||||||
|
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
|
||||||
// star ratings retain numeric value but display star text. this is needed because just using star text doesn't sort correctly:
|
/// </summary>
|
||||||
// - star
|
private static string GetDescriptionDisplay(Book book)
|
||||||
// - star star
|
|
||||||
// - star 1/2
|
|
||||||
|
|
||||||
public string Product_Rating
|
|
||||||
{
|
{
|
||||||
get
|
var doc = new HtmlAgilityPack.HtmlDocument();
|
||||||
{
|
doc.LoadHtml(book.Description);
|
||||||
displayValues[nameof(Product_Rating)] = starString(book.Rating);
|
var noHtml = doc.DocumentNode.InnerText;
|
||||||
return firstScore(book.Rating);
|
return
|
||||||
}
|
noHtml.Length < 63 ?
|
||||||
|
noHtml :
|
||||||
|
noHtml.Substring(0, 60) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Purchase_Date
|
/// <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>
|
||||||
|
private static string GetMiscDisplay(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
get
|
var details = new List<string>();
|
||||||
{
|
|
||||||
displayValues[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d");
|
var locale = ValueOrDefault(libraryBook.Book.Locale, "[unknown]");
|
||||||
return libraryBook.DateAdded.ToString("yyyy-MM-dd HH:mm:ss");
|
var acct = ValueOrDefault(libraryBook.Account, "[unknown]");
|
||||||
}
|
|
||||||
|
details.Add($"Account: {locale} - {acct}");
|
||||||
|
|
||||||
|
if (libraryBook.Book.HasPdf)
|
||||||
|
details.Add("Has PDF");
|
||||||
|
if (libraryBook.Book.IsAbridged)
|
||||||
|
details.Add("Abridged");
|
||||||
|
if (libraryBook.Book.DatePublished.HasValue)
|
||||||
|
details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}");
|
||||||
|
// this goes last since it's most likely to have a line-break
|
||||||
|
if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
|
||||||
|
details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
|
||||||
|
|
||||||
|
if (!details.Any())
|
||||||
|
return "[details not imported]";
|
||||||
|
|
||||||
|
return string.Join("\r\n", details);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string My_Rating
|
//Maybe add to Dinah StringExtensions?
|
||||||
{
|
private static string ValueOrDefault(string value, string defaultValue)
|
||||||
get
|
=> string.IsNullOrWhiteSpace(value) ? defaultValue : value;
|
||||||
{
|
|
||||||
displayValues[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating);
|
|
||||||
return firstScore(book.UserDefinedItem.Rating);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string starString(Rating rating)
|
#endregion
|
||||||
=> (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 locale
|
|
||||||
= string.IsNullOrWhiteSpace(book.Locale)
|
|
||||||
? "[unknown]"
|
|
||||||
: book.Locale;
|
|
||||||
var acct
|
|
||||||
= string.IsNullOrWhiteSpace(libraryBook.Account)
|
|
||||||
? "[unknown]"
|
|
||||||
: libraryBook.Account;
|
|
||||||
details.Add($"Account: {locale} - {acct}");
|
|
||||||
|
|
||||||
if (book.HasPdf)
|
|
||||||
details.Add("Has PDF");
|
|
||||||
if (book.IsAbridged)
|
|
||||||
details.Add("Abridged");
|
|
||||||
if (book.DatePublished.HasValue)
|
|
||||||
details.Add($"Date pub'd: {book.DatePublished.Value:MM/dd/yyyy}");
|
|
||||||
// this goes last since it's most likely to have a line-break
|
|
||||||
if (!string.IsNullOrWhiteSpace(book.Publisher))
|
|
||||||
details.Add($"Pub: {book.Publisher.Trim()}");
|
|
||||||
|
|
||||||
if (!details.Any())
|
|
||||||
return "[details not imported]";
|
|
||||||
|
|
||||||
return string.Join("\r\n", details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
LibationWinForms/IMemberComparable.cs
Normal file
11
LibationWinForms/IMemberComparable.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace LibationWinForms
|
||||||
|
{
|
||||||
|
internal interface IMemberComparable
|
||||||
|
{
|
||||||
|
IComparer GetMemberComparer(Type memberType);
|
||||||
|
object GetMemberValue(string memberName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
31
LibationWinForms/LiberateDataGridViewImageButtonColumn.cs
Normal file
31
LibationWinForms/LiberateDataGridViewImageButtonColumn.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
LibationWinForms/MemberComparer.cs
Normal file
21
LibationWinForms/MemberComparer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
10
LibationWinForms/ObjectComparer[T].cs
Normal file
10
LibationWinForms/ObjectComparer[T].cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
357
LibationWinForms/ProductsGrid.Designer.cs
generated
357
LibationWinForms/ProductsGrid.Designer.cs
generated
@ -28,146 +28,195 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
this.components = new System.ComponentModel.Container();
|
this.components = new System.ComponentModel.Container();
|
||||||
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
|
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||||
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
|
||||||
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
|
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
||||||
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewImageButtonBoxColumn1 = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
|
||||||
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
|
||||||
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
|
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
|
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.SuspendLayout();
|
this.dataGridViewImageButtonBoxColumn2 = new LibationWinForms.EditTagsDataGridViewImageButtonColumn();
|
||||||
//
|
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
|
||||||
// gridEntryBindingSource
|
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
|
||||||
//
|
this.SuspendLayout();
|
||||||
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry);
|
//
|
||||||
//
|
// gridEntryBindingSource
|
||||||
// gridEntryDataGridView
|
//
|
||||||
//
|
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry);
|
||||||
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
//
|
||||||
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
// gridEntryDataGridView
|
||||||
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
//
|
||||||
this.dataGridViewImageColumn1,
|
this.gridEntryDataGridView.AllowUserToAddRows = false;
|
||||||
this.dataGridViewTextBoxColumn1,
|
this.gridEntryDataGridView.AllowUserToDeleteRows = false;
|
||||||
this.dataGridViewTextBoxColumn2,
|
this.gridEntryDataGridView.AllowUserToResizeRows = false;
|
||||||
this.dataGridViewTextBoxColumn3,
|
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
||||||
this.dataGridViewTextBoxColumn4,
|
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
this.dataGridViewTextBoxColumn5,
|
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||||
this.dataGridViewTextBoxColumn6,
|
this.dataGridViewImageButtonBoxColumn1,
|
||||||
this.dataGridViewTextBoxColumn7,
|
this.dataGridViewImageColumn1,
|
||||||
this.dataGridViewTextBoxColumn8,
|
this.dataGridViewTextBoxColumn1,
|
||||||
this.dataGridViewTextBoxColumn9,
|
this.dataGridViewTextBoxColumn2,
|
||||||
this.dataGridViewTextBoxColumn10,
|
this.dataGridViewTextBoxColumn3,
|
||||||
this.dataGridViewTextBoxColumn11});
|
this.dataGridViewTextBoxColumn4,
|
||||||
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
|
this.dataGridViewTextBoxColumn5,
|
||||||
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58);
|
this.dataGridViewTextBoxColumn6,
|
||||||
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
|
this.dataGridViewTextBoxColumn7,
|
||||||
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220);
|
this.dataGridViewTextBoxColumn8,
|
||||||
this.gridEntryDataGridView.TabIndex = 0;
|
this.dataGridViewTextBoxColumn9,
|
||||||
//
|
this.dataGridViewTextBoxColumn10,
|
||||||
// dataGridViewImageColumn1
|
this.dataGridViewTextBoxColumn11,
|
||||||
//
|
this.dataGridViewImageButtonBoxColumn2});
|
||||||
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
|
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
|
||||||
this.dataGridViewImageColumn1.HeaderText = "Cover";
|
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||||
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
|
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
|
||||||
this.dataGridViewImageColumn1.ReadOnly = true;
|
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||||
//
|
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||||
// dataGridViewTextBoxColumn1
|
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||||
//
|
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||||
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
|
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
|
||||||
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
|
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
|
||||||
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
|
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
this.dataGridViewTextBoxColumn1.ReadOnly = true;
|
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
|
||||||
//
|
this.gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
// dataGridViewTextBoxColumn2
|
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
|
||||||
//
|
this.gridEntryDataGridView.ReadOnly = true;
|
||||||
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
|
this.gridEntryDataGridView.RowHeadersVisible = false;
|
||||||
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
|
this.gridEntryDataGridView.RowTemplate.Height = 82;
|
||||||
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
|
this.gridEntryDataGridView.Size = new System.Drawing.Size(1505, 380);
|
||||||
this.dataGridViewTextBoxColumn2.ReadOnly = true;
|
this.gridEntryDataGridView.TabIndex = 0;
|
||||||
//
|
//
|
||||||
// dataGridViewTextBoxColumn3
|
// dataGridViewImageButtonBoxColumn1
|
||||||
//
|
//
|
||||||
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
|
this.dataGridViewImageButtonBoxColumn1.DataPropertyName = "Liberate";
|
||||||
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
|
this.dataGridViewImageButtonBoxColumn1.HeaderText = "Liberate";
|
||||||
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
|
this.dataGridViewImageButtonBoxColumn1.Name = "dataGridViewImageButtonBoxColumn1";
|
||||||
this.dataGridViewTextBoxColumn3.ReadOnly = true;
|
this.dataGridViewImageButtonBoxColumn1.ReadOnly = true;
|
||||||
//
|
this.dataGridViewImageButtonBoxColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||||
// dataGridViewTextBoxColumn4
|
this.dataGridViewImageButtonBoxColumn1.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||||
//
|
this.dataGridViewImageButtonBoxColumn1.Width = 70;
|
||||||
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
|
//
|
||||||
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
|
// dataGridViewImageColumn1
|
||||||
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
|
//
|
||||||
this.dataGridViewTextBoxColumn4.ReadOnly = true;
|
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
|
||||||
//
|
this.dataGridViewImageColumn1.HeaderText = "Cover";
|
||||||
// dataGridViewTextBoxColumn5
|
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
|
||||||
//
|
this.dataGridViewImageColumn1.ReadOnly = true;
|
||||||
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
|
this.dataGridViewImageColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||||
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
|
this.dataGridViewImageColumn1.ToolTipText = "Cover Art";
|
||||||
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
|
this.dataGridViewImageColumn1.Width = 80;
|
||||||
this.dataGridViewTextBoxColumn5.ReadOnly = true;
|
//
|
||||||
//
|
// dataGridViewTextBoxColumn1
|
||||||
// dataGridViewTextBoxColumn6
|
//
|
||||||
//
|
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
|
||||||
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
|
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
|
||||||
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
|
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
|
||||||
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
|
this.dataGridViewTextBoxColumn1.ReadOnly = true;
|
||||||
this.dataGridViewTextBoxColumn6.ReadOnly = true;
|
this.dataGridViewTextBoxColumn1.Width = 200;
|
||||||
//
|
//
|
||||||
// dataGridViewTextBoxColumn7
|
// dataGridViewTextBoxColumn2
|
||||||
//
|
//
|
||||||
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
|
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
|
||||||
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
|
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
|
||||||
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
|
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
|
||||||
this.dataGridViewTextBoxColumn7.ReadOnly = true;
|
this.dataGridViewTextBoxColumn2.ReadOnly = true;
|
||||||
//
|
//
|
||||||
// dataGridViewTextBoxColumn8
|
// dataGridViewTextBoxColumn3
|
||||||
//
|
//
|
||||||
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating";
|
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
|
||||||
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating";
|
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
|
||||||
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8";
|
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
|
||||||
this.dataGridViewTextBoxColumn8.ReadOnly = true;
|
this.dataGridViewTextBoxColumn3.ReadOnly = true;
|
||||||
//
|
//
|
||||||
// dataGridViewTextBoxColumn9
|
// dataGridViewTextBoxColumn4
|
||||||
//
|
//
|
||||||
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date";
|
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
|
||||||
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date";
|
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
|
||||||
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9";
|
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
|
||||||
this.dataGridViewTextBoxColumn9.ReadOnly = true;
|
this.dataGridViewTextBoxColumn4.ReadOnly = true;
|
||||||
//
|
this.dataGridViewTextBoxColumn4.ToolTipText = "Recording Length";
|
||||||
// dataGridViewTextBoxColumn10
|
//
|
||||||
//
|
// dataGridViewTextBoxColumn5
|
||||||
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating";
|
//
|
||||||
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating";
|
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
|
||||||
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10";
|
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
|
||||||
this.dataGridViewTextBoxColumn10.ReadOnly = true;
|
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
|
||||||
//
|
this.dataGridViewTextBoxColumn5.ReadOnly = true;
|
||||||
// dataGridViewTextBoxColumn11
|
//
|
||||||
//
|
// dataGridViewTextBoxColumn6
|
||||||
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
|
//
|
||||||
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
|
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
|
||||||
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
|
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
|
||||||
this.dataGridViewTextBoxColumn11.ReadOnly = true;
|
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
|
||||||
//
|
this.dataGridViewTextBoxColumn6.ReadOnly = true;
|
||||||
// ProductsGrid
|
//
|
||||||
//
|
// dataGridViewTextBoxColumn7
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
//
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
|
||||||
this.Controls.Add(this.gridEntryDataGridView);
|
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
|
||||||
this.Name = "ProductsGrid";
|
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
|
||||||
this.Size = new System.Drawing.Size(434, 329);
|
this.dataGridViewTextBoxColumn7.ReadOnly = true;
|
||||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
|
//
|
||||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
|
// ProductRating
|
||||||
this.ResumeLayout(false);
|
//
|
||||||
|
this.dataGridViewTextBoxColumn8.DataPropertyName = "ProductRating";
|
||||||
|
this.dataGridViewTextBoxColumn8.HeaderText = "Product Rating";
|
||||||
|
this.dataGridViewTextBoxColumn8.Name = "ProductRating";
|
||||||
|
this.dataGridViewTextBoxColumn8.ReadOnly = true;
|
||||||
|
this.dataGridViewTextBoxColumn8.Width = 108;
|
||||||
|
//
|
||||||
|
// PurchaseDate
|
||||||
|
//
|
||||||
|
this.dataGridViewTextBoxColumn9.DataPropertyName = "PurchaseDate";
|
||||||
|
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase Date";
|
||||||
|
this.dataGridViewTextBoxColumn9.Name = "PurchaseDate";
|
||||||
|
this.dataGridViewTextBoxColumn9.ReadOnly = true;
|
||||||
|
//
|
||||||
|
// MyRating
|
||||||
|
//
|
||||||
|
this.dataGridViewTextBoxColumn10.DataPropertyName = "MyRating";
|
||||||
|
this.dataGridViewTextBoxColumn10.HeaderText = "My Rating";
|
||||||
|
this.dataGridViewTextBoxColumn10.Name = "MyRating";
|
||||||
|
this.dataGridViewTextBoxColumn10.ReadOnly = true;
|
||||||
|
this.dataGridViewTextBoxColumn10.Width = 108;
|
||||||
|
//
|
||||||
|
// dataGridViewTextBoxColumn11
|
||||||
|
//
|
||||||
|
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
|
||||||
|
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
|
||||||
|
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
|
||||||
|
this.dataGridViewTextBoxColumn11.ReadOnly = true;
|
||||||
|
this.dataGridViewTextBoxColumn11.Width = 135;
|
||||||
|
//
|
||||||
|
// dataGridViewImageButtonBoxColumn2
|
||||||
|
//
|
||||||
|
this.dataGridViewImageButtonBoxColumn2.DataPropertyName = "DisplayTags";
|
||||||
|
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Edit Tags";
|
||||||
|
this.dataGridViewImageButtonBoxColumn2.Name = "dataGridViewImageButtonBoxColumn2";
|
||||||
|
this.dataGridViewImageButtonBoxColumn2.ReadOnly = true;
|
||||||
|
this.dataGridViewImageButtonBoxColumn2.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||||
|
this.dataGridViewImageButtonBoxColumn2.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||||
|
//
|
||||||
|
// ProductsGrid
|
||||||
|
//
|
||||||
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
this.Controls.Add(this.gridEntryDataGridView);
|
||||||
|
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
|
this.Name = "ProductsGrid";
|
||||||
|
this.Size = new System.Drawing.Size(1505, 380);
|
||||||
|
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
|
||||||
|
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,17 +224,19 @@
|
|||||||
|
|
||||||
private System.Windows.Forms.BindingSource gridEntryBindingSource;
|
private System.Windows.Forms.BindingSource gridEntryBindingSource;
|
||||||
private System.Windows.Forms.DataGridView gridEntryDataGridView;
|
private System.Windows.Forms.DataGridView gridEntryDataGridView;
|
||||||
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
|
private LiberateDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn1;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
|
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
|
||||||
}
|
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
|
||||||
|
private EditTagsDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,418 +1,200 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
// INSTRUCTIONS TO UPDATE DATA_GRID_VIEW
|
// INSTRUCTIONS TO UPDATE DATA_GRID_VIEW
|
||||||
// - delete current DataGridView
|
// - delete current DataGridView
|
||||||
// - view > other windows > data sources
|
// - view > other windows > data sources
|
||||||
// - refresh
|
// - refresh
|
||||||
// OR
|
// OR
|
||||||
// - Add New Data Source
|
// - Add New Data Source
|
||||||
// Object. Next
|
// Object. Next
|
||||||
// LibationWinForms
|
// 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
|
||||||
public partial class ProductsGrid : UserControl
|
// AS OF AUGUST 2021 THIS DOES NOT WORK IN VS2019 WITH .NET-5 PROJECTS
|
||||||
{
|
|
||||||
public event EventHandler<int> VisibleCountChanged;
|
|
||||||
public event EventHandler BackupCountsChanged;
|
|
||||||
|
|
||||||
private const string EDIT_TAGS = "Edit Tags";
|
public partial class ProductsGrid : UserControl
|
||||||
private const string LIBERATE = "Liberate";
|
{
|
||||||
|
public event EventHandler<int> VisibleCountChanged;
|
||||||
|
public event EventHandler BackupCountsChanged;
|
||||||
|
|
||||||
// alias
|
// alias
|
||||||
private DataGridView dataGridView => gridEntryDataGridView;
|
private DataGridView _dataGridView => gridEntryDataGridView;
|
||||||
|
|
||||||
public ProductsGrid()
|
public ProductsGrid()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
formatDataGridView();
|
|
||||||
addLiberateButtons();
|
|
||||||
addEditTagsButtons();
|
|
||||||
formatColumns();
|
|
||||||
|
|
||||||
manageLiveImageUpdateSubscriptions();
|
// sorting breaks filters. must reapply filters after sorting
|
||||||
|
_dataGridView.Sorted += (_, __) => Filter();
|
||||||
|
_dataGridView.CellContentClick += DataGridView_CellContentClick;
|
||||||
|
|
||||||
enableDoubleBuffering();
|
EnableDoubleBuffering();
|
||||||
}
|
}
|
||||||
|
private void EnableDoubleBuffering()
|
||||||
private void enableDoubleBuffering()
|
|
||||||
{
|
{
|
||||||
var propertyInfo = dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
var propertyInfo = _dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||||
|
|
||||||
//var before = (bool)propertyInfo.GetValue(dataGridView);
|
propertyInfo.SetValue(_dataGridView, true, null);
|
||||||
propertyInfo.SetValue(dataGridView, true, null);
|
|
||||||
//var after = (bool)propertyInfo.GetValue(dataGridView);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void formatDataGridView()
|
#region Button controls
|
||||||
{
|
|
||||||
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.
|
private async void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
|
||||||
// this must be done before databinding. or can alter later by iterating through rows
|
|
||||||
dataGridView.RowTemplate.Height = 82;
|
|
||||||
dataGridView.CellFormatting += replaceFormatted;
|
|
||||||
dataGridView.CellFormatting += hiddenFormatting;
|
|
||||||
|
|
||||||
// sorting breaks filters. must reapply filters after sorting
|
|
||||||
dataGridView.Sorted += (_, __) => filter();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region format text cells. ie: not buttons
|
|
||||||
private void replaceFormatted(object sender, DataGridViewCellFormattingEventArgs e)
|
|
||||||
{
|
|
||||||
var col = ((DataGridView)sender).Columns[e.ColumnIndex];
|
|
||||||
if (col is DataGridViewTextBoxColumn textCol && getGridEntry(e.RowIndex).TryDisplayValue(textCol.Name, out string value))
|
|
||||||
{
|
|
||||||
// DO NOT DO THIS: getCell(e).Value = value;
|
|
||||||
// it's the wrong way and will infinitely call CellFormatting on each assign
|
|
||||||
|
|
||||||
// this is the correct way. will actually set FormattedValue (and EditedFormattedValue) while leaving Value as-is for sorting
|
|
||||||
e.Value = value;
|
|
||||||
|
|
||||||
getCell(e).ToolTipText = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hiddenFormatting(object sender, DataGridViewCellFormattingEventArgs e)
|
|
||||||
{
|
|
||||||
var dgv = (DataGridView)sender;
|
|
||||||
// no action needed for buttons
|
|
||||||
if (e.RowIndex < 0 || dgv.Columns[e.ColumnIndex] is DataGridViewButtonColumn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var isHidden = getGridEntry(e.RowIndex).TagsEnumerated.Contains("hidden");
|
|
||||||
|
|
||||||
getCell(e).Style
|
|
||||||
= isHidden
|
|
||||||
? new DataGridViewCellStyle { ForeColor = Color.LightGray }
|
|
||||||
: dgv.DefaultCellStyle;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region liberation buttons
|
|
||||||
private void addLiberateButtons()
|
|
||||||
{
|
|
||||||
dataGridView.Columns.Insert(0, new DataGridViewButtonColumn { HeaderText = LIBERATE });
|
|
||||||
|
|
||||||
dataGridView.CellPainting += liberate_Paint;
|
|
||||||
dataGridView.CellContentClick += liberate_Click;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void liberate_Paint(object sender, DataGridViewCellPaintingEventArgs e)
|
|
||||||
{
|
|
||||||
if (!isColumnValid(e, LIBERATE))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var cell = getCell(e);
|
|
||||||
var gridEntry = getGridEntry(e.RowIndex);
|
|
||||||
var liberatedStatus = gridEntry.Liberated_Status;
|
|
||||||
var pdfStatus = gridEntry.Pdf_Status;
|
|
||||||
|
|
||||||
// mouseover text
|
|
||||||
{
|
|
||||||
var libState = liberatedStatus switch
|
|
||||||
{
|
|
||||||
LiberatedState.Liberated => "Liberated",
|
|
||||||
LiberatedState.PartialDownload => "File has been at least\r\npartially downloaded",
|
|
||||||
LiberatedState.NotDownloaded => "Book NOT downloaded",
|
|
||||||
_ => throw new Exception("Unexpected liberation state")
|
|
||||||
};
|
|
||||||
|
|
||||||
var pdfState = pdfStatus switch
|
|
||||||
{
|
|
||||||
PdfState.Downloaded => "\r\nPDF downloaded",
|
|
||||||
PdfState.NotDownloaded => "\r\nPDF NOT downloaded",
|
|
||||||
PdfState.NoPdf => "",
|
|
||||||
_ => throw new Exception("Unexpected PDF state")
|
|
||||||
};
|
|
||||||
|
|
||||||
var text = libState + pdfState;
|
|
||||||
|
|
||||||
if (liberatedStatus == LiberatedState.NotDownloaded ||
|
|
||||||
liberatedStatus == LiberatedState.PartialDownload ||
|
|
||||||
pdfStatus == PdfState.NotDownloaded)
|
|
||||||
text += "\r\nClick to complete";
|
|
||||||
|
|
||||||
//DEBUG//cell.Value = text;
|
|
||||||
cell.ToolTipText = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw img
|
|
||||||
{
|
|
||||||
var image_lib
|
|
||||||
= liberatedStatus == LiberatedState.NotDownloaded ? "red"
|
|
||||||
: liberatedStatus == LiberatedState.PartialDownload ? "yellow"
|
|
||||||
: liberatedStatus == LiberatedState.Liberated ? "green"
|
|
||||||
: throw new Exception("Unexpected liberation state");
|
|
||||||
var image_pdf
|
|
||||||
= pdfStatus == PdfState.NoPdf ? ""
|
|
||||||
: pdfStatus == PdfState.NotDownloaded ? "_pdf_no"
|
|
||||||
: pdfStatus == PdfState.Downloaded ? "_pdf_yes"
|
|
||||||
: throw new Exception("Unexpected PDF state");
|
|
||||||
var image = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
|
|
||||||
drawImage(e, image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void liberate_Click(object sender, DataGridViewCellEventArgs e)
|
|
||||||
{
|
|
||||||
if (!isColumnValid(e, LIBERATE))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var libraryBook = getGridEntry(e.RowIndex).GetLibraryBook();
|
|
||||||
|
|
||||||
// liberated: open explorer to file
|
|
||||||
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book))
|
|
||||||
{
|
|
||||||
var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
|
||||||
if (!Go.To.File(filePath))
|
|
||||||
MessageBox.Show($"File not found:\r\n{filePath}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// else: liberate
|
|
||||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public void RefreshRow(string productId)
|
|
||||||
{
|
|
||||||
var rowId = getRowId((ge) => ge.AudibleProductId == productId);
|
|
||||||
|
|
||||||
// update cells incl Liberate button text
|
|
||||||
dataGridView.InvalidateRow(rowId);
|
|
||||||
|
|
||||||
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
|
|
||||||
filter();
|
|
||||||
|
|
||||||
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region tag buttons
|
|
||||||
private void addEditTagsButtons()
|
|
||||||
{
|
|
||||||
dataGridView.Columns.Add(new DataGridViewButtonColumn { HeaderText = EDIT_TAGS });
|
|
||||||
|
|
||||||
dataGridView.CellPainting += editTags_Paint;
|
|
||||||
dataGridView.CellContentClick += editTags_Click;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void editTags_Paint(object sender, DataGridViewCellPaintingEventArgs e)
|
|
||||||
{
|
|
||||||
// DataGridView Image for Button Column: https://stackoverflow.com/a/36253883
|
|
||||||
|
|
||||||
if (!isColumnValid(e, EDIT_TAGS))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var cell = getCell(e);
|
|
||||||
var gridEntry = getGridEntry(e.RowIndex);
|
|
||||||
|
|
||||||
var displayTags = gridEntry.TagsEnumerated.ToList();
|
|
||||||
|
|
||||||
if (displayTags.Any())
|
|
||||||
cell.Value = string.Join("\r\n", displayTags);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// if removing all tags: clear previous tag text
|
|
||||||
cell.Value = "";
|
|
||||||
drawImage(e, Properties.Resources.edit_tags_25x25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void editTags_Click(object sender, DataGridViewCellEventArgs e)
|
|
||||||
{
|
|
||||||
// handle grid button click: https://stackoverflow.com/a/13687844
|
|
||||||
|
|
||||||
var dgv = (DataGridView)sender;
|
|
||||||
|
|
||||||
if (!isColumnValid(e, EDIT_TAGS))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var liveGridEntry = getGridEntry(e.RowIndex);
|
|
||||||
|
|
||||||
// EditTagsDialog should display better-formatted title
|
|
||||||
liveGridEntry.TryDisplayValue(nameof(liveGridEntry.Title), out string value);
|
|
||||||
|
|
||||||
var bookDetailsForm = new BookDetailsDialog(value, liveGridEntry.Tags);
|
|
||||||
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.GetBook(), bookDetailsForm.NewTags);
|
|
||||||
if (qtyChanges == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// force a re-draw, and re-apply filters
|
|
||||||
|
|
||||||
// needed to update text colors
|
|
||||||
dgv.InvalidateRow(e.RowIndex);
|
|
||||||
|
|
||||||
filter();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private static void drawImage(DataGridViewCellPaintingEventArgs e, Bitmap image)
|
|
||||||
{
|
|
||||||
e.Paint(e.CellBounds, DataGridViewPaintParts.All);
|
|
||||||
|
|
||||||
var w = image.Width;
|
|
||||||
var h = image.Height;
|
|
||||||
var x = e.CellBounds.Left + (e.CellBounds.Width - w) / 2;
|
|
||||||
var y = e.CellBounds.Top + (e.CellBounds.Height - h) / 2;
|
|
||||||
|
|
||||||
e.Graphics.DrawImage(image, new Rectangle(x, y, w, h));
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool isColumnValid(DataGridViewCellEventArgs e, string colName) => isColumnValid(e.RowIndex, e.ColumnIndex, colName);
|
|
||||||
private bool isColumnValid(DataGridViewCellPaintingEventArgs e, string colName) => isColumnValid(e.RowIndex, e.ColumnIndex, colName);
|
|
||||||
private bool isColumnValid(int rowIndex, int colIndex, string colName)
|
|
||||||
{
|
|
||||||
var col = dataGridView.Columns[colIndex];
|
|
||||||
return rowIndex >= 0 && col.Name == colName && col is DataGridViewButtonColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatColumns()
|
|
||||||
{
|
|
||||||
for (var i = dataGridView.ColumnCount - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var col = dataGridView.Columns[i];
|
|
||||||
|
|
||||||
// initial HeaderText is the lookup name from GridEntry class. any formatting below won't change this
|
|
||||||
col.Name = col.HeaderText;
|
|
||||||
|
|
||||||
if (!(col is DataGridViewImageColumn || col is DataGridViewButtonColumn))
|
|
||||||
col.SortMode = DataGridViewColumnSortMode.Automatic;
|
|
||||||
|
|
||||||
col.HeaderText = col.HeaderText.Replace("_", " ");
|
|
||||||
|
|
||||||
col.Width = col.Name switch
|
|
||||||
{
|
|
||||||
LIBERATE => 70,
|
|
||||||
nameof(GridEntry.Cover) => 80,
|
|
||||||
nameof(GridEntry.Title) => col.Width * 2,
|
|
||||||
nameof(GridEntry.Misc) => (int)(col.Width * 1.35),
|
|
||||||
var n when n.In(nameof(GridEntry.My_Rating), nameof(GridEntry.Product_Rating)) => col.Width + 8,
|
|
||||||
_ => col.Width
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region live update newly downloaded and cached images
|
|
||||||
private void manageLiveImageUpdateSubscriptions()
|
|
||||||
{
|
{
|
||||||
FileManager.PictureStorage.PictureCached += crossThreadImageUpdate;
|
// handle grid button click: https://stackoverflow.com/a/13687844
|
||||||
Disposed += (_, __) => FileManager.PictureStorage.PictureCached -= crossThreadImageUpdate;
|
if (e.RowIndex < 0 || _dataGridView.Columns[e.ColumnIndex] is not DataGridViewButtonColumn)
|
||||||
}
|
|
||||||
|
|
||||||
private void crossThreadImageUpdate(object _, string pictureId)
|
|
||||||
=> dataGridView.UIThread(() => updateRowImage(pictureId));
|
|
||||||
private void updateRowImage(string pictureId)
|
|
||||||
{
|
|
||||||
var rowId = getRowId((ge) => ge.PictureId == pictureId);
|
|
||||||
if (rowId > -1)
|
|
||||||
dataGridView.InvalidateRow(rowId);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private bool hasBeenDisplayed = false;
|
|
||||||
public void Display()
|
|
||||||
{
|
|
||||||
if (hasBeenDisplayed)
|
|
||||||
return;
|
|
||||||
hasBeenDisplayed = true;
|
|
||||||
|
|
||||||
//
|
|
||||||
// transform into sorted GridEntry.s BEFORE binding
|
|
||||||
//
|
|
||||||
using var context = DbContexts.GetContext();
|
|
||||||
var lib = context.GetLibrary_Flat_NoTracking();
|
|
||||||
|
|
||||||
// if no data. hide all columns. return
|
|
||||||
if (!lib.Any())
|
|
||||||
{
|
|
||||||
for (var i = dataGridView.ColumnCount - 1; i >= 0; i--)
|
|
||||||
dataGridView.Columns.RemoveAt(i);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var orderedGridEntries = lib
|
|
||||||
.Select(lb => new GridEntry(lb)).ToList()
|
|
||||||
// default load order
|
|
||||||
.OrderByDescending(ge => ge.Purchase_Date)
|
|
||||||
//// more advanced example: sort by author, then series, then title
|
|
||||||
//.OrderBy(ge => ge.Authors)
|
|
||||||
// .ThenBy(ge => ge.Series)
|
|
||||||
// .ThenBy(ge => ge.Title)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
//
|
|
||||||
// BIND
|
|
||||||
//
|
|
||||||
gridEntryBindingSource.DataSource = orderedGridEntries.ToSortableBindingList();
|
|
||||||
|
|
||||||
//
|
|
||||||
// FILTER
|
|
||||||
//
|
|
||||||
filter();
|
|
||||||
|
|
||||||
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region filter
|
|
||||||
string _filterSearchString;
|
|
||||||
private void filter() => Filter(_filterSearchString);
|
|
||||||
public void Filter(string searchString)
|
|
||||||
{
|
|
||||||
_filterSearchString = searchString;
|
|
||||||
|
|
||||||
if (dataGridView.Rows.Count == 0)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var searchResults = SearchEngineCommands.Search(searchString);
|
var liveGridEntry = getGridEntry(e.RowIndex);
|
||||||
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/18942430
|
switch (_dataGridView.Columns[e.ColumnIndex].DataPropertyName)
|
||||||
var currencyManager = (CurrencyManager)BindingContext[dataGridView.DataSource];
|
{
|
||||||
currencyManager.SuspendBinding();
|
case nameof(liveGridEntry.Liberate):
|
||||||
{
|
await Liberate_Click(liveGridEntry);
|
||||||
for (var r = dataGridView.RowCount - 1; r >= 0; r--)
|
break;
|
||||||
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
|
case nameof(liveGridEntry.DisplayTags):
|
||||||
}
|
EditTags_Click(liveGridEntry);
|
||||||
currencyManager.ResumeBinding();
|
break;
|
||||||
VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible));
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
private int getRowId(Func<GridEntry, bool> func) => dataGridView.GetRowIdOfBoundItem(func);
|
private async Task Liberate_Click(GridEntry liveGridEntry)
|
||||||
|
{
|
||||||
|
var libraryBook = liveGridEntry.LibraryBook;
|
||||||
|
|
||||||
private GridEntry getGridEntry(int rowIndex) => dataGridView.GetBoundItem<GridEntry>(rowIndex);
|
// liberated: open explorer to file
|
||||||
|
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book))
|
||||||
|
{
|
||||||
|
var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
||||||
|
if (!Go.To.File(filePath))
|
||||||
|
MessageBox.Show($"File not found:\r\n{filePath}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private DataGridViewCell getCell(DataGridViewCellFormattingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex);
|
// else: liberate
|
||||||
|
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
|
||||||
|
}
|
||||||
|
|
||||||
private DataGridViewCell getCell(DataGridViewCellPaintingEventArgs e) => getCell(e.RowIndex, e.ColumnIndex);
|
private void EditTags_Click(GridEntry liveGridEntry)
|
||||||
|
{
|
||||||
|
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.Title, liveGridEntry.LibraryBook.Book.UserDefinedItem.Tags);
|
||||||
|
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
private DataGridViewCell getCell(int rowIndex, int columnIndex) => dataGridView.Rows[rowIndex].Cells[columnIndex];
|
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags);
|
||||||
}
|
if (qtyChanges == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Re-apply filters
|
||||||
|
Filter();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region UI display functions
|
||||||
|
|
||||||
|
private bool hasBeenDisplayed = false;
|
||||||
|
public void Display()
|
||||||
|
{
|
||||||
|
if (hasBeenDisplayed)
|
||||||
|
return;
|
||||||
|
hasBeenDisplayed = true;
|
||||||
|
|
||||||
|
//
|
||||||
|
// transform into sorted GridEntry.s BEFORE binding
|
||||||
|
//
|
||||||
|
using var context = DbContexts.GetContext();
|
||||||
|
var lib = context.GetLibrary_Flat_NoTracking();
|
||||||
|
|
||||||
|
// if no data. hide all columns. return
|
||||||
|
if (!lib.Any())
|
||||||
|
{
|
||||||
|
for (var i = _dataGridView.ColumnCount - 1; i >= 0; i--)
|
||||||
|
_dataGridView.Columns.RemoveAt(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedGridEntries = lib
|
||||||
|
.Select(lb => new GridEntry(lb)).ToList()
|
||||||
|
// default load order
|
||||||
|
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
|
||||||
|
//// more advanced example: sort by author, then series, then title
|
||||||
|
//.OrderBy(ge => ge.Authors)
|
||||||
|
// .ThenBy(ge => ge.Series)
|
||||||
|
// .ThenBy(ge => ge.Title)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// BIND
|
||||||
|
gridEntryBindingSource.DataSource = new SortableBindingList2<GridEntry>(orderedGridEntries);
|
||||||
|
|
||||||
|
// FILTER
|
||||||
|
Filter();
|
||||||
|
|
||||||
|
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshRow(string productId)
|
||||||
|
{
|
||||||
|
var rowIndex = getRowIndex((ge) => ge.AudibleProductId == productId);
|
||||||
|
|
||||||
|
// update cells incl Liberate button text
|
||||||
|
_dataGridView.InvalidateRow(rowIndex);
|
||||||
|
|
||||||
|
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
|
||||||
|
Filter();
|
||||||
|
|
||||||
|
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Filter
|
||||||
|
|
||||||
|
private string _filterSearchString;
|
||||||
|
private void Filter() => Filter(_filterSearchString);
|
||||||
|
public void Filter(string searchString)
|
||||||
|
{
|
||||||
|
_filterSearchString = searchString;
|
||||||
|
|
||||||
|
if (_dataGridView.Rows.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var searchResults = SearchEngineCommands.Search(searchString);
|
||||||
|
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/18942430
|
||||||
|
var bindingContext = BindingContext[_dataGridView.DataSource];
|
||||||
|
bindingContext.SuspendBinding();
|
||||||
|
{
|
||||||
|
for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
|
||||||
|
_dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Causes repainting of the DataGridView
|
||||||
|
bindingContext.ResumeBinding();
|
||||||
|
VisibleCountChanged?.Invoke(this, _dataGridView.AsEnumerable().Count(r => r.Visible));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DataGridView Macro
|
||||||
|
|
||||||
|
private int getRowIndex(Func<GridEntry, bool> func) => _dataGridView.GetRowIdOfBoundItem(func);
|
||||||
|
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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">
|
||||||
|
|||||||
75
LibationWinForms/SortableBindingList2[T].cs
Normal file
75
LibationWinForms/SortableBindingList2[T].cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
LibationWinForms/SynchronizeInvoker.cs
Normal file
122
LibationWinForms/SynchronizeInvoker.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user