diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs
index 54b351c5..259b3526 100644
--- a/Source/AaxDecrypter/NetworkFileStream.cs
+++ b/Source/AaxDecrypter/NetworkFileStream.cs
@@ -9,435 +9,435 @@ using System.Threading;
namespace AaxDecrypter
{
- ///
- /// A for a single Uri.
- ///
- public class SingleUriCookieContainer : CookieContainer
- {
- private Uri baseAddress;
- public Uri Uri
- {
- get => baseAddress;
- set
- {
- baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
- }
- }
-
- public CookieCollection GetCookies()
- {
- return GetCookies(Uri);
- }
- }
-
- ///
- /// A resumable, simultaneous file downloader and reader.
- ///
- public class NetworkFileStream : Stream, IUpdatable
- {
- public event EventHandler Updated;
-
- #region Public Properties
-
- ///
- /// Location to save the downloaded data.
- ///
- [JsonProperty(Required = Required.Always)]
- public string SaveFilePath { get; }
-
- ///
- /// Http(s) address of the file to download.
- ///
- [JsonProperty(Required = Required.Always)]
- public Uri Uri { get; private set; }
-
- ///
- /// All cookies set by caller or by the remote server.
- ///
- [JsonProperty(Required = Required.Always)]
- public SingleUriCookieContainer CookieContainer { get; }
-
- ///
- /// Http headers to be sent to the server with the request.
- ///
- [JsonProperty(Required = Required.Always)]
- public WebHeaderCollection RequestHeaders { get; private set; }
-
- ///
- /// The position in that has been written and flushed to disk.
- ///
- [JsonProperty(Required = Required.Always)]
- public long WritePosition { get; private set; }
-
- ///
- /// The total length of the file to download.
- ///
- [JsonProperty(Required = Required.Always)]
- public long ContentLength { get; private set; }
-
- #endregion
-
- #region Private Properties
- private HttpWebRequest HttpRequest { get; set; }
- private FileStream _writeFile { get; }
- private FileStream _readFile { get; }
- private Stream _networkStream { get; set; }
- private bool hasBegunDownloading { get; set; }
- public bool IsCancelled { get; private set; }
- private EventWaitHandle downloadEnded { get; set; }
- private EventWaitHandle downloadedPiece { get; set; }
-
- #endregion
-
- #region Constants
-
- //Download buffer size
- private const int DOWNLOAD_BUFF_SZ = 4 * 1024;
-
- //NetworkFileStream will flush all data in _writeFile to disk after every
- //DATA_FLUSH_SZ bytes are written to the file stream.
- private const int DATA_FLUSH_SZ = 1024 * 1024;
-
- #endregion
-
- #region Constructor
-
- ///
- /// A resumable, simultaneous file downloader and reader.
- ///
- /// Path to a location on disk to save the downloaded data from
- /// Http(s) address of the file to download.
- /// The position in to begin downloading.
- /// Http headers to be sent to the server with the .
- /// A with cookies to send with the . It will also be populated with any cookies set by the server.
- public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
- {
- ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
- ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
- ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
-
- if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
- throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
-
- SaveFilePath = saveFilePath;
- Uri = uri;
- WritePosition = writePosition;
- RequestHeaders = requestHeaders ?? new WebHeaderCollection();
- CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
-
- _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
- {
- Position = WritePosition
- };
-
- _readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-
- SetUriForSameFile(uri);
- }
-
- #endregion
-
- #region Downloader
-
- ///
- /// Update the .
- ///
- private void Update()
- {
- RequestHeaders = HttpRequest.Headers;
- Updated?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- /// Set a different to the same file targeted by this instance of
- ///
- /// New host must match existing host.
- public void SetUriForSameFile(Uri uriToSameFile)
- {
- ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
-
- if (uriToSameFile.Host != Uri.Host)
- throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
- if (hasBegunDownloading)
- throw new InvalidOperationException("Cannot change Uri after download has started.");
-
- Uri = uriToSameFile;
- HttpRequest = WebRequest.CreateHttp(Uri);
-
- HttpRequest.CookieContainer = CookieContainer;
- HttpRequest.Headers = RequestHeaders;
- //If NetworkFileStream is resuming, Header will already contain a range.
- HttpRequest.Headers.Remove("Range");
- HttpRequest.AddRange(WritePosition);
- }
-
- ///
- /// Begins downloading to in a background thread.
- ///
- private void BeginDownloading()
- {
- downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
-
- if (ContentLength != 0 && WritePosition == ContentLength)
- {
- hasBegunDownloading = true;
- downloadEnded.Set();
- return;
- }
-
- if (ContentLength != 0 && WritePosition > ContentLength)
- throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
-
- var response = HttpRequest.GetResponse() as HttpWebResponse;
-
- if (response.StatusCode != HttpStatusCode.PartialContent)
- throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
-
- //Content length is the length of the range request, and it is only equal
- //to the complete file length if requesting Range: bytes=0-
- if (WritePosition == 0)
- ContentLength = response.ContentLength;
-
- _networkStream = response.GetResponseStream();
- downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
-
- //Download the file in the background.
- new Thread(() => DownloadFile())
- { IsBackground = true }
- .Start();
-
- hasBegunDownloading = true;
- return;
- }
-
- ///
- /// Downlod to .
- ///
- private void DownloadFile()
- {
- var downloadPosition = WritePosition;
- var nextFlush = downloadPosition + DATA_FLUSH_SZ;
-
- var buff = new byte[DOWNLOAD_BUFF_SZ];
- do
- {
- var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
- _writeFile.Write(buff, 0, bytesRead);
-
- downloadPosition += bytesRead;
-
- if (downloadPosition > nextFlush)
- {
- _writeFile.Flush();
- WritePosition = downloadPosition;
- Update();
- nextFlush = downloadPosition + DATA_FLUSH_SZ;
- downloadedPiece.Set();
- }
-
- } while (downloadPosition < ContentLength && !IsCancelled);
-
- _writeFile.Close();
- _networkStream.Close();
- WritePosition = downloadPosition;
- Update();
-
- downloadedPiece.Set();
- downloadEnded.Set();
-
- if (!IsCancelled && WritePosition < ContentLength)
- throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
-
- if (WritePosition > ContentLength)
- throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
-
- }
-
- #endregion
-
- #region Json Connverters
-
- public static JsonSerializerSettings GetJsonSerializerSettings()
- {
- var settings = new JsonSerializerSettings();
- settings.Converters.Add(new CookieContainerConverter());
- settings.Converters.Add(new WebHeaderCollectionConverter());
- return settings;
- }
-
- internal class CookieContainerConverter : JsonConverter
- {
- public override bool CanConvert(Type objectType)
- => objectType == typeof(SingleUriCookieContainer);
-
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
- {
- var jObj = JObject.Load(reader);
-
- var result = new SingleUriCookieContainer()
- {
- Uri = new Uri(jObj["Uri"].Value()),
- Capacity = jObj["Capacity"].Value(),
- MaxCookieSize = jObj["MaxCookieSize"].Value(),
- PerDomainCapacity = jObj["PerDomainCapacity"].Value()
- };
-
- var cookieList = jObj["Cookies"].ToList();
-
- foreach (var cookie in cookieList)
- {
- result.Add(
- new Cookie
- {
- Comment = cookie["Comment"].Value(),
- HttpOnly = cookie["HttpOnly"].Value(),
- Discard = cookie["Discard"].Value(),
- Domain = cookie["Domain"].Value(),
- Expired = cookie["Expired"].Value(),
- Expires = cookie["Expires"].Value(),
- Name = cookie["Name"].Value(),
- Path = cookie["Path"].Value(),
- Port = cookie["Port"].Value(),
- Secure = cookie["Secure"].Value(),
- Value = cookie["Value"].Value(),
- Version = cookie["Version"].Value(),
- });
- }
-
- return result;
- }
-
- public override bool CanWrite => true;
-
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
- {
- var cookies = value as SingleUriCookieContainer;
- var obj = (JObject)JToken.FromObject(value);
- var container = cookies.GetCookies();
- var propertyNames = container.Select(c => JToken.FromObject(c));
- obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
- obj.WriteTo(writer);
- }
- }
-
- internal class WebHeaderCollectionConverter : JsonConverter
- {
- public override bool CanConvert(Type objectType)
- => objectType == typeof(WebHeaderCollection);
-
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
- {
- var jObj = JObject.Load(reader);
- var result = new WebHeaderCollection();
-
- foreach (var kvp in jObj)
- result.Add(kvp.Key, kvp.Value.Value());
-
- return result;
- }
-
- public override bool CanWrite => true;
-
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
- {
- var jObj = new JObject();
- var type = value.GetType();
- var headers = value as WebHeaderCollection;
- var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
- jObj.Add(jHeaders);
- jObj.WriteTo(writer);
- }
- }
-
- #endregion
-
- #region Download Stream Reader
-
- [JsonIgnore]
- public override bool CanRead => true;
-
- [JsonIgnore]
- public override bool CanSeek => true;
-
- [JsonIgnore]
- public override bool CanWrite => false;
-
- [JsonIgnore]
- public override long Length
- {
- get
- {
- if (!hasBegunDownloading)
- BeginDownloading();
- return ContentLength;
- }
- }
-
- [JsonIgnore]
- public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
-
- [JsonIgnore]
- public override bool CanTimeout => false;
-
- [JsonIgnore]
- public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
-
- [JsonIgnore]
- public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
-
- public override void Flush() => throw new NotImplementedException();
- public override void SetLength(long value) => throw new NotImplementedException();
- public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- if (!hasBegunDownloading)
- BeginDownloading();
-
- var toRead = Math.Min(count, Length - Position);
- WaitToPosition(Position + toRead);
- return _readFile.Read(buffer, offset, count);
- }
-
- public override long Seek(long offset, SeekOrigin origin)
- {
- var newPosition = origin switch
+ ///
+ /// A for a single Uri.
+ ///
+ public class SingleUriCookieContainer : CookieContainer
+ {
+ private Uri baseAddress;
+ public Uri Uri
+ {
+ get => baseAddress;
+ set
+ {
+ baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
+ }
+ }
+
+ public CookieCollection GetCookies()
+ {
+ return GetCookies(Uri);
+ }
+ }
+
+ ///
+ /// A resumable, simultaneous file downloader and reader.
+ ///
+ public class NetworkFileStream : Stream, IUpdatable
+ {
+ public event EventHandler Updated;
+
+ #region Public Properties
+
+ ///
+ /// Location to save the downloaded data.
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public string SaveFilePath { get; }
+
+ ///
+ /// Http(s) address of the file to download.
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public Uri Uri { get; private set; }
+
+ ///
+ /// All cookies set by caller or by the remote server.
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public SingleUriCookieContainer CookieContainer { get; }
+
+ ///
+ /// Http headers to be sent to the server with the request.
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public WebHeaderCollection RequestHeaders { get; private set; }
+
+ ///
+ /// The position in that has been written and flushed to disk.
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public long WritePosition { get; private set; }
+
+ ///
+ /// The total length of the file to download.
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public long ContentLength { get; private set; }
+
+ #endregion
+
+ #region Private Properties
+ private HttpWebRequest HttpRequest { get; set; }
+ private FileStream _writeFile { get; }
+ private FileStream _readFile { get; }
+ private Stream _networkStream { get; set; }
+ private bool hasBegunDownloading { get; set; }
+ public bool IsCancelled { get; private set; }
+ private EventWaitHandle downloadEnded { get; set; }
+ private EventWaitHandle downloadedPiece { get; set; }
+
+ #endregion
+
+ #region Constants
+
+ //Download buffer size
+ private const int DOWNLOAD_BUFF_SZ = 32 * 1024;
+
+ //NetworkFileStream will flush all data in _writeFile to disk after every
+ //DATA_FLUSH_SZ bytes are written to the file stream.
+ private const int DATA_FLUSH_SZ = 1024 * 1024;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// A resumable, simultaneous file downloader and reader.
+ ///
+ /// Path to a location on disk to save the downloaded data from
+ /// Http(s) address of the file to download.
+ /// The position in to begin downloading.
+ /// Http headers to be sent to the server with the .
+ /// A with cookies to send with the . It will also be populated with any cookies set by the server.
+ public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
+ {
+ ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
+ ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
+ ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
+
+ if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
+ throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
+
+ SaveFilePath = saveFilePath;
+ Uri = uri;
+ WritePosition = writePosition;
+ RequestHeaders = requestHeaders ?? new WebHeaderCollection();
+ CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
+
+ _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
+ {
+ Position = WritePosition
+ };
+
+ _readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ SetUriForSameFile(uri);
+ }
+
+ #endregion
+
+ #region Downloader
+
+ ///
+ /// Update the .
+ ///
+ private void Update()
+ {
+ RequestHeaders = HttpRequest.Headers;
+ Updated?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Set a different to the same file targeted by this instance of
+ ///
+ /// New host must match existing host.
+ public void SetUriForSameFile(Uri uriToSameFile)
+ {
+ ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
+
+ if (uriToSameFile.Host != Uri.Host)
+ throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
+ if (hasBegunDownloading)
+ throw new InvalidOperationException("Cannot change Uri after download has started.");
+
+ Uri = uriToSameFile;
+ HttpRequest = WebRequest.CreateHttp(Uri);
+
+ HttpRequest.CookieContainer = CookieContainer;
+ HttpRequest.Headers = RequestHeaders;
+ //If NetworkFileStream is resuming, Header will already contain a range.
+ HttpRequest.Headers.Remove("Range");
+ HttpRequest.AddRange(WritePosition);
+ }
+
+ ///
+ /// Begins downloading to in a background thread.
+ ///
+ private void BeginDownloading()
+ {
+ downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
+
+ if (ContentLength != 0 && WritePosition == ContentLength)
+ {
+ hasBegunDownloading = true;
+ downloadEnded.Set();
+ return;
+ }
+
+ if (ContentLength != 0 && WritePosition > ContentLength)
+ throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
+
+ var response = HttpRequest.GetResponse() as HttpWebResponse;
+
+ if (response.StatusCode != HttpStatusCode.PartialContent)
+ throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
+
+ //Content length is the length of the range request, and it is only equal
+ //to the complete file length if requesting Range: bytes=0-
+ if (WritePosition == 0)
+ ContentLength = response.ContentLength;
+
+ _networkStream = response.GetResponseStream();
+ downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
+
+ //Download the file in the background.
+ new Thread(() => DownloadFile())
+ { IsBackground = true }
+ .Start();
+
+ hasBegunDownloading = true;
+ return;
+ }
+
+ ///
+ /// Downlod to .
+ ///
+ private void DownloadFile()
+ {
+ var downloadPosition = WritePosition;
+ var nextFlush = downloadPosition + DATA_FLUSH_SZ;
+
+ var buff = new byte[DOWNLOAD_BUFF_SZ];
+ do
+ {
+ var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
+ _writeFile.Write(buff, 0, bytesRead);
+
+ downloadPosition += bytesRead;
+
+ if (downloadPosition > nextFlush)
+ {
+ _writeFile.Flush();
+ WritePosition = downloadPosition;
+ Update();
+ nextFlush = downloadPosition + DATA_FLUSH_SZ;
+ downloadedPiece.Set();
+ }
+
+ } while (downloadPosition < ContentLength && !IsCancelled);
+
+ _writeFile.Close();
+ _networkStream.Close();
+ WritePosition = downloadPosition;
+ Update();
+
+ downloadedPiece.Set();
+ downloadEnded.Set();
+
+ if (!IsCancelled && WritePosition < ContentLength)
+ throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
+
+ if (WritePosition > ContentLength)
+ throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
+
+ }
+
+ #endregion
+
+ #region Json Connverters
+
+ public static JsonSerializerSettings GetJsonSerializerSettings()
+ {
+ var settings = new JsonSerializerSettings();
+ settings.Converters.Add(new CookieContainerConverter());
+ settings.Converters.Add(new WebHeaderCollectionConverter());
+ return settings;
+ }
+
+ internal class CookieContainerConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ => objectType == typeof(SingleUriCookieContainer);
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var jObj = JObject.Load(reader);
+
+ var result = new SingleUriCookieContainer()
+ {
+ Uri = new Uri(jObj["Uri"].Value()),
+ Capacity = jObj["Capacity"].Value(),
+ MaxCookieSize = jObj["MaxCookieSize"].Value(),
+ PerDomainCapacity = jObj["PerDomainCapacity"].Value()
+ };
+
+ var cookieList = jObj["Cookies"].ToList();
+
+ foreach (var cookie in cookieList)
+ {
+ result.Add(
+ new Cookie
+ {
+ Comment = cookie["Comment"].Value(),
+ HttpOnly = cookie["HttpOnly"].Value(),
+ Discard = cookie["Discard"].Value(),
+ Domain = cookie["Domain"].Value(),
+ Expired = cookie["Expired"].Value(),
+ Expires = cookie["Expires"].Value(),
+ Name = cookie["Name"].Value(),
+ Path = cookie["Path"].Value(),
+ Port = cookie["Port"].Value(),
+ Secure = cookie["Secure"].Value(),
+ Value = cookie["Value"].Value(),
+ Version = cookie["Version"].Value(),
+ });
+ }
+
+ return result;
+ }
+
+ public override bool CanWrite => true;
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ var cookies = value as SingleUriCookieContainer;
+ var obj = (JObject)JToken.FromObject(value);
+ var container = cookies.GetCookies();
+ var propertyNames = container.Select(c => JToken.FromObject(c));
+ obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
+ obj.WriteTo(writer);
+ }
+ }
+
+ internal class WebHeaderCollectionConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ => objectType == typeof(WebHeaderCollection);
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var jObj = JObject.Load(reader);
+ var result = new WebHeaderCollection();
+
+ foreach (var kvp in jObj)
+ result.Add(kvp.Key, kvp.Value.Value());
+
+ return result;
+ }
+
+ public override bool CanWrite => true;
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ var jObj = new JObject();
+ var type = value.GetType();
+ var headers = value as WebHeaderCollection;
+ var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
+ jObj.Add(jHeaders);
+ jObj.WriteTo(writer);
+ }
+ }
+
+ #endregion
+
+ #region Download Stream Reader
+
+ [JsonIgnore]
+ public override bool CanRead => true;
+
+ [JsonIgnore]
+ public override bool CanSeek => true;
+
+ [JsonIgnore]
+ public override bool CanWrite => false;
+
+ [JsonIgnore]
+ public override long Length
+ {
+ get
+ {
+ if (!hasBegunDownloading)
+ BeginDownloading();
+ return ContentLength;
+ }
+ }
+
+ [JsonIgnore]
+ public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
+
+ [JsonIgnore]
+ public override bool CanTimeout => false;
+
+ [JsonIgnore]
+ public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
+
+ [JsonIgnore]
+ public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
+
+ public override void Flush() => throw new NotImplementedException();
+ public override void SetLength(long value) => throw new NotImplementedException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (!hasBegunDownloading)
+ BeginDownloading();
+
+ var toRead = Math.Min(count, Length - Position);
+ WaitToPosition(Position + toRead);
+ return _readFile.Read(buffer, offset, count);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ var newPosition = origin switch
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => ContentLength + offset,
_ => offset,
};
- WaitToPosition(newPosition);
- return _readFile.Position = newPosition;
- }
+ WaitToPosition(newPosition);
+ return _readFile.Position = newPosition;
+ }
- ///
- /// Blocks until the file has downloaded to at least , then returns.
- ///
- /// The minimum required flished data length in .
- private void WaitToPosition(long requiredPosition)
+ ///
+ /// Blocks until the file has downloaded to at least , then returns.
+ ///
+ /// The minimum required flished data length in .
+ private void WaitToPosition(long requiredPosition)
{
- while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
- }
+ while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
+ }
- public override void Close()
- {
- IsCancelled = true;
+ public override void Close()
+ {
+ IsCancelled = true;
- while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
+ while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
- _readFile.Close();
- _writeFile.Close();
- _networkStream?.Close();
- Update();
- }
+ _readFile.Close();
+ _writeFile.Close();
+ _networkStream?.Close();
+ Update();
+ }
- #endregion
- ~NetworkFileStream()
- {
- downloadEnded?.Close();
- downloadedPiece?.Close();
- }
- }
+ #endregion
+ ~NetworkFileStream()
+ {
+ downloadEnded?.Close();
+ downloadedPiece?.Close();
+ }
+ }
}
diff --git a/Source/FileLiberator/ConvertToMp3.cs b/Source/FileLiberator/ConvertToMp3.cs
index 35f3411e..09eb1b1e 100644
--- a/Source/FileLiberator/ConvertToMp3.cs
+++ b/Source/FileLiberator/ConvertToMp3.cs
@@ -17,17 +17,24 @@ namespace FileLiberator
public override string Name => "Convert to Mp3";
private Mp4File m4bBook;
- private long fileSize;
+ private long fileSize;
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
- public override void Cancel() => m4bBook?.Cancel();
+ private bool cancelled = false;
+ public override void Cancel()
+ {
+ m4bBook?.Cancel();
+ cancelled = true;
+ }
- public override bool Validate(LibraryBook libraryBook)
- {
+ public static bool ValidateMp3(LibraryBook libraryBook)
+ {
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
}
+ public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
+
public override async Task ProcessAsync(LibraryBook libraryBook)
{
OnBegin(libraryBook);
@@ -57,12 +64,12 @@ namespace FileLiberator
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
OnFileCreated(libraryBook, realMp3Path);
- var statusHandler = new StatusHandler();
-
if (result == ConversionResult.Failed)
- statusHandler.AddError("Conversion failed");
-
- return statusHandler;
+ return new StatusHandler { "Conversion failed" };
+ else if (result == ConversionResult.Cancelled)
+ return new StatusHandler { "Cancelled" };
+ else
+ return new StatusHandler();
}
finally
{
diff --git a/Source/LibationWinForms/Form1.Designer.cs b/Source/LibationWinForms/Form1.Designer.cs
index b54990b3..66f1cb4e 100644
--- a/Source/LibationWinForms/Form1.Designer.cs
+++ b/Source/LibationWinForms/Form1.Designer.cs
@@ -72,24 +72,25 @@
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addQuickFilterBtn = new System.Windows.Forms.Button();
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
- this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessBookQueue();
+ this.hideQueueBtn = new System.Windows.Forms.Button();
+ this.panel1 = new System.Windows.Forms.Panel();
+ this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
this.splitContainer1.Panel2.SuspendLayout();
this.splitContainer1.SuspendLayout();
+ this.panel1.SuspendLayout();
this.SuspendLayout();
//
// gridPanel
//
- this.gridPanel.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.gridPanel.Location = new System.Drawing.Point(15, 60);
+ this.gridPanel.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.gridPanel.Location = new System.Drawing.Point(0, 0);
this.gridPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.gridPanel.Name = "gridPanel";
- this.gridPanel.Size = new System.Drawing.Size(865, 556);
+ this.gridPanel.Size = new System.Drawing.Size(864, 560);
this.gridPanel.TabIndex = 5;
//
// filterHelpBtn
@@ -106,8 +107,8 @@
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
- this.filterBtn.Location = new System.Drawing.Point(792, 27);
- this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3);
+ this.filterBtn.Location = new System.Drawing.Point(750, 27);
+ this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27);
this.filterBtn.TabIndex = 2;
@@ -119,10 +120,10 @@
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
- this.filterSearchTb.Location = new System.Drawing.Point(220, 30);
+ this.filterSearchTb.Location = new System.Drawing.Point(194, 30);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb";
- this.filterSearchTb.Size = new System.Drawing.Size(564, 23);
+ this.filterSearchTb.Size = new System.Drawing.Size(548, 23);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
@@ -140,7 +141,7 @@
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
- this.menuStrip1.Size = new System.Drawing.Size(895, 24);
+ this.menuStrip1.Size = new System.Drawing.Size(894, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
@@ -396,7 +397,7 @@
this.statusStrip1.Location = new System.Drawing.Point(0, 619);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
- this.statusStrip1.Size = new System.Drawing.Size(895, 22);
+ this.statusStrip1.Size = new System.Drawing.Size(894, 22);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
@@ -409,7 +410,7 @@
// springLbl
//
this.springLbl.Name = "springLbl";
- this.springLbl.Size = new System.Drawing.Size(436, 17);
+ this.springLbl.Size = new System.Drawing.Size(435, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
@@ -429,7 +430,7 @@
this.addQuickFilterBtn.Location = new System.Drawing.Point(49, 27);
this.addQuickFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.addQuickFilterBtn.Name = "addQuickFilterBtn";
- this.addQuickFilterBtn.Size = new System.Drawing.Size(163, 27);
+ this.addQuickFilterBtn.Size = new System.Drawing.Size(137, 27);
this.addQuickFilterBtn.TabIndex = 4;
this.addQuickFilterBtn.Text = "Add To Quick Filters";
this.addQuickFilterBtn.UseVisualStyleBackColor = true;
@@ -443,8 +444,9 @@
//
// splitContainer1.Panel1
//
+ this.splitContainer1.Panel1.Controls.Add(this.hideQueueBtn);
+ this.splitContainer1.Panel1.Controls.Add(this.panel1);
this.splitContainer1.Panel1.Controls.Add(this.menuStrip1);
- this.splitContainer1.Panel1.Controls.Add(this.gridPanel);
this.splitContainer1.Panel1.Controls.Add(this.filterSearchTb);
this.splitContainer1.Panel1.Controls.Add(this.addQuickFilterBtn);
this.splitContainer1.Panel1.Controls.Add(this.filterBtn);
@@ -455,17 +457,42 @@
//
this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1);
this.splitContainer1.Size = new System.Drawing.Size(1231, 641);
- this.splitContainer1.SplitterDistance = 895;
+ this.splitContainer1.SplitterDistance = 894;
this.splitContainer1.SplitterWidth = 8;
this.splitContainer1.TabIndex = 7;
//
+ // hideQueueBtn
+ //
+ this.hideQueueBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
+ this.hideQueueBtn.Location = new System.Drawing.Point(846, 27);
+ this.hideQueueBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3);
+ this.hideQueueBtn.Name = "hideQueueBtn";
+ this.hideQueueBtn.Size = new System.Drawing.Size(33, 27);
+ this.hideQueueBtn.TabIndex = 8;
+ this.hideQueueBtn.Text = "❰❰❰";
+ this.hideQueueBtn.UseVisualStyleBackColor = true;
+ this.hideQueueBtn.Click += new System.EventHandler(this.HideQueueBtn_Click);
+ //
+ // panel1
+ //
+ this.panel1.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.panel1.Controls.Add(this.gridPanel);
+ this.panel1.Location = new System.Drawing.Point(15, 59);
+ this.panel1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
+ this.panel1.Name = "panel1";
+ this.panel1.Size = new System.Drawing.Size(864, 560);
+ this.panel1.TabIndex = 7;
+ //
// processBookQueue1
//
this.processBookQueue1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.processBookQueue1.Dock = System.Windows.Forms.DockStyle.Fill;
this.processBookQueue1.Location = new System.Drawing.Point(0, 0);
+ this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.processBookQueue1.Name = "processBookQueue1";
- this.processBookQueue1.Size = new System.Drawing.Size(328, 641);
+ this.processBookQueue1.Size = new System.Drawing.Size(329, 641);
this.processBookQueue1.TabIndex = 0;
//
// Form1
@@ -489,6 +516,7 @@
this.splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
this.splitContainer1.ResumeLayout(false);
+ this.panel1.ResumeLayout(false);
this.ResumeLayout(false);
}
@@ -538,6 +566,8 @@
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem liberateVisible2ToolStripMenuItem;
private System.Windows.Forms.SplitContainer splitContainer1;
- private LibationWinForms.ProcessQueue.ProcessBookQueue processBookQueue1;
+ private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
+ private System.Windows.Forms.Panel panel1;
+ private System.Windows.Forms.Button hideQueueBtn;
}
}
diff --git a/Source/LibationWinForms/Form1.Liberate.cs b/Source/LibationWinForms/Form1.Liberate.cs
index 43f91546..d1369d7a 100644
--- a/Source/LibationWinForms/Form1.Liberate.cs
+++ b/Source/LibationWinForms/Form1.Liberate.cs
@@ -1,4 +1,6 @@
using System;
+using System.Linq;
+using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms
@@ -7,11 +9,14 @@ namespace LibationWinForms
{
private void Configure_Liberate() { }
+ //GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
- => await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
+ => await Task.Run(() => processBookQueue1.AddDownloadDecrypt(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
+ .Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.NotLiberated)));
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
- => await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync();
+ => await Task.Run(() => processBookQueue1.AddDownloadPdf(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
+ .Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated)));
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
{
@@ -24,7 +29,9 @@ namespace LibationWinForms
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
- await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
+ await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
+ .Where(lb=>lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated)));
+ //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
}
}
}
diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs
index 2cdd7e72..acf4ac66 100644
--- a/Source/LibationWinForms/Form1.ProcessQueue.cs
+++ b/Source/LibationWinForms/Form1.ProcessQueue.cs
@@ -1,19 +1,41 @@
-using System;
-using System.Collections.Generic;
+using ApplicationServices;
+using LibationFileManager;
+using LibationWinForms.ProcessQueue;
+using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
-using LibationFileManager;
-using LibationWinForms.ProcessQueue;
namespace LibationWinForms
{
- public partial class Form1
- {
- private void Configure_ProcessQueue()
- {
- //splitContainer1.Panel2Collapsed = true;
- processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
+ public partial class Form1
+ {
+ private void Configure_ProcessQueue()
+ {
+ productsGrid.LiberateClicked += (_, lb) => processBookQueue1.AddDownloadDecrypt(lb);
+ processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
+ }
+
+ int WidthChange = 0;
+ private void HideQueueBtn_Click(object sender, EventArgs e)
+ {
+ if (splitContainer1.Panel2Collapsed)
+ {
+ WidthChange = WidthChange == 0 ? splitContainer1.Panel2.Width + splitContainer1.SplitterWidth : WidthChange;
+ Width += WidthChange;
+ splitContainer1.Panel2.Controls.Add(processBookQueue1);
+ splitContainer1.Panel2Collapsed = false;
+ processBookQueue1.popoutBtn.Visible = true;
+ hideQueueBtn.Text = "❰❰❰";
+ }
+ else
+ {
+ WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
+ splitContainer1.Panel2.Controls.Remove(processBookQueue1);
+ splitContainer1.Panel2Collapsed = true;
+ Width -= WidthChange;
+ hideQueueBtn.Text = "❱❱❱";
+ }
}
private void ProcessBookQueue1_PopOut(object sender, EventArgs e)
@@ -28,6 +50,10 @@ namespace LibationWinForms
dockForm.PassControl(processBookQueue1);
dockForm.Show();
this.Width -= dockForm.WidthChange;
+ hideQueueBtn.Visible = false;
+ int deltax = filterBtn.Margin.Right + hideQueueBtn.Width + hideQueueBtn.Margin.Left;
+ filterBtn.Location= new System.Drawing.Point(filterBtn.Location.X + deltax, filterBtn.Location.Y);
+ filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X + deltax, filterSearchTb.Location.Y);
}
private void DockForm_FormClosing(object sender, FormClosingEventArgs e)
@@ -40,6 +66,10 @@ namespace LibationWinForms
processBookQueue1.popoutBtn.Visible = true;
dockForm.SaveSizeAndLocation(Configuration.Instance);
this.Focus();
+ hideQueueBtn.Visible = true;
+ int deltax = filterBtn.Margin.Right + hideQueueBtn.Width + hideQueueBtn.Margin.Left;
+ filterBtn.Location = new System.Drawing.Point(filterBtn.Location.X - deltax, filterBtn.Location.Y);
+ filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X - deltax, filterSearchTb.Location.Y);
}
}
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
index df142e56..75cb21e9 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
@@ -5,7 +5,10 @@ using LibationFileManager;
using LibationWinForms.BookLiberation;
using System;
using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -16,153 +19,283 @@ namespace LibationWinForms.ProcessQueue
None,
Success,
Cancelled,
+ ValidationFail,
FailedRetry,
FailedSkip,
FailedAbort
}
- internal enum QueuePosition
+ public enum ProcessBookStatus
{
- Absent,
- Current,
- Fisrt,
- OneUp,
- OneDown,
- Last
+ Queued,
+ Cancelled,
+ Working,
+ Completed,
+ Failed
}
- internal delegate QueuePosition ProcessControlReorderHandler(ProcessBook sender, QueuePosition arg);
- internal delegate void ProcessControlEventArgs(ProcessBook sender, T arg);
- internal delegate void ProcessControlEventArgs(ProcessBook sender, EventArgs arg);
-
- internal class ProcessBook
+ ///
+ /// This is the viewmodel for queued processables
+ ///
+ public class ProcessBook : INotifyPropertyChanged
{
public event EventHandler Completed;
- public event ProcessControlEventArgs Cancelled;
- public event ProcessControlReorderHandler RequestMove;
- public GridEntry Entry { get; }
- public ILiberationBaseForm BookControl { get; }
+ public event PropertyChangedEventHandler PropertyChanged;
- private Func _makeFirstProc;
- private Processable _firstProcessable;
- private bool cancelled = false;
- private bool running = false;
- public Processable FirstProcessable => _firstProcessable ??= _makeFirstProc?.Invoke();
+ private ProcessBookResult _result = ProcessBookResult.None;
+ private ProcessBookStatus _status = ProcessBookStatus.Queued;
+ private string _bookText;
+ private int _progress;
+ private TimeSpan _timeRemaining;
+ private Image _cover;
+
+ public ProcessBookResult Result { get => _result; private set { _result = value; NotifyPropertyChanged(); } }
+ public ProcessBookStatus Status { get => _status; private set { _status = value; NotifyPropertyChanged(); } }
+ public string BookText { get => _bookText; private set { _bookText = value; NotifyPropertyChanged(); } }
+ public int Progress { get => _progress; private set { _progress = value; NotifyPropertyChanged(); } }
+ public TimeSpan TimeRemaining { get => _timeRemaining; private set { _timeRemaining = value; NotifyPropertyChanged(); } }
+ public Image Cover { get => _cover; private set { _cover = value; NotifyPropertyChanged(); } }
+
+ public LibraryBook LibraryBook { get; private set; }
+ private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
+ private Processable NextProcessable() => _currentProcessable = null;
+ private Processable _currentProcessable;
+ private Func GetCoverArtDelegate;
private readonly Queue> Processes = new();
+ private readonly LogMe Logger;
- LogMe Logger;
+ public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- public ProcessBook(GridEntry entry, LogMe logme)
+ public ProcessBook(LibraryBook libraryBook, LogMe logme)
{
- Entry = entry;
- BookControl = new ProcessBookControl(Entry.Title, Entry.Cover);
- BookControl.CancelAction = Cancel;
- BookControl.MoveUpAction = MoveUp;
- BookControl.MoveDownAction = MoveDown;
+ LibraryBook = libraryBook;
Logger = logme;
+
+ title = LibraryBook.Book.Title;
+ authorNames = LibraryBook.Book.AuthorNames();
+ narratorNames = LibraryBook.Book.NarratorNames();
+ _bookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
+
+ (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
+
+ if (isDefault)
+ PictureStorage.PictureCached += PictureStorage_PictureCached;
+ _cover = Dinah.Core.Drawing.ImageReader.ToImage(picture);
+
}
- public QueuePosition? MoveUp()
+ private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
{
- return RequestMove?.Invoke(this, QueuePosition.OneUp);
- }
- public QueuePosition? MoveDown()
- {
- return RequestMove?.Invoke(this, QueuePosition.OneDown);
- }
-
- public void Cancel()
- {
- cancelled = true;
- try
+ if (e.Definition.PictureId == LibraryBook.Book.PictureId)
{
- if (FirstProcessable is AudioDecodable audioDecodable)
- audioDecodable.Cancel();
+ Cover = Dinah.Core.Drawing.ImageReader.ToImage(e.Picture);
+ PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
- catch(Exception ex)
- {
- Logger.Error(ex, "Error while cancelling");
- }
-
- if (!running)
- Cancelled?.Invoke(this, EventArgs.Empty);
}
public async Task ProcessOneAsync()
{
- running = true;
- ProcessBookResult result = ProcessBookResult.None;
+ string procName = CurrentProcessable.Name;
try
{
- var firstProc = FirstProcessable;
-
- LinkProcessable(firstProc);
-
- var statusHandler = await firstProc.ProcessSingleAsync(Entry.LibraryBook, validate: true);
+ LinkProcessable(CurrentProcessable);
+ var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
if (statusHandler.IsSuccess)
- return result = ProcessBookResult.Success;
- else if (cancelled)
+ return Result = ProcessBookResult.Success;
+ else if (statusHandler.Errors.Contains("Cancelled"))
{
- Logger.Info($"Process was cancelled {Entry.LibraryBook.Book}");
- return result = ProcessBookResult.Cancelled;
+ Logger.Info($"{procName}: Process was cancelled {LibraryBook.Book}");
+ return Result = ProcessBookResult.Cancelled;
+ }
+ else if (statusHandler.Errors.Contains("Validation failed"))
+ {
+ Logger.Info($"{procName}: Validation failed {LibraryBook.Book}");
+ return Result = ProcessBookResult.ValidationFail;
}
foreach (var errorMessage in statusHandler.Errors)
- Logger.Error(errorMessage);
+ Logger.Error($"{procName}: {errorMessage}");
}
catch (Exception ex)
{
- Logger.Error(ex);
+ Logger.Error(ex, procName);
}
finally
{
- if (result == ProcessBookResult.None)
- result = showRetry(Entry.LibraryBook);
+ if (Result == ProcessBookResult.None)
+ Result = showRetry(LibraryBook);
- BookControl.SetResult(result);
+ Status = Result switch
+ {
+ ProcessBookResult.Success => ProcessBookStatus.Completed,
+ ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
+ ProcessBookResult.FailedRetry => ProcessBookStatus.Queued,
+ _ => ProcessBookStatus.Failed,
+ };
}
- return result;
+ return Result;
}
- public void AddPdfProcessable() => AddProcessable();
- public void AddDownloadDecryptProcessable() => AddProcessable();
- public void AddConvertMp3Processable() => AddProcessable();
+ public async Task Cancel()
+ {
+ try
+ {
+ if (CurrentProcessable is AudioDecodable audioDecodable)
+ {
+ //There's some threadding bug that causes this to hang if executed synchronously.
+ await Task.Run(audioDecodable.Cancel);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
+ }
+ }
+
+ public void AddDownloadPdf() => AddProcessable();
+ public void AddDownloadDecryptBook() => AddProcessable();
+ public void AddConvertToMp3() => AddProcessable();
private void AddProcessable() where T : Processable, new()
{
- if (FirstProcessable == null)
- {
- _makeFirstProc = () => new T();
- }
- else
- Processes.Enqueue(() => new T());
+ Processes.Enqueue(() => new T());
}
- private void LinkProcessable(Processable strProc)
+ public override string ToString() => LibraryBook.ToString();
+
+ #region Subscribers and Unsubscribers
+
+ private void LinkProcessable(Processable processable)
{
- strProc.Begin += Processable_Begin;
- strProc.Completed += Processable_Completed;
+ processable.Begin += Processable_Begin;
+ processable.Completed += Processable_Completed;
+ processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
+ processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
+
+ if (processable is AudioDecodable audioDecodable)
+ {
+ audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
+ audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
+ audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
+ audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
+ audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
+ }
}
+ private void UnlinkProcessable(Processable processable)
+ {
+ processable.Begin -= Processable_Begin;
+ processable.Completed -= Processable_Completed;
+ processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
+ processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
+
+ if (processable is AudioDecodable audioDecodable)
+ {
+ audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
+ audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
+ audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
+ audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
+ audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
+ }
+ }
+
+ #endregion
+
+ #region AudioDecodable event handlers
+
+ private string title;
+ private string authorNames;
+ private string narratorNames;
+ private void AudioDecodable_TitleDiscovered(object sender, string title)
+ {
+ this.title = title;
+ updateBookInfo();
+ }
+
+ private void AudioDecodable_AuthorsDiscovered(object sender, string authors)
+ {
+ authorNames = authors;
+ updateBookInfo();
+ }
+
+ private void AudioDecodable_NarratorsDiscovered(object sender, string narrators)
+ {
+ narratorNames = narrators;
+ updateBookInfo();
+ }
+
+ private void updateBookInfo()
+ {
+ BookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
+ }
+
+ public void AudioDecodable_RequestCoverArt(object sender, Action setCoverArtDelegate)
+ {
+ byte[] coverData = GetCoverArtDelegate();
+ setCoverArtDelegate(coverData);
+ AudioDecodable_CoverImageDiscovered(this, coverData);
+ }
+
+ private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
+ {
+ Cover = Dinah.Core.Drawing.ImageReader.ToImage(coverArt);
+ }
+
+ #endregion
+
+ #region Streamable event handlers
+ private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining)
+ {
+ TimeRemaining = timeRemaining;
+ }
+
+ private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
+ {
+ if (!downloadProgress.ProgressPercentage.HasValue)
+ return;
+
+ if (downloadProgress.ProgressPercentage == 0)
+ TimeRemaining = TimeSpan.Zero;
+ else
+ Progress = (int)downloadProgress.ProgressPercentage;
+ }
+
+ #endregion
+
+ #region Processable event handlers
+
private void Processable_Begin(object sender, LibraryBook libraryBook)
{
- BookControl.RegisterFileLiberator((Processable)sender, Logger);
- BookControl.Processable_Begin(sender, libraryBook);
+ Status = ProcessBookStatus.Working;
+
+ Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
+
+ GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously(
+ new PictureDefinition(
+ libraryBook.Book.PictureId,
+ PictureSize._500x500));
+
+ title = libraryBook.Book.Title;
+ authorNames = libraryBook.Book.AuthorNames();
+ narratorNames = libraryBook.Book.NarratorNames();
+ updateBookInfo();
}
- private async void Processable_Completed(object sender, LibraryBook e)
+ private async void Processable_Completed(object sender, LibraryBook libraryBook)
{
- ((Processable)sender).Begin -= Processable_Begin;
+
+ Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
+ UnlinkProcessable((Processable)sender);
if (Processes.Count > 0)
{
- var nextProcessFunc = Processes.Dequeue();
- var nextProcess = nextProcessFunc();
- LinkProcessable(nextProcess);
- var result = await nextProcess.ProcessSingleAsync(e, true);
+ NextProcessable();
+ LinkProcessable(CurrentProcessable);
+ var result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
if (result.HasErrors)
{
@@ -170,16 +303,18 @@ namespace LibationWinForms.ProcessQueue
Logger.Error(errorMessage);
Completed?.Invoke(this, EventArgs.Empty);
- running = false;
}
}
else
{
Completed?.Invoke(this, EventArgs.Empty);
- running = false;
}
}
+ #endregion
+
+ #region Failure Handler
+
private ProcessBookResult showRetry(LibraryBook libraryBook)
{
Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
@@ -232,7 +367,7 @@ $@" Title: {libraryBook.Book.Title}
}
- protected string SkipDialogText => @"
+ private string SkipDialogText => @"
An error occurred while trying to process this book.
{0}
@@ -242,8 +377,10 @@ An error occurred while trying to process this book.
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim();
- protected MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
- protected MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
- protected DialogResult SkipResult => DialogResult.Ignore;
+ private MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
+ private MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
+ private DialogResult SkipResult => DialogResult.Ignore;
}
+
+ #endregion
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.Designer.cs
index ee395dd3..8a3db3dd 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.Designer.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.Designer.cs
@@ -30,13 +30,16 @@
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ProcessBookControl));
this.pictureBox1 = new System.Windows.Forms.PictureBox();
- this.bookInfoLbl = new System.Windows.Forms.Label();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.remainingTimeLbl = new System.Windows.Forms.Label();
- this.label1 = new System.Windows.Forms.Label();
+ this.etaLbl = new System.Windows.Forms.Label();
this.cancelBtn = new System.Windows.Forms.Button();
+ this.statusLbl = new System.Windows.Forms.Label();
+ this.bookInfoLbl = new System.Windows.Forms.Label();
this.moveUpBtn = new System.Windows.Forms.Button();
this.moveDownBtn = new System.Windows.Forms.Button();
+ this.moveFirstBtn = new System.Windows.Forms.Button();
+ this.moveLastBtn = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout();
//
@@ -49,23 +52,12 @@
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
//
- // bookInfoLbl
- //
- this.bookInfoLbl.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.bookInfoLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
- this.bookInfoLbl.Location = new System.Drawing.Point(89, 3);
- this.bookInfoLbl.Name = "bookInfoLbl";
- this.bookInfoLbl.Size = new System.Drawing.Size(255, 56);
- this.bookInfoLbl.TabIndex = 1;
- this.bookInfoLbl.Text = "[multi-\r\nline\r\nbook\r\n info]";
- //
// progressBar1
//
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar1.Location = new System.Drawing.Point(88, 65);
+ this.progressBar1.MarqueeAnimationSpeed = 0;
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(212, 17);
this.progressBar1.TabIndex = 2;
@@ -81,17 +73,17 @@
this.remainingTimeLbl.Text = "--:--";
this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
- // label1
+ // etaLbl
//
- this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
- this.label1.AutoSize = true;
- this.label1.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
- this.label1.Location = new System.Drawing.Point(304, 66);
- this.label1.Name = "label1";
- this.label1.Size = new System.Drawing.Size(28, 13);
- this.label1.TabIndex = 3;
- this.label1.Text = "ETA:";
- this.label1.TextAlign = System.Drawing.ContentAlignment.TopRight;
+ this.etaLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+ this.etaLbl.AutoSize = true;
+ this.etaLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
+ this.etaLbl.Location = new System.Drawing.Point(304, 66);
+ this.etaLbl.Name = "etaLbl";
+ this.etaLbl.Size = new System.Drawing.Size(28, 13);
+ this.etaLbl.TabIndex = 3;
+ this.etaLbl.Text = "ETA:";
+ this.etaLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// cancelBtn
//
@@ -101,45 +93,96 @@
this.cancelBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.cancelBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.cancelBtn.ForeColor = System.Drawing.SystemColors.Control;
- this.cancelBtn.Location = new System.Drawing.Point(352, 3);
+ this.cancelBtn.Location = new System.Drawing.Point(348, 6);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(0);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(20, 20);
this.cancelBtn.TabIndex = 4;
this.cancelBtn.UseVisualStyleBackColor = false;
- this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
+ //
+ // statusLbl
+ //
+ this.statusLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+ this.statusLbl.AutoSize = true;
+ this.statusLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
+ this.statusLbl.Location = new System.Drawing.Point(89, 66);
+ this.statusLbl.Name = "statusLbl";
+ this.statusLbl.Size = new System.Drawing.Size(50, 13);
+ this.statusLbl.TabIndex = 3;
+ this.statusLbl.Text = "[STATUS]";
+ this.statusLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
+ //
+ // bookInfoLbl
+ //
+ this.bookInfoLbl.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.bookInfoLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
+ this.bookInfoLbl.Location = new System.Drawing.Point(89, 6);
+ this.bookInfoLbl.Name = "bookInfoLbl";
+ this.bookInfoLbl.Size = new System.Drawing.Size(219, 56);
+ this.bookInfoLbl.TabIndex = 1;
+ this.bookInfoLbl.Text = "[multi-\r\nline\r\nbook\r\n info]";
//
// moveUpBtn
//
- this.moveUpBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+ this.moveUpBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+ | System.Windows.Forms.AnchorStyles.Right)));
this.moveUpBtn.BackColor = System.Drawing.Color.Transparent;
this.moveUpBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveUpBtn.BackgroundImage")));
- this.moveUpBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch;
+ this.moveUpBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.moveUpBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.moveUpBtn.ForeColor = System.Drawing.SystemColors.Control;
- this.moveUpBtn.Location = new System.Drawing.Point(347, 39);
- this.moveUpBtn.Margin = new System.Windows.Forms.Padding(0);
+ this.moveUpBtn.Location = new System.Drawing.Point(314, 24);
this.moveUpBtn.Name = "moveUpBtn";
- this.moveUpBtn.Size = new System.Drawing.Size(25, 10);
- this.moveUpBtn.TabIndex = 4;
+ this.moveUpBtn.Size = new System.Drawing.Size(30, 17);
+ this.moveUpBtn.TabIndex = 5;
this.moveUpBtn.UseVisualStyleBackColor = false;
- this.moveUpBtn.Click += new System.EventHandler(this.moveUpBtn_Click);
//
// moveDownBtn
//
- this.moveDownBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+ this.moveDownBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+ | System.Windows.Forms.AnchorStyles.Right)));
this.moveDownBtn.BackColor = System.Drawing.Color.Transparent;
this.moveDownBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveDownBtn.BackgroundImage")));
- this.moveDownBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch;
+ this.moveDownBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.moveDownBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.moveDownBtn.ForeColor = System.Drawing.SystemColors.Control;
- this.moveDownBtn.Location = new System.Drawing.Point(347, 49);
- this.moveDownBtn.Margin = new System.Windows.Forms.Padding(0);
+ this.moveDownBtn.Location = new System.Drawing.Point(314, 40);
this.moveDownBtn.Name = "moveDownBtn";
- this.moveDownBtn.Size = new System.Drawing.Size(25, 10);
+ this.moveDownBtn.Size = new System.Drawing.Size(30, 17);
this.moveDownBtn.TabIndex = 5;
this.moveDownBtn.UseVisualStyleBackColor = false;
- this.moveDownBtn.Click += new System.EventHandler(this.moveDownBtn_Click);
+ //
+ // moveFirstBtn
+ //
+ this.moveFirstBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.moveFirstBtn.BackColor = System.Drawing.Color.Transparent;
+ this.moveFirstBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveFirstBtn.BackgroundImage")));
+ this.moveFirstBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
+ this.moveFirstBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
+ this.moveFirstBtn.ForeColor = System.Drawing.SystemColors.Control;
+ this.moveFirstBtn.Location = new System.Drawing.Point(314, 3);
+ this.moveFirstBtn.Name = "moveFirstBtn";
+ this.moveFirstBtn.Size = new System.Drawing.Size(30, 17);
+ this.moveFirstBtn.TabIndex = 5;
+ this.moveFirstBtn.UseVisualStyleBackColor = false;
+ //
+ // moveLastBtn
+ //
+ this.moveLastBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.moveLastBtn.BackColor = System.Drawing.Color.Transparent;
+ this.moveLastBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveLastBtn.BackgroundImage")));
+ this.moveLastBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
+ this.moveLastBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
+ this.moveLastBtn.ForeColor = System.Drawing.SystemColors.Control;
+ this.moveLastBtn.Location = new System.Drawing.Point(314, 63);
+ this.moveLastBtn.Name = "moveLastBtn";
+ this.moveLastBtn.Size = new System.Drawing.Size(30, 17);
+ this.moveLastBtn.TabIndex = 5;
+ this.moveLastBtn.UseVisualStyleBackColor = false;
//
// ProcessBookControl
//
@@ -147,15 +190,18 @@
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.SystemColors.ControlLight;
this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.Controls.Add(this.moveLastBtn);
this.Controls.Add(this.moveDownBtn);
+ this.Controls.Add(this.moveFirstBtn);
this.Controls.Add(this.moveUpBtn);
this.Controls.Add(this.cancelBtn);
- this.Controls.Add(this.label1);
+ this.Controls.Add(this.statusLbl);
+ this.Controls.Add(this.etaLbl);
this.Controls.Add(this.remainingTimeLbl);
this.Controls.Add(this.progressBar1);
this.Controls.Add(this.bookInfoLbl);
this.Controls.Add(this.pictureBox1);
- this.Margin = new System.Windows.Forms.Padding(2, 1, 2, 1);
+ this.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2);
this.Name = "ProcessBookControl";
this.Size = new System.Drawing.Size(375, 86);
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
@@ -167,12 +213,15 @@
#endregion
private System.Windows.Forms.PictureBox pictureBox1;
- private System.Windows.Forms.Label bookInfoLbl;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label remainingTimeLbl;
- private System.Windows.Forms.Label label1;
- private System.Windows.Forms.Button cancelBtn;
- private System.Windows.Forms.Button moveUpBtn;
- private System.Windows.Forms.Button moveDownBtn;
+ private System.Windows.Forms.Label etaLbl;
+ private System.Windows.Forms.Label statusLbl;
+ private System.Windows.Forms.Label bookInfoLbl;
+ public System.Windows.Forms.Button cancelBtn;
+ public System.Windows.Forms.Button moveUpBtn;
+ public System.Windows.Forms.Button moveDownBtn;
+ public System.Windows.Forms.Button moveFirstBtn;
+ public System.Windows.Forms.Button moveLastBtn;
}
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
index f0875715..7eee6913 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
@@ -1,302 +1,175 @@
using System;
using System.Drawing;
using System.Windows.Forms;
-using DataLayer;
-using Dinah.Core.Net.Http;
-using Dinah.Core.Threading;
-using FileLiberator;
-using LibationFileManager;
-using LibationWinForms.BookLiberation;
-using LibationWinForms.ProcessQueue;
namespace LibationWinForms.ProcessQueue
{
- internal interface ILiberationBaseForm
+ internal partial class ProcessBookControl : UserControl
{
- Action CancelAction { get; set; }
- Func MoveUpAction { get; set; }
- Func MoveDownAction { get; set; }
- void SetResult(ProcessBookResult status);
- void SetQueuePosition(QueuePosition status);
- void RegisterFileLiberator(Processable streamable, LogMe logMe);
- void Processable_Begin(object sender, LibraryBook libraryBook);
- int Width { get; set; }
- int Height { get; set; }
- Padding Margin { get; set; }
- }
+ private static int ControlNumberCounter = 0;
+
+ ///
+ /// The contol's position within
+ ///
+ public int ControlNumber { get; }
+ private ProcessBookStatus Status { get; set; } = ProcessBookStatus.Queued;
+ private readonly int CancelBtnDistanceFromEdge;
+ private readonly int ProgressBarDistanceFromEdge;
+
+ public static Color FailedColor = Color.LightCoral;
+ public static Color CancelledColor = Color.Khaki;
+ public static Color QueuedColor = SystemColors.Control;
+ public static Color SuccessColor = Color.PaleGreen;
- internal partial class ProcessBookControl : UserControl, ILiberationBaseForm
- {
- public Action CancelAction { get; set; }
- public Func MoveUpAction { get; set; }
- public Func MoveDownAction { get; set; }
- public string DecodeActionName { get; } = "Decoding";
- private Func GetCoverArtDelegate;
- protected Processable Processable { get; private set; }
- protected LogMe LogMe { get; private set; }
public ProcessBookControl()
{
InitializeComponent();
- label1.Text = "Queued";
+ statusLbl.Text = "Queued";
remainingTimeLbl.Visible = false;
progressBar1.Visible = false;
+ etaLbl.Visible = false;
+
+ CancelBtnDistanceFromEdge = Width - cancelBtn.Location.X;
+ ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
+ ControlNumber = ControlNumberCounter++;
}
- public void SetResult(ProcessBookResult status)
- {
- var statusTxt = status switch
- {
- ProcessBookResult.Success => "Finished",
- ProcessBookResult.Cancelled => "Cancelled",
- ProcessBookResult.FailedRetry => "Error, Retry",
- ProcessBookResult.FailedSkip => "Error, Skip",
- ProcessBookResult.FailedAbort => "Error, Abort",
- _ => throw new NotImplementedException(),
- };
-
- Color backColor = status switch
- {
- ProcessBookResult.Success => Color.PaleGreen,
- ProcessBookResult.Cancelled => Color.Khaki,
- ProcessBookResult.FailedRetry => Color.LightCoral,
- ProcessBookResult.FailedSkip => Color.LightCoral,
- ProcessBookResult.FailedAbort => Color.Firebrick,
- _ => throw new NotImplementedException(),
- };
-
- this.UIThreadAsync(() =>
- {
- cancelBtn.Visible = false;
- moveDownBtn.Visible = false;
- moveUpBtn.Visible = false;
- remainingTimeLbl.Visible = false;
- progressBar1.Visible = false;
- label1.Text = statusTxt;
- BackColor = backColor;
- });
- }
-
- public ProcessBookControl(string title, Image cover) : this()
+ public void SetCover(Image cover)
{
pictureBox1.Image = cover;
+ }
+
+ public void SetBookInfo(string title)
+ {
bookInfoLbl.Text = title;
}
- public void RegisterFileLiberator(Processable processable, LogMe logMe = null)
+ public void SetProgrss(int progress)
{
- if (processable is null) return;
-
- Processable = processable;
- LogMe = logMe;
-
- Subscribe((Streamable)processable);
- Subscribe(processable);
- if (processable is AudioDecodable audioDecodable)
- Subscribe(audioDecodable);
+ //Disable slow fill
+ //https://stackoverflow.com/a/5332770/3335599
+ if (progress < progressBar1.Maximum)
+ progressBar1.Value = progress + 1;
+ progressBar1.Value = progress;
}
-
- #region Event Subscribers and Unsubscribers
- private void Subscribe(Streamable streamable)
+ public void SetRemainingTime(TimeSpan remaining)
{
- UnsubscribeStreamable(this, EventArgs.Empty);
-
- streamable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
- streamable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
-
- Disposed += UnsubscribeStreamable;
- }
- private void Subscribe(Processable processable)
- {
- UnsubscribeProcessable(this, null);
-
- processable.Begin += Processable_Begin;
- processable.Completed += Processable_Completed;
-
- //Don't unsubscribe from Dispose because it fires when
- //Streamable.StreamingCompleted closes the form, and
- //the Processable events need to live past that event.
- processable.Completed += UnsubscribeProcessable;
- }
- private void Subscribe(AudioDecodable audioDecodable)
- {
- UnsubscribeAudioDecodable(this, EventArgs.Empty);
-
- audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
-
- Disposed += UnsubscribeAudioDecodable;
- }
- private void UnsubscribeStreamable(object sender, EventArgs e)
- {
- Disposed -= UnsubscribeStreamable;
-
- Processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
- Processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
- }
- private void UnsubscribeProcessable(object sender, LibraryBook e)
- {
- Processable.Completed -= UnsubscribeProcessable;
- Processable.Begin -= Processable_Begin;
- Processable.Completed -= Processable_Completed;
- }
- private void UnsubscribeAudioDecodable(object sender, EventArgs e)
- {
- if (Processable is not AudioDecodable audioDecodable)
- return;
-
- Disposed -= UnsubscribeAudioDecodable;
- audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
-
- audioDecodable.Cancel();
- }
- #endregion
-
- #region Streamable event handlers
- public void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress)
- {
- if (!downloadProgress.ProgressPercentage.HasValue)
- return;
-
- if (downloadProgress.ProgressPercentage == 0)
- updateRemainingTime(0);
- else
- progressBar1.UIThreadAsync(() => progressBar1.Value = (int)downloadProgress.ProgressPercentage);
+ remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
}
- public void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining)
+ public void SetResult(ProcessBookResult result)
{
- updateRemainingTime((int)timeRemaining.TotalSeconds);
- }
-
- private void updateRemainingTime(int remaining)
- => remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = formatTime(remaining));
-
- private string formatTime(int seconds)
- {
- var timeSpan = TimeSpan.FromSeconds(seconds);
- return $"{timeSpan:mm\\:ss}";
- }
-
- #endregion
-
- #region Processable event handlers
- public void Processable_Begin(object sender, LibraryBook libraryBook)
- {
- LogMe.Info($"{Environment.NewLine}{Processable.Name} Step, Begin: {libraryBook.Book}");
-
- this.UIThreadAsync(() =>
+ string statusText = default;
+ switch (result)
{
- label1.Text = "ETA:";
- remainingTimeLbl.Visible = true;
- progressBar1.Visible = true;
- });
-
- GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously(
- new PictureDefinition(
- libraryBook.Book.PictureId,
- PictureSize._500x500));
-
- //Set default values from library
- AudioDecodable_TitleDiscovered(sender, libraryBook.Book.Title);
- AudioDecodable_AuthorsDiscovered(sender, libraryBook.Book.AuthorNames());
- AudioDecodable_NarratorsDiscovered(sender, libraryBook.Book.NarratorNames());
- AudioDecodable_CoverImageDiscovered(sender,
- PictureStorage.GetPicture(
- new PictureDefinition(
- libraryBook.Book.PictureId,
- PictureSize._80x80)).bytes);
- }
-
- public void Processable_Completed(object sender, LibraryBook libraryBook)
- {
- LogMe.Info($"{Processable.Name} Step, Completed: {libraryBook.Book}");
- }
-
- #endregion
-
- #region AudioDecodable event handlers
-
- private string title;
- private string authorNames;
- private string narratorNames;
- public void AudioDecodable_TitleDiscovered(object sender, string title)
- {
- this.UIThreadAsync(() => this.Text = DecodeActionName + " " + title);
- this.title = title;
- updateBookInfo();
- }
-
- public void AudioDecodable_AuthorsDiscovered(object sender, string authors)
- {
- authorNames = authors;
- updateBookInfo();
- }
-
- public void AudioDecodable_NarratorsDiscovered(object sender, string narrators)
- {
- narratorNames = narrators;
- updateBookInfo();
- }
-
- private void updateBookInfo()
- => bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
-
- public void AudioDecodable_RequestCoverArt(object sender, Action setCoverArtDelegate)
- {
- setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
- }
-
- public void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
- {
- pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
- }
- #endregion
-
- private void cancelBtn_Click(object sender, EventArgs e)
- {
- CancelAction?.Invoke();
- }
-
- private void moveUpBtn_Click(object sender, EventArgs e)
- {
- HandleMovePositionResult(MoveUpAction?.Invoke());
- }
-
-
- private void moveDownBtn_Click(object sender, EventArgs e)
- {
- HandleMovePositionResult(MoveDownAction?.Invoke());
- }
-
- private void HandleMovePositionResult(QueuePosition? result)
- {
- if (result.HasValue)
- SetQueuePosition(result.Value);
- else
- SetQueuePosition(QueuePosition.Absent);
- }
-
- public void SetQueuePosition(QueuePosition status)
- {
- if (status is QueuePosition.Absent or QueuePosition.Current)
- {
- moveUpBtn.Visible = false;
- moveDownBtn.Visible = false;
+ case ProcessBookResult.Success:
+ statusText = "Finished";
+ Status = ProcessBookStatus.Completed;
+ break;
+ case ProcessBookResult.Cancelled:
+ statusText = "Cancelled";
+ Status = ProcessBookStatus.Cancelled;
+ break;
+ case ProcessBookResult.FailedRetry:
+ statusText = "Queued";
+ Status = ProcessBookStatus.Queued;
+ break;
+ case ProcessBookResult.FailedSkip:
+ statusText = "Error, Skippping";
+ Status = ProcessBookStatus.Failed;
+ break;
+ case ProcessBookResult.FailedAbort:
+ statusText = "Error, Abort";
+ Status = ProcessBookStatus.Failed;
+ break;
+ case ProcessBookResult.ValidationFail:
+ statusText = "Validion fail";
+ Status = ProcessBookStatus.Failed;
+ break;
+ case ProcessBookResult.None:
+ statusText = "UNKNOWN";
+ Status = ProcessBookStatus.Failed;
+ break;
}
- if (status == QueuePosition.Absent)
- cancelBtn.Enabled = false;
+ SetStatus(Status, statusText);
+ }
- moveUpBtn.Enabled = status != QueuePosition.Fisrt;
- moveDownBtn.Enabled = status != QueuePosition.Last;
+ public void SetStatus(ProcessBookStatus status, string statusText = null)
+ {
+ Color backColor = default;
+ switch (status)
+ {
+ case ProcessBookStatus.Completed:
+ backColor = SuccessColor;
+ Status = ProcessBookStatus.Completed;
+ break;
+ case ProcessBookStatus.Cancelled:
+ backColor = CancelledColor;
+ Status = ProcessBookStatus.Cancelled;
+ break;
+ case ProcessBookStatus.Queued:
+ backColor = QueuedColor;
+ Status = ProcessBookStatus.Queued;
+ break;
+ case ProcessBookStatus.Working:
+ backColor = QueuedColor;
+ Status = ProcessBookStatus.Working;
+ break;
+ case ProcessBookStatus.Failed:
+ backColor = FailedColor;
+ Status = ProcessBookStatus.Failed;
+ break;
+ }
+
+ SuspendLayout();
+
+ cancelBtn.Visible = Status is ProcessBookStatus.Queued or ProcessBookStatus.Working;
+ moveLastBtn.Visible = Status == ProcessBookStatus.Queued;
+ moveDownBtn.Visible = Status == ProcessBookStatus.Queued;
+ moveUpBtn.Visible = Status == ProcessBookStatus.Queued;
+ moveFirstBtn.Visible = Status == ProcessBookStatus.Queued;
+ remainingTimeLbl.Visible = Status == ProcessBookStatus.Working;
+ progressBar1.Visible = Status == ProcessBookStatus.Working;
+ etaLbl.Visible = Status == ProcessBookStatus.Working;
+ statusLbl.Visible = Status != ProcessBookStatus.Working;
+ statusLbl.Text = statusText ?? Status.ToString();
+ BackColor = backColor;
+
+ int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge;
+
+ if (Status is ProcessBookStatus.Queued or ProcessBookStatus.Working && deltaX != 0)
+ {
+ //If the last book to occupy this control before resizing was not
+ //queued, the buttons were not Visible so the Anchor property was
+ //ignored. Manually resize and reposition everyhting
+
+ cancelBtn.Location = new Point(cancelBtn.Location.X + deltaX, cancelBtn.Location.Y);
+ moveFirstBtn.Location = new Point(moveFirstBtn.Location.X + deltaX, moveFirstBtn.Location.Y);
+ moveUpBtn.Location = new Point(moveUpBtn.Location.X + deltaX, moveUpBtn.Location.Y);
+ moveDownBtn.Location = new Point(moveDownBtn.Location.X + deltaX, moveDownBtn.Location.Y);
+ moveLastBtn.Location = new Point(moveLastBtn.Location.X + deltaX, moveLastBtn.Location.Y);
+ etaLbl.Location = new Point(etaLbl.Location.X + deltaX, etaLbl.Location.Y);
+ remainingTimeLbl.Location = new Point(remainingTimeLbl.Location.X + deltaX, remainingTimeLbl.Location.Y);
+ progressBar1.Width = Width - ProgressBarDistanceFromEdge - progressBar1.Location.X;
+ }
+
+ if (status == ProcessBookStatus.Working)
+ {
+ bookInfoLbl.Width = cancelBtn.Location.X - bookInfoLbl.Location.X - bookInfoLbl.Padding.Left + cancelBtn.Padding.Right;
+ }
+ else
+ {
+ bookInfoLbl.Width = moveLastBtn.Location.X - bookInfoLbl.Location.X - bookInfoLbl.Padding.Left + moveLastBtn.Padding.Right;
+ }
+
+ ResumeLayout();
+ }
+
+ public override string ToString()
+ {
+ return bookInfoLbl.Text ?? "[NO TITLE]";
}
}
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.resx b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.resx
index 180633dc..83921ba7 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.resx
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.resx
@@ -60,16 +60,13 @@
True
-
- True
-
True
True
-
+
True
@@ -737,31 +734,40 @@
/x9W31o+WFcHNAAAAABJRU5ErkJggg==
+
+ True
+
+
+ True
+
True
- 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
+ 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
@@ -769,27 +775,91 @@
- iVBORw0KGgoAAAANSUhEUgAAAKoAAABXCAYAAACUet5FAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
- MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAAQ+SURBVHhe7dLLcRsxFERRrRyH
- Q3DICtEZ0NOy6aKoq+H0fB+AXpxNVwFvc99ut1tEeThGVINjRDU4RlSDY0Q1OEZUg6O83X78mvyeTEPE
- 4d6pwzsc76bHiTXOMBup4Pho+iSxxpFeRio4Pps+S6xxhEWRCo5k+jSxxp4WRyo4kunjxBp7sSIVHMn0
- eWKNPdiRCo5kOpBYY6tVkQqOZDryLLGGY3WkgiOZDpHEGktsilRwJNOx7yTWmPM+waYcOBIdm5FYg3xE
- KtSUA0dyPzgjscaj/5EKNeXAkTwenZFYQz5FKtSUA0fyfHhGYh3bl0iFmnLgSOj4jMQ6JoxUqCkHjoSO
- v5BYx/JtpEJNOXAkdHyBxDqG2UiFmnLgSOj4Qom1by8jFWrKgSOh44bE2qdFkQo15cCR0HFTYu3L4kiF
- mnLgSOj4Com1D1akQk05cCR0fKXE2jY7UqGmHDgSOr5BYm3TqkiFmnLgSOj4Rom1LasjFWrKgSOh4ztI
- rG3YFKlQUw4cCR3fSWKtbXOkQk05cCR0fEeJtaZdIhVqyoEjoeM7S6y17BapUFMOHAkdP0BirWHXSIWa
- cuBI6PhBEuu1do9UqCkHjoSOHyixXuOQSIWacuBI6PjBEuu5DotUqCkHjoSOnyCxnuPQSIWacuBI6PhJ
- EuuxDo9UqCkHjoSOnyixHuOUSIWacuBI6PjJEuu+TotUqCkHjoSOXyCx7uPUSIWacuBI6PhFEus2p0cq
- 1JQDR0LHL5RY17kkUqGmHDgSOn6xxOq5LFKhphw4EjpeQGJd5tJIhZpy4EjoeBGJdd7lkQo15cCR0PFC
- EisrEalQUw4cCR0vJrF+ViZSoaYcOBI6XlBi/atUpEJNOXAkdLyo0WMtF6lQUw4cCR0vbNRYS0Yq1JQD
- R0LHixst1rKRCjXlwJHQ8QaMEmvpSIWacuBI6Hgjeo+1fKRCTTlwJHS8Ib3G2kSkQk05cCR0vDG9xdpM
- pEJNOXAkdLxBvcTaVKRCTTlwJHS8Ua3H2lykQk05cCR0vGGtxtpkpEJNOXAkdLxxrcXabKRCTTlwJHS8
- A63E2nSkQk05cCR0vBPVY20+UqGmHDgSOt6RqrF2EalQUw4cCR3vTLVYu4lUqCkHjoSOd6hKrF1FKtSU
- A0dCxzt1dazdRSrUlANHQsc7dlWsXUYq1JQDR0LHO3d2rN1GKtSUA0dCxwdwVqxdRyrUlANHQscHcXSs
- 3Ucq1JQDR0LHB3JUrENEKtSUA0dCxwezd6zDRCrUlANHQscHtFesQ0Uq1JQDR0LHB7U11uEiFWrKgSOh
- 4wNbG+uQkQo15cCR0PHBubEOG6lQUw4cCR2PxbEOHalQUw4cCR2PD69iHT5SoaYcOBI6Hv99F2si/Yea
- cuBI6Hh88hxrIn1ATTlwJHQ8vrjHmkifUFMOHAkdD/QTtuFRUw4cI6rBMaKW29sfKR2pZX+g2KEAAAAA
- SUVORK5CYII=
+ iVBORw0KGgoAAAANSUhEUgAAAMgAAABNCAYAAADjJSv1AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
+ MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAATWSURBVHhe7Zlfh+1lGIaHiIiI
+ 6DQiog8QERHRUacRERHRN4hNRET0GSIiIvoAERERnUZEdBTRUUzP1dLe7559zTtr1vr9/90XF3Uf7D17
+ Zt3e5zYXl5eXMcZr9PDi4o3yr5L/iXHL/lI+bz1ADw88VX5Xtn9YjFvys/LRUnuAHt7jofJO+U/Z/sEx
+ rtk/y9fLu1gP0MMHeaHkKWr/khjX6Lcl19F9WA/QQ+ex8vOy/ctiXItcQe+XXEUPYD1AD/tkwMe1+d8Q
+ L6/FeoAe3gxP1Pdl+0XEuETvDvEe1gP08Dh4qj4oM+DjEuXKuW+I97AeoIe348Xy17L94mKcU3498cAQ
+ 72E9QA9vDwP+i7L9ImOcWq4Zfi2hQ7yH9QA9PJ0M+DiXDHF+HXES1gP08Dwy4OPU8usHrpiTsR6gh+eT
+ AR+nkGuFq+VsrAfo4XBkwMex5Eq51RDvYT1AD4clAz4OKVcJ18mth3gP6wF6OA5vlhnw8Ry5RrhKBsd6
+ gB6Ox9NlBnw8xbOHeA/rAXo4Lg+XPJHtPz7G6xxsiPewHqCH08BT+VvZfjNibB10iPewHqCH08GT+WXZ
+ flNiHGWI97AeoIfTkwEf/3e0Id7DeoAezkMGfOTXAaMN8R7WA/RwPjLg9ynXA1fEbFgP0MP5yYDfj1wN
+ XA+zYj1AD5dBBvz25Vrgapgd6wF6uCzeKjPgtyXXweRDvIf1AD1cHs+UP5TtNzmuU66CWYZ4D+sBerhM
+ eIo/LNtvdlyPXAFcA4vEeoAeLpuXygz4dcnrzxWwWKwH6OHyebzMgF+HixniPawH6OF6yIBfrosb4j2s
+ B+jhusiAX56LHOI9rAfo4frIgF+Gix7iPawH6OF6yYCfz8UP8R7WA/Rw3WTATy+v9+KHeA/rAXq4Dd4u
+ /y7bH2QcVl5rXu3VYz1AD7cDT/6PZftDjcPIK81rvQmsB+jhtuDp/6hsf7jxdHmVeZ03hfUAPdwmL5cZ
+ 8OfJa7zaId7DeoAebpcM+NPlFV71EO9hPUAPt08G/PHy6vL6bhrrAXq4DzLgb/ar8oly81gP0MP9kAHv
+ bnKI97AeoIf7IwP+npsd4j2sB+jhPuGU4KRoPyx7c9NDvIf1AD3cN++UexvwuxjiPawH6GHgxPipbD9E
+ W3U3Q7yH9QA9DMCp8XHZfpi2JK8kr2UorAfoYWh5pdzagOd13N0Q72E9QA/DVbY04HkVdznEe1gP0MNw
+ HWse8L+XvIZBsB6gh6HHGgf81+Xuh3gP6wF6GG5iLQOe1+7dMtyA9QA9DMfCycLp0n4olyKv3LNlOALr
+ AXoYbgOnCydM++Gc20/KR8pwJNYD9DCcAqfM3AM+Q/xErAfoYTgVTpq5BnyG+BlYD9DDcA6cNlMO+Azx
+ AbAeoIdhCKYY8BniA2E9QA/DUIw54DPEB8R6gB6GoRlywP9RZogPjPUAPQxjMMSA/6bMEB8B6wF6GMaC
+ k4jTqP3QHyOvz3tlGAnrAXoYxoYTiVOpLcF1/lw+V4YRsR6gh2EKOJU4mdoyXPXTMkN8AqwH6GGYEk6n
+ qwOe1+XVMkyE9QA9DFPDCcUpxTefV+XJMkyI9QA9DHPAKfXa4T/D1FgPUMMY40ENY4wHNYwxHtQwxnhQ
+ wxjjQQ1jjAc1jDEe1DDGiJcX/wKcO4zm90rrbQAAAABJRU5ErkJggg==
+
+
+
+ True
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAAMgAAABZCAYAAAB7Ymt4AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
+ MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAAT8SURBVHhe7dlvp+ZVFMbxIWKI
+ iJ4OEdELiIiIIeZpRERERO8ghoiI6DUMERHRC4iIiOgFRERPI3oUp3WNWWPfa52z53fv+/dv//b34kNn
+ KR3n3Je9lnPr6urqWpZ7Rv8AjOTeSQ/KL0r6F4v/CBgFBQEqKAhQQUGACgoCVFAQoIKCABUUBKigIEAF
+ BQEqKAhQQUGACgoCVFAQoIKCABUUBKigIEAFBQEqKAhQQUGACgoCVFAQoIKCABWTC0KWz23zpSl/QdF3
+ 5nlDVspJD8ovSmTxvGx+M2UZbvKXedOQFXLSg/KLElk0H5l/TVmCKb4wTxuyUFIP4sCRRaJV6XtTfujP
+ pVfnJUMWSOpBHDgye7QiaVUqP+yt9Pp8aMjMST2IA0dmi1YirUblB3wuHPAzJ/UgDhyZJVqFph7irf40
+ dw2ZIakHceDIxdEK1HKIt/rccMBfmNSDOHCkOVp5tPqUH961/Go44C9I6kEcONIUrTpaecoP7dr0an1g
+ SENSD+LAkbOy5CHeigO+IakHceDI5KxxiLfigD8zqQdx4MikrH2It+KAn5jUgzhwpJotD/FWHPATknoQ
+ B47cmD0c4q044J+Q1IM4cCRFK4pWlfID16tvzXOGhKQexIEjJ9FqohWl/JD1Tq/gG4YUST2IA0ceRytJ
+ D4d4q88MB/yjpB7EgSMPVxCtIuWH6ah+MRzwltSDOHCDR6tHr4d4Kw54S+pBHLhBo1VDK0f5wRnN0Ad8
+ 6kEcuAGjFUOrRvlhGdWwB3zqQRy4wfK++ceUHxIMeMCnHsSBGyQjHeKthjrgUw/iwA2QEQ/xVnpd9coe
+ PqkHceAOHA7xdnptnzWHTepBHLiDhkP8cnp1XzeHTOpBHLgDhkN8Xp+awx3wqQdx4A4UrQQc4sv42Rzq
+ gE89iAN3kGgV4BBf1qEO+NSDOHCdR0+/VoDyF4llHeKATz2IA9dxXjR6+stfHtbR/QGfehAHrtO8ZzjE
+ t9ftAZ96EAeus+hp/8aUvyRsS6+4XvOuknoQB66j6En/w5S/HOyDXnO96t0k9SAOXAd5yugp/8+UvxTs
+ j173Lg741IM4cDsPh3h/9Mrv/oBPPYgDt+NwiPdLr/2uD/jUgzhwOwyH+HHs9oBPPYgDt7NwiB/PLg/4
+ 1IM4cDsJh/jx7eqATz2IA7eDvGA4xMewmwM+9SAO3MZ513CIj8UPeG0NmyX1IA7cRtFT+7Upf3AYi7YG
+ bQ+bJPUgDtwGec38bsofFsak7UFbxOpJPYgDt2L0pH5iOMQRaZtY9YBPPYgDt1L0lP5kyh8KUNJWsdoB
+ n3oQB26FvGM4xDHFagd86kEcuAXzjPnKlD8AYIrFD/jUgzhwC+VVwyGOSyx6wKcexIGbORzimNsiB3zq
+ QRy4GcMhjqVoG9GfB2ZL6kEcuJnCIY6laSvRdjLLAZ96EAfuwnCIY23aUi4+4FMP4sBdEA5xbEXbiraW
+ 5qQexIFriJ64+4ZDHFvT9tJ0wKcexIE7M3rafjTlNwlsqemATz2IA3dG3jZ/m/KbA/bg7AM+9SAO3ITo
+ EH9gym8I2KPJB3zqQRy4J+QVwyGOnkw64FMP4sDdED1VHxsOcfRKB7y2n2uTehAH7prcMT+Y8n8G9Ejb
+ j/4ckZJ6EAcu5C3DIY4j0RakP0ucHPCpB3HgHoVDHEenP088PuBTD+LAWTjEMQptRw8P+NMeXN36HzdL
+ jfkqyMbMAAAAAElFTkSuQmCC
+
+
+
+ True
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAAMgAAABZCAYAAAB7Ymt4AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
+ MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAAUASURBVHhe7Zntpq1lFIYXERER
+ +wAiIjqAiIiIfQYRERHRGURERMQ+hoiIiA4gIiKivxER/YroV6zGncbezxxjrrHmmvP9eJ73vS6uH+uW
+ vVdrz9szbuvq+vr6wP95w/zLVIC4ZX8xX4w9cHPwiGfM78z2D0Pckp+ZT5qpB24ODnnM/MD8x2z/YMSR
+ /dN83XxI7IGbg+O8ZOopav8SxBH91tR1dEDsgZuDm3nK/Nxs/zLEUdQV9L6pqygRe+Dm4HYY8Dia/w1x
+ 80ZiD9wcnIaeqO/N9ptA7NGHQ7wi9sDNwenoqfrQZMBjj+rKORjiFbEHbg7uzsvmr2b7zSGuqX49kYZ4
+ ReyBm4Pz0ID/wmy/ScSl1TWjX0scHeIVsQduDi6DAY9rqSGuX0ecReyBm4PLYcDj0urXD7pizib2wM3B
+ NDDgcQl1rehquZjYAzcH08KAx7nUlXKnIV4Re+DmYHoY8Dilukp0ndx5iFfEHrg5mI83TQY8XqKuEV0l
+ kxN74OZgXp41GfB4jhcP8YrYAzcH8/O4qSey/Z9HvMnJhnhF7IGbg+XQU/mb2f4wEFsnHeIVsQduDpZF
+ T+aXZvtDQZxliFfEHrg5WAcGPLqzDfGK2AM3B+vBgEf9OmC2IV4Re+DmYF0Y8PtU14OuiNWIPXBz0AcM
+ +P2oq0HXw6rEHrg56AcG/PbVtaCrYXViD9wc9MdbJgN+W+o6WHyIV8QeuDnok+fMH8z2h4xjqqtglSFe
+ EXvg5qBf9BR/ZLY/bBxHXQG6Brok9sDNQf+8YjLgx1Kvv66Abok9cHMwBk+bDPgx7GaIV8QeuDkYCwZ8
+ v3Y3xCtiD9wcjAcDvj+7HOIVsQduDsaEAd+HXQ/xitgDNwdjw4Bfz+6HeEXsgZuD8WHAL69e7+6HeEXs
+ gZuD7fC2+bfZ/kPitOq11qs9PLEHbg62hZ78H832HxWnUa+0XutNEHvg5mB76On/2Gz/cfF89Srrdd4U
+ sQduDrbLqyYD/jL1Gg87xCtiD9wcbBsG/PnqFR56iFfEHrg52AcM+NPVq6vXd9PEHrg52A8M+Nv9yrxn
+ bp7YAzcH+4IBf9xNDvGK2AM3B/uEAf/IzQ7xitgDNwf7RaeETor2w7I3Nz3EK2IP3BzAO+beBvwuhnhF
+ 7IGbAxA6MX4y2w/RVt3NEK+IPXBzAI5OjU/M9sO0JfVK6rUEI/bAzQFEXjO3NuD1Ou5uiFfEHrg5gGNs
+ acDrVdzlEK+IPXBzABUjD/jfTb2GcITYAzcHcBsjDvivzd0P8YrYAzcHcAqjDHi9du+acAuxB24O4C7o
+ ZNHp0n4oe1Gv3PMmnEDsgZsDuCs6XXTCtB/Otf3UfMKEE4k9cHMA56JTZu0BzxA/k9gDNwdwCTpp1hrw
+ DPELiD1wcwCXotNmyQHPEJ+A2AM3BzAVSwx4hvhExB64OYApmXPAM8QnJPbAzQHMwZQD/g+TIT4xsQdu
+ DmAuphjw35gM8RmIPXBzAHOik0inUfuhP0W9Pu+ZMBOxB+7hF7AUOpF0KrUluMmfzRdMmJG2B62HX8CS
+ 6FTSydSWIfrAZIgvQNuD1sMvrq7u679F3Jn32x60Hn5BQXCfUhDEQgqCWEhBEAspCGIhBUEspCCIhRQE
+ sZCCIBZSEMRCCoJYSEEQCykIYiEFQSykIIiFFASxkIIgFlIQxEIKglhIQRALKQhiIQVBLKQgiIU3FOT6
+ 6l8KOJAbKVKmPQAAAABJRU5ErkJggg==
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookForm.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookForm.Designer.cs
index 036022fb..548f06d2 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookForm.Designer.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookForm.Designer.cs
@@ -37,7 +37,7 @@
this.ClientSize = new System.Drawing.Size(522, 638);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
this.Name = "ProcessBookForm";
- this.Text = "ProcessBookForm";
+ this.Text = "Book Processing Queue";
this.ResumeLayout(false);
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookForm.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookForm.cs
index ebdf2784..b1444c84 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookForm.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookForm.cs
@@ -1,12 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Drawing;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows.Forms;
+using System.Windows.Forms;
namespace LibationWinForms.ProcessQueue
{
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.Designer.cs
deleted file mode 100644
index 62848fbe..00000000
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.Designer.cs
+++ /dev/null
@@ -1,235 +0,0 @@
-namespace LibationWinForms.ProcessQueue
-{
- partial class ProcessBookQueue
- {
- ///
- /// Required designer variable.
- ///
- private System.ComponentModel.IContainer components = null;
-
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
- #region Component Designer generated code
-
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
- private void InitializeComponent()
- {
- this.statusStrip1 = new System.Windows.Forms.StatusStrip();
- this.toolStripProgressBar1 = new System.Windows.Forms.ToolStripProgressBar();
- this.tabControl1 = new System.Windows.Forms.TabControl();
- this.tabPage1 = new System.Windows.Forms.TabPage();
- this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
- this.panel1 = new System.Windows.Forms.Panel();
- this.btnCleanFinished = new System.Windows.Forms.Button();
- this.cancelAllBtn = new System.Windows.Forms.Button();
- this.tabPage2 = new System.Windows.Forms.TabPage();
- this.panel2 = new System.Windows.Forms.Panel();
- this.clearLogBtn = new System.Windows.Forms.Button();
- this.logMeTbox = new System.Windows.Forms.TextBox();
- this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
- this.statusStrip1.SuspendLayout();
- this.tabControl1.SuspendLayout();
- this.tabPage1.SuspendLayout();
- this.panel1.SuspendLayout();
- this.tabPage2.SuspendLayout();
- this.panel2.SuspendLayout();
- this.SuspendLayout();
- //
- // statusStrip1
- //
- this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.toolStripProgressBar1,
- this.toolStripStatusLabel1});
- this.statusStrip1.Location = new System.Drawing.Point(0, 486);
- this.statusStrip1.Name = "statusStrip1";
- this.statusStrip1.Size = new System.Drawing.Size(359, 22);
- this.statusStrip1.TabIndex = 1;
- this.statusStrip1.Text = "statusStrip1";
- //
- // toolStripProgressBar1
- //
- this.toolStripProgressBar1.Name = "toolStripProgressBar1";
- this.toolStripProgressBar1.Size = new System.Drawing.Size(100, 16);
- //
- // tabControl1
- //
- this.tabControl1.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.tabControl1.Controls.Add(this.tabPage1);
- this.tabControl1.Controls.Add(this.tabPage2);
- this.tabControl1.Location = new System.Drawing.Point(0, 0);
- this.tabControl1.Margin = new System.Windows.Forms.Padding(0);
- this.tabControl1.Name = "tabControl1";
- this.tabControl1.SelectedIndex = 0;
- this.tabControl1.Size = new System.Drawing.Size(360, 486);
- this.tabControl1.TabIndex = 3;
- //
- // tabPage1
- //
- this.tabPage1.Controls.Add(this.flowLayoutPanel1);
- this.tabPage1.Controls.Add(this.panel1);
- this.tabPage1.Location = new System.Drawing.Point(4, 24);
- this.tabPage1.Name = "tabPage1";
- this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
- this.tabPage1.Size = new System.Drawing.Size(352, 458);
- this.tabPage1.TabIndex = 0;
- this.tabPage1.Text = "Process Queue";
- this.tabPage1.UseVisualStyleBackColor = true;
- //
- // flowLayoutPanel1
- //
- this.flowLayoutPanel1.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.flowLayoutPanel1.AutoScroll = true;
- this.flowLayoutPanel1.BackColor = System.Drawing.SystemColors.ControlDarkDark;
- this.flowLayoutPanel1.Location = new System.Drawing.Point(3, 3);
- this.flowLayoutPanel1.Name = "flowLayoutPanel1";
- this.flowLayoutPanel1.Size = new System.Drawing.Size(346, 419);
- this.flowLayoutPanel1.TabIndex = 0;
- this.flowLayoutPanel1.ClientSizeChanged += new System.EventHandler(this.flowLayoutPanel1_ClientSizeChanged);
- this.flowLayoutPanel1.Layout += new System.Windows.Forms.LayoutEventHandler(this.flowLayoutPanel1_Layout);
- //
- // panel1
- //
- this.panel1.BackColor = System.Drawing.SystemColors.ControlDark;
- this.panel1.Controls.Add(this.btnCleanFinished);
- this.panel1.Controls.Add(this.cancelAllBtn);
- this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
- this.panel1.Location = new System.Drawing.Point(3, 425);
- this.panel1.Name = "panel1";
- this.panel1.Size = new System.Drawing.Size(346, 30);
- this.panel1.TabIndex = 2;
- //
- // btnCleanFinished
- //
- this.btnCleanFinished.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.btnCleanFinished.Location = new System.Drawing.Point(253, 3);
- this.btnCleanFinished.Name = "btnCleanFinished";
- this.btnCleanFinished.Size = new System.Drawing.Size(90, 23);
- this.btnCleanFinished.TabIndex = 3;
- this.btnCleanFinished.Text = "Clear Finished";
- this.btnCleanFinished.UseVisualStyleBackColor = true;
- this.btnCleanFinished.Click += new System.EventHandler(this.btnCleanFinished_Click);
- //
- // cancelAllBtn
- //
- this.cancelAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Left)));
- this.cancelAllBtn.Location = new System.Drawing.Point(3, 3);
- this.cancelAllBtn.Name = "cancelAllBtn";
- this.cancelAllBtn.Size = new System.Drawing.Size(75, 23);
- this.cancelAllBtn.TabIndex = 2;
- this.cancelAllBtn.Text = "Cancel All";
- this.cancelAllBtn.UseVisualStyleBackColor = true;
- this.cancelAllBtn.Click += new System.EventHandler(this.cancelAllBtn_Click);
- //
- // tabPage2
- //
- this.tabPage2.Controls.Add(this.panel2);
- this.tabPage2.Controls.Add(this.logMeTbox);
- this.tabPage2.Location = new System.Drawing.Point(4, 24);
- this.tabPage2.Name = "tabPage2";
- this.tabPage2.Padding = new System.Windows.Forms.Padding(3);
- this.tabPage2.Size = new System.Drawing.Size(352, 458);
- this.tabPage2.TabIndex = 1;
- this.tabPage2.Text = "Log";
- this.tabPage2.UseVisualStyleBackColor = true;
- //
- // panel2
- //
- this.panel2.BackColor = System.Drawing.SystemColors.ControlDark;
- this.panel2.Controls.Add(this.clearLogBtn);
- this.panel2.Dock = System.Windows.Forms.DockStyle.Bottom;
- this.panel2.Location = new System.Drawing.Point(3, 425);
- this.panel2.Name = "panel2";
- this.panel2.Size = new System.Drawing.Size(346, 30);
- this.panel2.TabIndex = 1;
- //
- // clearLogBtn
- //
- this.clearLogBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Left)));
- this.clearLogBtn.Location = new System.Drawing.Point(3, 3);
- this.clearLogBtn.Name = "clearLogBtn";
- this.clearLogBtn.Size = new System.Drawing.Size(75, 23);
- this.clearLogBtn.TabIndex = 0;
- this.clearLogBtn.Text = "Clear Log";
- this.clearLogBtn.UseVisualStyleBackColor = true;
- this.clearLogBtn.Click += new System.EventHandler(this.clearLogBtn_Click);
- //
- // logMeTbox
- //
- this.logMeTbox.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.logMeTbox.Location = new System.Drawing.Point(3, 3);
- this.logMeTbox.Margin = new System.Windows.Forms.Padding(3, 3, 3, 0);
- this.logMeTbox.MaxLength = 10000000;
- this.logMeTbox.Multiline = true;
- this.logMeTbox.Name = "logMeTbox";
- this.logMeTbox.ReadOnly = true;
- this.logMeTbox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
- this.logMeTbox.Size = new System.Drawing.Size(346, 419);
- this.logMeTbox.TabIndex = 0;
- //
- // toolStripStatusLabel1
- //
- this.toolStripStatusLabel1.Name = "toolStripStatusLabel1";
- this.toolStripStatusLabel1.Size = new System.Drawing.Size(211, 17);
- this.toolStripStatusLabel1.Spring = true;
- //
- // ProcessBookQueue
- //
- this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
- this.Controls.Add(this.tabControl1);
- this.Controls.Add(this.statusStrip1);
- this.Name = "ProcessBookQueue";
- this.Size = new System.Drawing.Size(359, 508);
- this.statusStrip1.ResumeLayout(false);
- this.statusStrip1.PerformLayout();
- this.tabControl1.ResumeLayout(false);
- this.tabPage1.ResumeLayout(false);
- this.panel1.ResumeLayout(false);
- this.tabPage2.ResumeLayout(false);
- this.tabPage2.PerformLayout();
- this.panel2.ResumeLayout(false);
- this.ResumeLayout(false);
- this.PerformLayout();
-
- }
-
- #endregion
- private System.Windows.Forms.StatusStrip statusStrip1;
- private System.Windows.Forms.ToolStripProgressBar toolStripProgressBar1;
- private System.Windows.Forms.TabControl tabControl1;
- private System.Windows.Forms.TabPage tabPage1;
- private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1;
- private System.Windows.Forms.Panel panel1;
- private System.Windows.Forms.TabPage tabPage2;
- private System.Windows.Forms.TextBox logMeTbox;
- private System.Windows.Forms.Button btnCleanFinished;
- private System.Windows.Forms.Button cancelAllBtn;
- private System.Windows.Forms.Panel panel2;
- private System.Windows.Forms.Button clearLogBtn;
- private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1;
- }
-}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.cs
deleted file mode 100644
index af748764..00000000
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.cs
+++ /dev/null
@@ -1,343 +0,0 @@
-using DataLayer;
-using Dinah.Core.Threading;
-using LibationWinForms.BookLiberation;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-using System.Windows.Forms;
-
-namespace LibationWinForms.ProcessQueue
-{
- internal partial class ProcessBookQueue : UserControl, ILogForm
- {
- private ProcessBook CurrentBook;
- private readonly LinkedList BookQueue = new();
- private readonly List CompletedBooks = new();
- private readonly LogMe Logger;
- private readonly object lockObject = new();
-
-
- public Task QueueRunner { get; private set; }
- public bool Running => !QueueRunner?.IsCompleted ?? false;
-
- public ToolStripButton popoutBtn = new();
-
- public ProcessBookQueue()
- {
- InitializeComponent();
- Logger = LogMe.RegisterForm(this);
-
-
- this.popoutBtn.DisplayStyle = ToolStripItemDisplayStyle.Text;
- this.popoutBtn.Name = "popoutBtn";
- this.popoutBtn.Text = "Pop Out";
- this.popoutBtn.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
- this.popoutBtn.Alignment = ToolStripItemAlignment.Right;
- this.popoutBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
-
- statusStrip1.Items.Add(popoutBtn);
- }
- public async Task AddDownloadDecrypt(IEnumerable entries)
- {
- foreach (var entry in entries)
- await AddDownloadDecryptAsync(entry);
- }
-
- public async Task AddDownloadDecryptAsync(GridEntry gridEntry)
- {
- if (BookExists(gridEntry.LibraryBook))
- return;
-
- ProcessBook pbook = new ProcessBook(gridEntry, Logger);
- pbook.Completed += Pbook_Completed;
- pbook.Cancelled += Pbook_Cancelled;
- pbook.RequestMove += (o,d) => RequestMove(o, d);
-
- var libStatus = gridEntry.Liberate;
-
- if (libStatus.BookStatus != LiberatedStatus.Liberated)
- pbook.AddDownloadDecryptProcessable();
-
- if (libStatus.PdfStatus != LiberatedStatus.Liberated)
- pbook.AddPdfProcessable();
-
- EnqueueBook(pbook);
-
- await AddBookControlAsync(pbook.BookControl);
-
- if (!Running)
- {
- QueueRunner = QueueLoop();
- }
- }
-
- private async void Pbook_Cancelled(ProcessBook sender, EventArgs e)
- {
- lock (lockObject)
- {
- if (BookQueue.Contains(sender))
- BookQueue.Remove(sender);
- }
- await RemoveBookControlAsync(sender.BookControl);
- }
-
- ///
- /// Handles requests by to change its order in the queue
- ///
- /// The requesting
- /// The requested position
- /// The resultant position
- private QueuePosition RequestMove(ProcessBook sender, QueuePosition direction)
- {
- var node = BookQueue.Find(sender);
-
- if (node == null || direction == QueuePosition.Absent)
- return QueuePosition.Absent;
- if (CurrentBook != null && CurrentBook == sender)
- return QueuePosition.Current;
- if ((direction == QueuePosition.Fisrt || direction == QueuePosition.OneUp) && BookQueue.First.Value == sender)
- return QueuePosition.Fisrt;
- if ((direction == QueuePosition.Last || direction == QueuePosition.OneDown) && BookQueue.Last.Value == sender)
- return QueuePosition.Last;
-
- if (direction == QueuePosition.OneUp)
- {
- var oneUp = node.Previous;
- BookQueue.Remove(node);
- BookQueue.AddBefore(oneUp, node.Value);
- }
- else if (direction == QueuePosition.OneDown)
- {
- var oneDown = node.Next;
- BookQueue.Remove(node);
- BookQueue.AddAfter(oneDown, node.Value);
- }
- else if (direction == QueuePosition.Fisrt)
- {
- BookQueue.Remove(node);
- BookQueue.AddFirst(node);
- }
- else
- {
- BookQueue.Remove(node);
- BookQueue.AddLast(node);
- }
-
- var index = flowLayoutPanel1.Controls.IndexOf((Control)sender.BookControl);
-
- index = direction switch
- {
- QueuePosition.Fisrt => 0,
- QueuePosition.OneUp => index - 1,
- QueuePosition.OneDown => index + 1,
- QueuePosition.Last => flowLayoutPanel1.Controls.Count - 1,
- _ => throw new NotImplementedException(),
- };
-
- flowLayoutPanel1.Controls.SetChildIndex((Control)sender.BookControl, index);
-
- if (index == 0) return QueuePosition.Fisrt;
- if (index == flowLayoutPanel1.Controls.Count - 1) return QueuePosition.Last;
- return direction;
- }
-
- private async Task QueueLoop()
- {
- while (MoreInQueue())
- {
- var nextBook = NextBook();
- nextBook.BookControl.SetQueuePosition(QueuePosition.Current);
- PeekBook()?.BookControl.SetQueuePosition(QueuePosition.Fisrt);
-
- var result = await nextBook.ProcessOneAsync();
-
- AddCompletedBook(nextBook);
-
- switch (result)
- {
- case ProcessBookResult.FailedRetry:
- EnqueueBook(nextBook);
- break;
- case ProcessBookResult.FailedAbort:
- return;
- }
- }
- }
-
- private bool BookExists(LibraryBook libraryBook)
- {
- lock (lockObject)
- {
- return CurrentBook?.Entry?.AudibleProductId == libraryBook.Book.AudibleProductId ||
- CompletedBooks.Union(BookQueue).Any(p => p.Entry.AudibleProductId == libraryBook.Book.AudibleProductId);
- }
- }
-
- private ProcessBook NextBook()
- {
- lock (lockObject)
- {
- CurrentBook = BookQueue.First.Value;
- BookQueue.RemoveFirst();
- return CurrentBook;
- }
- }
- private ProcessBook PeekBook()
- {
- lock (lockObject)
- return BookQueue.Count > 0 ? BookQueue.First.Value : default;
- }
-
- private void EnqueueBook(ProcessBook pbook)
- {
- lock (lockObject)
- BookQueue.AddLast(pbook);
- }
-
- private void AddCompletedBook(ProcessBook pbook)
- {
- lock (lockObject)
- CompletedBooks.Add(pbook);
- }
-
- private bool MoreInQueue()
- {
- lock (lockObject)
- return BookQueue.Count > 0;
- }
-
-
- private void Pbook_Completed(object sender, EventArgs e)
- {
- if (CurrentBook == sender)
- CurrentBook = default;
- }
-
- private async void cancelAllBtn_Click(object sender, EventArgs e)
- {
- List l1 = new();
- lock (lockObject)
- {
- l1.AddRange(BookQueue);
- BookQueue.Clear();
- }
- CurrentBook?.Cancel();
- CurrentBook = default;
-
- await RemoveBookControlsAsync(l1.Select(l => l.BookControl));
- }
-
- private async void btnCleanFinished_Click(object sender, EventArgs e)
- {
- List l1 = new();
- lock (lockObject)
- {
- l1.AddRange(CompletedBooks);
- CompletedBooks.Clear();
- }
-
- await RemoveBookControlsAsync(l1.Select(l => l.BookControl));
- }
-
- private async Task AddBookControlAsync(ILiberationBaseForm control)
- {
- await Task.Run(() => Invoke(() =>
- {
- SetBookControlWidth((Control)control);
- flowLayoutPanel1.Controls.Add((Control)control);
- flowLayoutPanel1.SetFlowBreak((Control)control, true);
- Refresh();
- }));
- }
-
- private async Task RemoveBookControlAsync(ILiberationBaseForm control)
- {
- await Task.Run(() => Invoke(() =>
- {
- flowLayoutPanel1.Controls.Remove((Control)control);
- }));
- }
-
- private async Task RemoveBookControlsAsync(IEnumerable control)
- {
- await Task.Run(() => Invoke(() =>
- {
- SuspendLayout();
- foreach (var l in control)
- flowLayoutPanel1.Controls.Remove((Control)l);
- ResumeLayout();
- }));
- }
-
- public void WriteLine(string text)
- {
- if (!IsDisposed)
- logMeTbox.UIThreadAsync(() => logMeTbox.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
- }
-
- private void clearLogBtn_Click(object sender, EventArgs e)
- {
- logMeTbox.Clear();
- }
-
- [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
- private static extern long GetWindowLongPtr(IntPtr hWnd, int nIndex);
-
- [DllImport("user32.dll")]
- private static extern bool ShowScrollBar(IntPtr hWnd, SBOrientation bar, bool show);
-
- public const int WS_VSCROLL = 0x200000;
- public const int WS_HSCROLL = 0x100000;
- enum SBOrientation : int
- {
- SB_HORZ = 0,
- SB_VERT = 1,
- SB_CTL = 2,
- SB_BOTH = 3
- }
-
- private void flowLayoutPanel1_ClientSizeChanged(object sender, EventArgs e)
- {
- ReorderControls();
- }
-
- private void flowLayoutPanel1_Layout(object sender, LayoutEventArgs e)
- {
- ReorderControls();
- }
-
- bool V_SHOWN = false;
-
- private void ReorderControls()
- {
- bool hShown = (GetWindowLongPtr(flowLayoutPanel1.Handle, -16) & WS_HSCROLL) != 0;
- bool vShown = (GetWindowLongPtr(flowLayoutPanel1.Handle, -16) & WS_VSCROLL) != 0;
-
- if (hShown)
- ShowScrollBar(flowLayoutPanel1.Handle, SBOrientation.SB_HORZ, false);
-
- if (vShown != V_SHOWN)
- {
- flowLayoutPanel1.SuspendLayout();
-
- foreach (Control c in flowLayoutPanel1.Controls)
- SetBookControlWidth(c);
-
- flowLayoutPanel1.ResumeLayout();
- V_SHOWN = vShown;
- }
- }
-
- private void SetBookControlWidth(Control book)
- {
- book.Width = flowLayoutPanel1.ClientRectangle.Width - book.Margin.Left - book.Margin.Right;
- }
-
- private void toolStripSplitButton1_ButtonClick(object sender, EventArgs e)
- {
-
- }
- }
-}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
new file mode 100644
index 00000000..3e99394f
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
@@ -0,0 +1,341 @@
+namespace LibationWinForms.ProcessQueue
+{
+ partial class ProcessQueueControl
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.components = new System.ComponentModel.Container();
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ProcessQueueControl));
+ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
+ this.statusStrip1 = new System.Windows.Forms.StatusStrip();
+ this.toolStripProgressBar1 = new System.Windows.Forms.ToolStripProgressBar();
+ this.queueNumberLbl = new System.Windows.Forms.ToolStripStatusLabel();
+ this.completedNumberLbl = new System.Windows.Forms.ToolStripStatusLabel();
+ this.errorNumberLbl = new System.Windows.Forms.ToolStripStatusLabel();
+ this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
+ this.runningTimeLbl = new System.Windows.Forms.ToolStripStatusLabel();
+ this.tabControl1 = new System.Windows.Forms.TabControl();
+ this.tabPage1 = new System.Windows.Forms.TabPage();
+ this.panel3 = new System.Windows.Forms.Panel();
+ this.virtualFlowControl2 = new LibationWinForms.ProcessQueue.VirtualFlowControl();
+ this.panel1 = new System.Windows.Forms.Panel();
+ this.btnCleanFinished = new System.Windows.Forms.Button();
+ this.cancelAllBtn = new System.Windows.Forms.Button();
+ this.tabPage2 = new System.Windows.Forms.TabPage();
+ this.logDGV = new System.Windows.Forms.DataGridView();
+ this.timestampColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.logEntryColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.panel4 = new System.Windows.Forms.Panel();
+ this.panel2 = new System.Windows.Forms.Panel();
+ this.clearLogBtn = new System.Windows.Forms.Button();
+ this.counterTimer = new System.Windows.Forms.Timer(this.components);
+ this.logCopyBtn = new System.Windows.Forms.Button();
+ this.statusStrip1.SuspendLayout();
+ this.tabControl1.SuspendLayout();
+ this.tabPage1.SuspendLayout();
+ this.panel1.SuspendLayout();
+ this.tabPage2.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.logDGV)).BeginInit();
+ this.panel2.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // statusStrip1
+ //
+ this.statusStrip1.ImageScalingSize = new System.Drawing.Size(20, 20);
+ this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.toolStripProgressBar1,
+ this.queueNumberLbl,
+ this.completedNumberLbl,
+ this.errorNumberLbl,
+ this.toolStripStatusLabel1,
+ this.runningTimeLbl});
+ this.statusStrip1.Location = new System.Drawing.Point(0, 483);
+ this.statusStrip1.Name = "statusStrip1";
+ this.statusStrip1.Size = new System.Drawing.Size(404, 25);
+ this.statusStrip1.TabIndex = 1;
+ this.statusStrip1.Text = "baseStatusStrip";
+ //
+ // toolStripProgressBar1
+ //
+ this.toolStripProgressBar1.Name = "toolStripProgressBar1";
+ this.toolStripProgressBar1.Size = new System.Drawing.Size(100, 19);
+ //
+ // queueNumberLbl
+ //
+ this.queueNumberLbl.Image = ((System.Drawing.Image)(resources.GetObject("queueNumberLbl.Image")));
+ this.queueNumberLbl.Name = "queueNumberLbl";
+ this.queueNumberLbl.Size = new System.Drawing.Size(51, 20);
+ this.queueNumberLbl.Text = "[Q#]";
+ //
+ // completedNumberLbl
+ //
+ this.completedNumberLbl.Image = ((System.Drawing.Image)(resources.GetObject("completedNumberLbl.Image")));
+ this.completedNumberLbl.Name = "completedNumberLbl";
+ this.completedNumberLbl.Size = new System.Drawing.Size(56, 20);
+ this.completedNumberLbl.Text = "[DL#]";
+ //
+ // errorNumberLbl
+ //
+ this.errorNumberLbl.Image = ((System.Drawing.Image)(resources.GetObject("errorNumberLbl.Image")));
+ this.errorNumberLbl.Name = "errorNumberLbl";
+ this.errorNumberLbl.Size = new System.Drawing.Size(62, 20);
+ this.errorNumberLbl.Text = "[ERR#]";
+ //
+ // toolStripStatusLabel1
+ //
+ this.toolStripStatusLabel1.Name = "toolStripStatusLabel1";
+ this.toolStripStatusLabel1.Size = new System.Drawing.Size(77, 20);
+ this.toolStripStatusLabel1.Spring = true;
+ //
+ // runningTimeLbl
+ //
+ this.runningTimeLbl.AutoSize = false;
+ this.runningTimeLbl.Name = "runningTimeLbl";
+ this.runningTimeLbl.Size = new System.Drawing.Size(41, 20);
+ this.runningTimeLbl.Text = "[TIME]";
+ //
+ // tabControl1
+ //
+ this.tabControl1.Controls.Add(this.tabPage1);
+ this.tabControl1.Controls.Add(this.tabPage2);
+ this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.tabControl1.Location = new System.Drawing.Point(0, 0);
+ this.tabControl1.Margin = new System.Windows.Forms.Padding(0);
+ this.tabControl1.Name = "tabControl1";
+ this.tabControl1.SelectedIndex = 0;
+ this.tabControl1.Size = new System.Drawing.Size(404, 483);
+ this.tabControl1.TabIndex = 3;
+ //
+ // tabPage1
+ //
+ this.tabPage1.Controls.Add(this.panel3);
+ this.tabPage1.Controls.Add(this.virtualFlowControl2);
+ this.tabPage1.Controls.Add(this.panel1);
+ this.tabPage1.Location = new System.Drawing.Point(4, 24);
+ this.tabPage1.Name = "tabPage1";
+ this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
+ this.tabPage1.Size = new System.Drawing.Size(396, 455);
+ this.tabPage1.TabIndex = 0;
+ this.tabPage1.Text = "Process Queue";
+ this.tabPage1.UseVisualStyleBackColor = true;
+ //
+ // panel3
+ //
+ this.panel3.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.panel3.Location = new System.Drawing.Point(3, 422);
+ this.panel3.Name = "panel3";
+ this.panel3.Size = new System.Drawing.Size(390, 5);
+ this.panel3.TabIndex = 4;
+ //
+ // virtualFlowControl2
+ //
+ this.virtualFlowControl2.AccessibleRole = System.Windows.Forms.AccessibleRole.None;
+ this.virtualFlowControl2.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.virtualFlowControl2.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.virtualFlowControl2.Location = new System.Drawing.Point(3, 3);
+ this.virtualFlowControl2.Name = "virtualFlowControl2";
+ this.virtualFlowControl2.Size = new System.Drawing.Size(390, 424);
+ this.virtualFlowControl2.TabIndex = 3;
+ this.virtualFlowControl2.VirtualControlCount = 0;
+ //
+ // panel1
+ //
+ this.panel1.BackColor = System.Drawing.SystemColors.Control;
+ this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.panel1.Controls.Add(this.btnCleanFinished);
+ this.panel1.Controls.Add(this.cancelAllBtn);
+ this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.panel1.Location = new System.Drawing.Point(3, 427);
+ this.panel1.Name = "panel1";
+ this.panel1.Size = new System.Drawing.Size(390, 25);
+ this.panel1.TabIndex = 2;
+ //
+ // btnCleanFinished
+ //
+ this.btnCleanFinished.Dock = System.Windows.Forms.DockStyle.Right;
+ this.btnCleanFinished.Location = new System.Drawing.Point(298, 0);
+ this.btnCleanFinished.Name = "btnCleanFinished";
+ this.btnCleanFinished.Size = new System.Drawing.Size(90, 23);
+ this.btnCleanFinished.TabIndex = 3;
+ this.btnCleanFinished.Text = "Clear Finished";
+ this.btnCleanFinished.UseVisualStyleBackColor = true;
+ this.btnCleanFinished.Click += new System.EventHandler(this.btnClearFinished_Click);
+ //
+ // cancelAllBtn
+ //
+ this.cancelAllBtn.Dock = System.Windows.Forms.DockStyle.Left;
+ this.cancelAllBtn.Location = new System.Drawing.Point(0, 0);
+ this.cancelAllBtn.Name = "cancelAllBtn";
+ this.cancelAllBtn.Size = new System.Drawing.Size(75, 23);
+ this.cancelAllBtn.TabIndex = 2;
+ this.cancelAllBtn.Text = "Cancel All";
+ this.cancelAllBtn.UseVisualStyleBackColor = true;
+ this.cancelAllBtn.Click += new System.EventHandler(this.cancelAllBtn_Click);
+ //
+ // tabPage2
+ //
+ this.tabPage2.Controls.Add(this.logDGV);
+ this.tabPage2.Controls.Add(this.panel4);
+ this.tabPage2.Controls.Add(this.panel2);
+ this.tabPage2.Location = new System.Drawing.Point(4, 24);
+ this.tabPage2.Name = "tabPage2";
+ this.tabPage2.Padding = new System.Windows.Forms.Padding(3);
+ this.tabPage2.Size = new System.Drawing.Size(396, 455);
+ this.tabPage2.TabIndex = 1;
+ this.tabPage2.Text = "Log";
+ this.tabPage2.UseVisualStyleBackColor = true;
+ //
+ // logDGV
+ //
+ this.logDGV.AllowUserToAddRows = false;
+ this.logDGV.AllowUserToDeleteRows = false;
+ this.logDGV.AllowUserToOrderColumns = true;
+ this.logDGV.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells;
+ this.logDGV.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
+ this.logDGV.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
+ this.timestampColumn,
+ this.logEntryColumn});
+ this.logDGV.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.logDGV.Location = new System.Drawing.Point(3, 3);
+ this.logDGV.Name = "logDGV";
+ this.logDGV.RowHeadersVisible = false;
+ this.logDGV.RowTemplate.Height = 40;
+ this.logDGV.Size = new System.Drawing.Size(390, 419);
+ this.logDGV.TabIndex = 3;
+ this.logDGV.Resize += new System.EventHandler(this.LogDGV_Resize);
+ //
+ // timestampColumn
+ //
+ this.timestampColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.DisplayedCells;
+ this.timestampColumn.HeaderText = "Timestamp";
+ this.timestampColumn.Name = "timestampColumn";
+ this.timestampColumn.ReadOnly = true;
+ this.timestampColumn.Width = 91;
+ //
+ // logEntryColumn
+ //
+ dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
+ this.logEntryColumn.DefaultCellStyle = dataGridViewCellStyle1;
+ this.logEntryColumn.HeaderText = "Log";
+ this.logEntryColumn.Name = "logEntryColumn";
+ this.logEntryColumn.ReadOnly = true;
+ //
+ // panel4
+ //
+ this.panel4.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.panel4.Location = new System.Drawing.Point(3, 422);
+ this.panel4.Name = "panel4";
+ this.panel4.Size = new System.Drawing.Size(390, 5);
+ this.panel4.TabIndex = 2;
+ //
+ // panel2
+ //
+ this.panel2.BackColor = System.Drawing.SystemColors.Control;
+ this.panel2.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.panel2.Controls.Add(this.logCopyBtn);
+ this.panel2.Controls.Add(this.clearLogBtn);
+ this.panel2.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.panel2.Location = new System.Drawing.Point(3, 427);
+ this.panel2.Name = "panel2";
+ this.panel2.Size = new System.Drawing.Size(390, 25);
+ this.panel2.TabIndex = 1;
+ //
+ // clearLogBtn
+ //
+ this.clearLogBtn.Dock = System.Windows.Forms.DockStyle.Left;
+ this.clearLogBtn.Location = new System.Drawing.Point(0, 0);
+ this.clearLogBtn.Name = "clearLogBtn";
+ this.clearLogBtn.Size = new System.Drawing.Size(60, 23);
+ this.clearLogBtn.TabIndex = 0;
+ this.clearLogBtn.Text = "Clear";
+ this.clearLogBtn.UseVisualStyleBackColor = true;
+ this.clearLogBtn.Click += new System.EventHandler(this.clearLogBtn_Click);
+ //
+ // counterTimer
+ //
+ this.counterTimer.Interval = 950;
+ this.counterTimer.Tick += new System.EventHandler(this.CounterTimer_Tick);
+ //
+ // logCopyBtn
+ //
+ this.logCopyBtn.Dock = System.Windows.Forms.DockStyle.Right;
+ this.logCopyBtn.Location = new System.Drawing.Point(331, 0);
+ this.logCopyBtn.Name = "logCopyBtn";
+ this.logCopyBtn.Size = new System.Drawing.Size(57, 23);
+ this.logCopyBtn.TabIndex = 1;
+ this.logCopyBtn.Text = "Copy";
+ this.logCopyBtn.UseVisualStyleBackColor = true;
+ this.logCopyBtn.Click += new System.EventHandler(this.LogCopyBtn_Click);
+ //
+ // ProcessQueueControl
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.Controls.Add(this.tabControl1);
+ this.Controls.Add(this.statusStrip1);
+ this.Name = "ProcessQueueControl";
+ this.Size = new System.Drawing.Size(404, 508);
+ this.statusStrip1.ResumeLayout(false);
+ this.statusStrip1.PerformLayout();
+ this.tabControl1.ResumeLayout(false);
+ this.tabPage1.ResumeLayout(false);
+ this.panel1.ResumeLayout(false);
+ this.tabPage2.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)(this.logDGV)).EndInit();
+ this.panel2.ResumeLayout(false);
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+ private System.Windows.Forms.StatusStrip statusStrip1;
+ private System.Windows.Forms.ToolStripProgressBar toolStripProgressBar1;
+ private System.Windows.Forms.TabControl tabControl1;
+ private System.Windows.Forms.TabPage tabPage1;
+ private System.Windows.Forms.Panel panel1;
+ private System.Windows.Forms.TabPage tabPage2;
+ private System.Windows.Forms.Button btnCleanFinished;
+ private System.Windows.Forms.Button cancelAllBtn;
+ private System.Windows.Forms.Panel panel2;
+ private System.Windows.Forms.Button clearLogBtn;
+ private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1;
+ private VirtualFlowControl virtualFlowControl2;
+ private System.Windows.Forms.ToolStripStatusLabel queueNumberLbl;
+ private System.Windows.Forms.ToolStripStatusLabel completedNumberLbl;
+ private System.Windows.Forms.ToolStripStatusLabel errorNumberLbl;
+ private System.Windows.Forms.Panel panel3;
+ private System.Windows.Forms.Panel panel4;
+ private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl;
+ private System.Windows.Forms.Timer counterTimer;
+ private System.Windows.Forms.DataGridView logDGV;
+ private System.Windows.Forms.DataGridViewTextBoxColumn timestampColumn;
+ private System.Windows.Forms.DataGridViewTextBoxColumn logEntryColumn;
+ private System.Windows.Forms.Button logCopyBtn;
+ }
+}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
new file mode 100644
index 00000000..b0fa7aa3
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
@@ -0,0 +1,361 @@
+using LibationWinForms.BookLiberation;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace LibationWinForms.ProcessQueue
+{
+ internal partial class ProcessQueueControl : UserControl, ILogForm
+ {
+ private TrackedQueue Queue = new();
+ private readonly LogMe Logger;
+ private int QueuedCount
+ {
+ set
+ {
+ queueNumberLbl.Text = value.ToString();
+ queueNumberLbl.Visible = value > 0;
+ }
+ }
+ private int ErrorCount
+ {
+ set
+ {
+ errorNumberLbl.Text = value.ToString();
+ errorNumberLbl.Visible = value > 0;
+ }
+ }
+
+ private int CompletedCount
+ {
+ set
+ {
+ completedNumberLbl.Text = value.ToString();
+ completedNumberLbl.Visible = value > 0;
+ }
+ }
+
+ public Task QueueRunner { get; private set; }
+ public bool Running => !QueueRunner?.IsCompleted ?? false;
+ public ToolStripButton popoutBtn = new();
+
+ public ProcessQueueControl()
+ {
+ InitializeComponent();
+ Logger = LogMe.RegisterForm(this);
+
+ runningTimeLbl.Text = string.Empty;
+ popoutBtn.DisplayStyle = ToolStripItemDisplayStyle.Text;
+ popoutBtn.Name = "popoutBtn";
+ popoutBtn.Text = "Pop Out";
+ popoutBtn.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ popoutBtn.Alignment = ToolStripItemAlignment.Right;
+ popoutBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+
+ statusStrip1.Items.Add(popoutBtn);
+
+ virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData;
+ virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
+
+ Queue.QueuededCountChanged += Queue_QueuededCountChanged;
+ Queue.CompletedCountChanged += Queue_CompletedCountChanged;
+
+ QueuedCount = 0;
+ ErrorCount = 0;
+ CompletedCount = 0;
+ }
+
+ public void AddDownloadPdf(IEnumerable entries)
+ {
+ foreach (var entry in entries)
+ AddDownloadPdf(entry);
+ }
+
+ public void AddDownloadDecrypt(IEnumerable entries)
+ {
+ foreach (var entry in entries)
+ AddDownloadDecrypt(entry);
+ }
+
+ public void AddConvertMp3(IEnumerable entries)
+ {
+ foreach (var entry in entries)
+ AddConvertMp3(entry);
+ }
+
+ public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
+ {
+ if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId))
+ return;
+
+ ProcessBook pbook = new(libraryBook, Logger);
+ pbook.PropertyChanged += Pbook_DataAvailable;
+ pbook.AddDownloadPdf();
+ AddToQueue(pbook);
+ }
+
+ public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook)
+ {
+ if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId))
+ return;
+
+ ProcessBook pbook = new(libraryBook, Logger);
+ pbook.PropertyChanged += Pbook_DataAvailable;
+ pbook.AddDownloadDecryptBook();
+ pbook.AddDownloadPdf();
+ AddToQueue(pbook);
+ }
+
+ public void AddConvertMp3(DataLayer.LibraryBook libraryBook)
+ {
+ if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId))
+ return;
+
+ ProcessBook pbook = new(libraryBook, Logger);
+ pbook.PropertyChanged += Pbook_DataAvailable;
+ pbook.AddConvertToMp3();
+ AddToQueue(pbook);
+ }
+
+ private void AddToQueue(ProcessBook pbook)
+ {
+ BeginInvoke(() =>
+ {
+ Queue.Enqueue(pbook);
+ if (!Running)
+ QueueRunner = QueueLoop();
+ });
+ }
+
+ DateTime StartintTime;
+ private async Task QueueLoop()
+ {
+ StartintTime = DateTime.Now;
+ counterTimer.Start();
+
+ while (Queue.MoveNext())
+ {
+ var nextBook = Queue.Current;
+
+ var result = await nextBook.ProcessOneAsync();
+
+ if (result == ProcessBookResult.FailedRetry)
+ Queue.Enqueue(nextBook);
+ else if (result == ProcessBookResult.ValidationFail)
+ Queue.ClearCurrent();
+ else if (result == ProcessBookResult.FailedAbort)
+ return;
+ }
+ Queue_CompletedCountChanged(this, 0);
+ counterTimer.Stop();
+ virtualFlowControl2.VirtualControlCount = Queue.Count;
+ UpdateAllControls();
+ }
+
+ public void WriteLine(string text)
+ {
+ if (IsDisposed) return;
+
+ var timeStamp = DateTime.Now;
+ logDGV.Rows.Add(timeStamp, text.Trim());
+ }
+
+ #region Control event handlers
+
+ private void Queue_CompletedCountChanged(object sender, int e)
+ {
+ int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.ValidationFail);
+ int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
+
+ ErrorCount = errCount;
+ CompletedCount = completeCount;
+ UpdateProgressBar();
+ }
+ private void Queue_QueuededCountChanged(object sender, int cueCount)
+ {
+ QueuedCount = cueCount;
+ virtualFlowControl2.VirtualControlCount = Queue.Count;
+ UpdateProgressBar();
+ }
+ private void UpdateProgressBar()
+ {
+ toolStripProgressBar1.Maximum = Queue.Count;
+ toolStripProgressBar1.Value = Queue.Completed.Count;
+ }
+
+ private void cancelAllBtn_Click(object sender, EventArgs e)
+ {
+ Queue.ClearQueue();
+ Queue.Current?.Cancel();
+ virtualFlowControl2.VirtualControlCount = Queue.Count;
+ UpdateAllControls();
+ }
+
+ private void btnClearFinished_Click(object sender, EventArgs e)
+ {
+ Queue.ClearCompleted();
+ virtualFlowControl2.VirtualControlCount = Queue.Count;
+ UpdateAllControls();
+
+ if (!Running)
+ runningTimeLbl.Text = string.Empty;
+ }
+
+ private void CounterTimer_Tick(object sender, EventArgs e)
+ {
+ string timeToStr(TimeSpan time)
+ {
+ string minsSecs = $"{time:mm\\:ss}";
+ if (time.TotalHours >= 1)
+ return $"{time.TotalHours:F0}:{minsSecs}";
+ return minsSecs;
+ }
+
+ if (Running)
+ runningTimeLbl.Text = timeToStr(DateTime.Now - StartintTime);
+ }
+
+ private void clearLogBtn_Click(object sender, EventArgs e)
+ {
+ logDGV.Rows.Clear();
+ }
+
+ private void LogCopyBtn_Click(object sender, EventArgs e)
+ {
+ string logText = string.Join("\r\n", logDGV.Rows.Cast().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}"));
+ Clipboard.SetDataObject(logText, false, 5, 150);
+ }
+
+ private void LogDGV_Resize(object sender, EventArgs e)
+ {
+ logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width;
+ }
+
+ #endregion
+
+ #region View-Model update event handling
+
+ ///
+ /// Index of the first visible in the
+ ///
+ private int FirstVisible = 0;
+ ///
+ /// Number of visible in the
+ ///
+ private int NumVisible = 0;
+ ///
+ /// Controls displaying the state, starting with
+ ///
+ private IReadOnlyList Panels;
+
+ ///
+ /// Updates the display of a single at within
+ ///
+ /// index of the within the
+ private void UpdateControl(int queueIndex, string propertyName = null)
+ {
+ int i = queueIndex - FirstVisible;
+
+ if (i > NumVisible || i < 0) return;
+
+ var proc = Queue[queueIndex];
+
+ Panels[i].Invoke(() =>
+ {
+ Panels[i].SuspendLayout();
+ if (propertyName is null || propertyName == nameof(proc.Cover))
+ Panels[i].SetCover(proc.Cover);
+ if (propertyName is null || propertyName == nameof(proc.BookText))
+ Panels[i].SetBookInfo(proc.BookText);
+
+ if (proc.Result != ProcessBookResult.None)
+ {
+ Panels[i].SetResult(proc.Result);
+ return;
+ }
+
+ if (propertyName is null || propertyName == nameof(proc.Status))
+ Panels[i].SetStatus(proc.Status);
+ if (propertyName is null || propertyName == nameof(proc.Progress))
+ Panels[i].SetProgrss(proc.Progress);
+ if (propertyName is null || propertyName == nameof(proc.TimeRemaining))
+ Panels[i].SetRemainingTime(proc.TimeRemaining);
+ Panels[i].ResumeLayout();
+ });
+ }
+
+ private void UpdateAllControls()
+ {
+ int numToShow = Math.Min(NumVisible, Queue.Count - FirstVisible);
+
+ for (int i = 0; i < numToShow; i++)
+ UpdateControl(FirstVisible + i);
+ }
+
+
+ ///
+ /// View notified the model that a botton was clicked
+ ///
+ /// index of the within
+ /// The clicked control to update
+ private async void VirtualFlowControl2_ButtonClicked(int queueIndex, string buttonName, ProcessBookControl panelClicked)
+ {
+ ProcessBook item = Queue[queueIndex];
+ if (buttonName == nameof(panelClicked.cancelBtn))
+ {
+ await item.Cancel();
+ Queue.RemoveQueued(item);
+ virtualFlowControl2.VirtualControlCount = Queue.Count;
+ }
+ else if (buttonName == nameof(panelClicked.moveFirstBtn))
+ {
+ Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
+ UpdateAllControls();
+ }
+ else if (buttonName == nameof(panelClicked.moveUpBtn))
+ {
+ Queue.MoveQueuePosition(item, QueuePosition.OneUp);
+ UpdateControl(queueIndex);
+ if (queueIndex > 0)
+ UpdateControl(queueIndex - 1);
+ }
+ else if (buttonName == nameof(panelClicked.moveDownBtn))
+ {
+ Queue.MoveQueuePosition(item, QueuePosition.OneDown);
+ UpdateControl(queueIndex);
+ if (queueIndex + 1 < Queue.Count)
+ UpdateControl(queueIndex + 1);
+ }
+ else if (buttonName == nameof(panelClicked.moveLastBtn))
+ {
+ Queue.MoveQueuePosition(item, QueuePosition.Last);
+ UpdateAllControls();
+ }
+ }
+
+ ///
+ /// View needs updating
+ ///
+ private void VirtualFlowControl1_RequestData(int firstIndex, int numVisible, IReadOnlyList panelsToFill)
+ {
+ FirstVisible = firstIndex;
+ NumVisible = numVisible;
+ Panels = panelsToFill;
+ UpdateAllControls();
+ }
+
+ ///
+ /// Model updates the view
+ ///
+ private void Pbook_DataAvailable(object sender, PropertyChangedEventArgs e)
+ {
+ int index = Queue.IndexOf((ProcessBook)sender);
+ UpdateControl(index, e.PropertyName);
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.resx b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.resx
new file mode 100644
index 00000000..00da05c2
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.resx
@@ -0,0 +1,642 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAAm4AAAJuCAYAAAAJqI4TAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
+ MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAACyySURBVHhe7d0/zL5tXd9xwQik
+ okkDuEhs0kGj3URxFYOmSYeq0a1xdDUdmq4MHVwZuzqadCLibCNOTVAcUTb/LCKDYIJBepziHeTg+zzH
+ 735+93Xen+9xvD7JK8Hv4lM5r+d850pz3T/w7W9/GwCABsojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnK
+ IwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnK
+ IwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnK
+ IwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnK
+ IwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnK
+ IwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnK
+ IwAAecojAAB5yiMAAHnKIwAAecojAAB5yiMAAHnKIwAAecojAAB5ymMnts3++3D9F8o7+2/D3ftfQ/XP
+ wnf95+Hu/buh+mfhu/7vYButaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ E5XHTmybCbc14ZZJuGUSbputaoATlcdObJsJtzXhlkm4ZRJum61qgBOVx05smwm3NeGWSbhlEm6brWqA
+ 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
+ nHBbE26ZhFum1w63zw++RHjBVQ1wovLYydgVbr8/fOz6H6zthNuacMsk3DK9Vrh9dfAlwgNWNcCJymMn
+ Y9cHxfpPuK0Jt0zCLdNrhNv1G6Ef/c5/tJde1QAnKo+d2DYTbmvCLZNwy/Qa4WYPXNUAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj53YNhNua8Itk3DLJNw2W9UAJyqPndg2E25r
+ wi2TcMsk3DZb1QAnKo+d2DYTbmvCLZNwyyTcNlvVACcqj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4
+ rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZ
+ i0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcp
+ j6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3
+ Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B7
+ 0Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMIt
+ k3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3
+ TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4rQm3TMItk3B70Kp3Ivcpj6zZi0+4
+ rQm3TMItk3B70Kp3Ivcpj6yNfbqZTw7JE25rwi2TcMvUJdyufzdX/86OVb0TuU95ZG3s3/4LooP/NyRP
+ uK0Jt0zCLVOXcLv+3Vz988ea34fcqzyyNj/IDQi3/oRbJuGWSbg9yPw+5F7lkbX5QW5AuPUn3DIJt0zC
+ 7UHm9yH3Ko+szQ9yA8KtP+GWSbhlEm4PMr8PuVd5ZG1+kBsQbv0Jt0zCLZNwe5D5fci9yiNr84PcgHDr
+ T7hlEm6ZhNuDzO9D7lUeWZsf5AaEW3/CLZNwyyTcHmR+H3Kv8sja/CA3INz6E26ZhFsm4fYg8/uQe5VH
+ 1uYHuQHh1p9wyyTcMgm3B5nfh9yrPLI2P8gNCLf+hFsm4ZZJuD3I/D7kXuWRtflBbkC49SfcMgm3TMLt
+ Qeb3Ifcqj6zND3IDwq0/4ZZJuGUSbg8yvw+5V3lkbX6QGxBu/Qm3TMItk3B7kPl9yL3KI2vzg9yAcOtP
+ uGUSbpmE24PM70PuVR5Zmx/kBoRbf8Itk3DLJNweZH4fcq/yyNr8IDcg3PoTbpmEWybh9iDz+5B7lUfW
+ 5ge5AeHWn3DLJNwyCbcHmd+H3Ks8sjY/yA0It/6EWybhlkm4Pcj8PuRe5ZG1+UFuQLj1J9wyCbdMwu1B
+ 5vch9yqPrM0PcgPCrT/hlkm4ZRJuDzK/D7lXeWRtfpAbEG79CbdMwi2TcHuQ+X3Ivcoja/OD3IBw60+4
+ ZRJumYTbg8zvQ+5VHlmbH+QGhFt/wi2TcMsk3B5kfh9yr/LI2vwgNyDc+hNumYRbJuH2IPP7kHuVR9bm
+ B7kB4dafcMsk3DIJtweZ34fcqzyyNj/IDQi3/oRbJuGWSbg9yPw+5F7lkbX5QW5AuPUn3DIJt0zC7UHm
+ 9yH3Ko+szQ9yA8KtP+GWSbhlEm4PMr8PuVd5ZG1+kBsQbv0Jt0zCLZNwe5D5fci9yiNr84PcgHDrT7hl
+ Em6ZhNuDzO9D7lUeWZsf5AaEW3/CLZNwyyTcHmR+H3Kv8sja/CA3INz6E26ZhFsm4fYg8/uQe5VH1uYH
+ uQHh1p9wyyTcMgm3B5nfh9yrPLI2P8gNCLf+hFsm4ZZJuD3I/D7kXuWRtflBbkC49SfcMgm3TMLtQeb3
+ Ifcqj6zND3IDwq0/4ZZJuGUSbg8yvw+5V3lkbX6QGxBu/Qm3TMItk3B7kPl9yL3KI2vzg9yAcOtPuGUS
+ bpmE24PM70PuVR5Zmx/kBoRbf8Itk3DLJNweZH4fcq/yyNr8IDcg3PoTbpmEWybh9iDz+5B7lUfW5ge5
+ AeHWn3DLJNwyCbcHmd+H3Ks8sjY/yA0It/6EWybhlkm4Pcj8PuRe5ZG1+UFuQLj1J9wyCbdMwu1B5vch
+ 9yqPrM0PcgPCrT/hlkm4ZRJuDzK/D7lXeWRtfpAbEG79CbdMwi2TcHuQ+X3Ivcoja/OD3IBw60+4ZRJu
+ mYTbg8zvQ+5VHlmbH+QGhFt/wi2TcMsk3B5kfh9yr/LI2vwgNyDc+hNumYRbJuH2IPP7kHuVR9bmB7kB
+ 4dafcMsk3DIJtweZ34fcqzyyNj/IDQi3/oRbJuGWSbg9yPw+5F7lkbX5QW5AuPUn3DIJt0zC7UHm9yH3
+ Ko+szQ9yA8KtP+GWSbhlEm4PMr8PuVd5ZG1+kBsQbv0Jt0zCLZNwe5D5fci9yiNr84PcgHDrT7hlEm6Z
+ hNuDzO9D7lUeWZsf5AbSw+3Tw//mXf3CcPf+61D9s/Bd/2m4ex8Yqn8Wvut/DB0m3HiW8sja/CA3kB5u
+ ZmYnTrjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbkC4
+ mZnlTbjxLOWRtflBbkC4mZnlTbjxLOWRtflBbuCrw+8DEOX6d3P17+xY8/uQe5VH1uYHGQBOML8PuVd5
+ ZG1+kAHgBPP7kHuVR9bmBxkATjC/D7lXeWRtfpAB4ATz+5B7lUfW5gcZAE4wvw+5V3lkbX6QAeAE8/uQ
+ e5VH1uYHGQBOML8PuVd5ZG1+kAHgBPP7kHuVR9bmBxkATjC/D7lXeWRtfpAB4ATz+5B7lUfW5gcZAE4w
+ vw+5V3lkbX6QAeAE8/uQe5VH1uYHGQBOML8PuVd5ZG1+kAHgBPP7kHuVR9bmBxkATjC/D7lXeWRtfpAB
+ 4ATz+5B7lUfW5gcZAE4wvw+5V3lkbX6QAeAE8/uQe5VH1uYHGQBOML8PuVd5ZG1+kAHgBPP7kHuVR9bm
+ BxkATjC/D7lXeWRtfpAB4ATz+5B7lUfW5gcZAE4wvw+5V3lkbX6QAeAE8/uQe5VH1uYHGQBOML8PuVd5
+ ZG1+kAHgBPP7kHuVR9bmBxkATjC/D7lXeWRtfpAB4ATz+5B7lUfW5gcZAE4wvw+5V3lkbX6QAeAE8/uQ
+ e5VH1uYHGQBOML8PuVd5ZG1+kAHgBPP7kHuVR9bmBxkATjC/D7lXeWRtfpAB4ATz+5B7lUfW5gcZAE4w
+ vw+5V3lkbX6QAeAE8/uQe5VH1uYHGQBOML8PuVd5ZG1+kAHgBPP7kHuVR9bmBxkATjC/D7lXeWRtfpAB
+ 4ATz+5B7lUfW5gcZAE4wvw+5V3lkbX6QAeAE8/uQe5VH1uYHGQBOML8PuVd5ZG1+kAHgBPP7kHuVR9bG
+ /iMAnKZ6J3Kf8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7yCABAnvIIAECe8ggAQJ7y
+ CABAnvIIAECe8ggAQJ7yCABAmm//wP8HTmEikkRXgigAAAAASUVORK5CYII=
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAAj0AAAI9CAYAAADRkckBAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
+ MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAACyHSURBVHhe7d1ptGVVee5xKIq+
+ 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
+ gg==
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAAmwAAAJsCAYAAABAlf8lAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
+ MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAACdtSURBVHhe7d3Li239dtdhteGl
+ ExNQMDfvNoQkaozGC9oXQRQbKoK2xEQTbCiJ6H/hLSjYtSHGGFEbgpgQGzZsKGrSESV4CQhGOzEnxpz4
+ W8d3ndT5nrH3rl/VWmuOseYz4OkMeOuFMWvX/jCr9t4/72d/9mcBAGisXAIA0Ee5BACgj3IJAEAf5RIA
+ gD7KJQAAfZRLAAD6KJcAAPRRLgEA6KNcAgDQR7kEAKCPcgkAQB/lEgCAPsolAAB9lEsAAPoolwAA9FEu
+ AQDoo1wCANBHuQQAoI9yCQBAH+USAIA+yiUAAH2USwAA+iiXAAD0US4BAOijXAIA0Ee5BACgj3IJAEAf
+ 5RIAgD7KJQAAfZRLAAD6KJcAAPRRLgEA6KNcAgDQR7kEAKCPcgkAQB/lEgCAPsolAAB9lMuONue3LN+9
+ fN/yI8uPL59fLh8IAJjpZ5bL7+n/dvm7y3cuv3559VSNMUG57OgV80uWb19+eHn5cAGA53V5IfNDyx9c
+ fv7y0akaY4Jy2dEn5g8tP7q8fIAAwLn8y+U3Lx+cqjEmKJcdfWB+8fK3l5cPCwA4r/+z/LmlnKoxJiiX
+ HRXzFcsPLi8fEgDAxV9ffsHyJVM1xgTlsqOYy5u1H1hePhgAgJf+2vIlUzXGBOWyoxjfBgUAXuM7li9O
+ 1RgTlMuOXszlDxi8fBAAAB/yueUbli9M1RgTlMuOPpvLX93hT4MCADsuP/P+hb/yo2qMCcplR5/Nn1le
+ PgAAgNf4fUvZGBOUy44+m3+/vDw+AMBr/LOlbIwJymVHay7/3NTLwwMAvNbln7X6+qoxJiiXHa25/Nug
+ Lw8PALDj26rGmKBcdrTmH7w4OADArr9TNcYE5bKjNT/y4uAAALv+ddUYE5TLjtb8rxcHBwDY9T+qxpig
+ XHa05qdfHBwAYNdPV40xQbnsKA4OALAt+2KKctlRHhwAYFf2xRTlsqM8OADAruyLKcplR3lwAIBd2RdT
+ lMuO8uAAALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDsyr6Yolx2lAcHANiVfTFFuewoDw4AsCv7Yopy2VEe
+ HABgV/bFFOWyozw4AMCu7IspymVHeXAAgF3ZF1OUy47y4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMDAOzK
+ vpiiXHaUBwcA2JV9MUW57CgPDgCwK/tiinLZUR4cAGBX9sUU5bKjPDgAwK7siynKZUd5cACAXdkXU5TL
+ jvLgAAC7si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+mKJcdpQHBwDYlX0xRbnsKA8OALAr+2KKctlRHhwA
+ YFf2xRTlsqM8OADAruyLKcplR3lwAIBd2RdTlMuO8uAAALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDsyr6Y
+ olx2lAcHANiVfTFFuewoDw4AsCv7Yopy2VEeHABgV/bFFOWyozw4AMCu7IspymVHeXAAgF3ZF1OUy47y
+ 4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMDAOzKvpiiXHaUBwcA2JV9MUW57CgPDgCwK/tiinLZUR4cAGBX
+ 9sUU5bKjPDgAwK7siynKZUd5cACAXdkXU5TLjvLgAAC7si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+mKJc
+ dpQHBwDYlX0xRbnsKA8OALAr+2KKctlRHhwAYFf2xRTlsqM8OADAruyLKcplR3lwAIBd2RdTlMuO8uAA
+ ALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDsyr6Yolx2lAcHANiVfTFFuewoDw4AsCv7Yopy2VEeHABgV/bF
+ FOWyozw4AMCu7IspymVHeXAAgF3ZF1OUy47y4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMDAOzKvpiiXHaU
+ BwcA2JV9MUW57CgPDgCwK/tiinLZUR4cAGBX9sUU5bKjPDgAwK7siynKZUd5cACAXdkXU5TLjvLgAAC7
+ si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+mKJcdpQHBwDYlX0xRbnsKA8OALAr+2KKctlRHhwAYFf2xRTl
+ sqM8OADAruyLKcplR3lwAIBd2RdTlMuO8uAAALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDsyr6Yolx2lAcH
+ ANiVfTFFuewoDw4AsCv7Yopy2VEeHABgV/bFFOWyozw4AMCu7IspymVHeXAAgF3ZF1OUy47y4AAAu7Iv
+ piiXHeXBAQB2ZV9MUS47yoMDAOzKvpiiXHaUBwcA2JV9MUW57CgPDgCwK/tiinLZUR4cAGBX9sUU5bKj
+ PDgAwK7siynKZUd5cACAXdkXU5TLjvLgAAC7si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+mKJcdpQHBwDY
+ lX0xRbnsKA8OALAr+2KKctlRHhwAYFf2xRTlsqM8OADAruyLKcplR3lwAIBd2RdTlMuO8uAAALuyL6Yo
+ lx3lwQEAdmVfTFEuO8qDAwDsyr6Yolx2lAcHANiVfTFFuewoDw4AsCv7Yopy2VEeHABgV/bFFOWyozw4
+ AMCu7IspymVHeXAAgF3ZF1OUy47y4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMDAOzKvpiiXHaUBwcA2JV9
+ MUW57CgPDgCwK/tiinLZUR4cAGBX9sUU5bKjPDg08zPLDyzfvfyu5dcsX7V83fLblm9f/uHyk0v130N3
+ l8/d71++bfmW5fK5ffkc/7XL717+4vKDy+XXQvXfQwvZF1OUy47y4NDE55e/t3zD8pr5muWvLj+1VB8P
+ uvnc8leWr15eM9+4/P2l+lhwuOyLKcplR3lwaODyxuFPLG+Zb15+dKk+LnTxX5dvXd4yf2T5iaX6uHCY
+ 7IspymVHeXA42H9fLtH1nrm8sfg3S/Xx4WiXz83XvlX70Fx+jVx+rVQfHw6RfTFFuewoDw4HuvwGdPm2
+ zy3m8jNA/2qp/j9wlEus/bLlFvMblx9bqv8PPFz2xRTlsqM8OBzklrF2HdFGJ7eMteuINtrIvpiiXHaU
+ B4cD3CPWriPa6OAesXYd0UYL2RdTlMuO8uDwYPeMteuINo50z1i7jmjjcNkXU5TLjvLg8ECPiLXriDaO
+ 8IhYu45o41DZF1OUy47y4PAgj4y164g2HumRsXYd0cZhsi+mKJcd5cHhAY6IteuINh7hiFi7jmjjENkX
+ U5TLjvLgcGdHxtp1RBv3dGSsXUe08XDZF1OUy47y4HBHHWLtOqKNe+gQa9cRbTxU9sUU5bKjPDjcSadY
+ u45o45Y6xdp1RBsPk30xRbnsKA8Od9Ax1q4j2riFjrF2HdHGQ2RfTFEuO8qDw411jrXriDbeo3OsXUe0
+ cXfZF1OUy47y4HBDE2LtOqKNt5gQa9cRbdxV9sUU5bKjPDjcyKRYu45oY8ekWLuOaONusi+mKJcd5cHh
+ BibG2nVEG68xMdauI9q4i+yLKcplR3lweKfJsXYd0cbHTI6164g2bi77Yopy2VEeHN7hGWLtOqKNyjPE
+ 2nVEGzeVfTFFuewoDw5v9Eyxdh3RxkvPFGvXEW3cTPbFFOWyozw4vMEzxtp1RBsXzxhr1xFt3ET2xRTl
+ sqM8OGx65li7jmg7t2eOteuINt4t+2KKctlRHhw2nCHWriPazukMsXYd0ca7ZF9MUS47yoPDK50p1q4j
+ 2s7lTLF2HdHGm2VfTFEuO8qDwyucMdauI9rO4Yyxdh3RxptkX0xRLjvKg8MnnDnWriPantuZY+06oo1t
+ 2RdTlMuO8uDwEWLt50a0PSex9nMj2tiSfTFFuewoDw4fINa+fETbcxFrXz6ijVfLvpiiXHaUB4eCWPvw
+ iLbnINY+PKKNV8m+mKJcdpQHhyDWPj2ibTax9ukRbXxS9sUU5bKjPDi8INZeP6JtJrH2+hFtfFT2xRTl
+ sqM8OHxGrO2PaJtFrO2PaOODsi+mKJcd5cFh+YnlmxezP5cAuIRAdVf6EGtvn8vXhsvXiOqunFj2xRTl
+ sqM8OKf3+eWPLubt401bb2Lt/fOHl8vXiuq+nFT2xRTlsqM8OKf39xbz/hFtPYm1280/WKobc1LZF1OU
+ y47y4JzazyzftJjbjGjrRazddr5huXzNqG7NCWVfTFEuO8qDc2o/sJjbjp9p60Gs3Wd+aKnuzQllX0xR
+ LjvKg3Nq37WY2483bccSa/ebv7RUN+eEsi+mKJcd5cE5td+1mPuMN23HEGv3nd+zVHfnhLIvpiiXHeXB
+ ObVfvZj7jTdtjyXW7j+/bqluzwllX0xRLjvKg3Nqv3Qx9x1v2h5DrD1mvnKp7s8JZV9MUS47yoNzal+/
+ mPuPN233JdYeN5e38tUz4ISyL6Yolx3lwTm1b1nMY8abtvsQa4+db12q58AJZV9MUS47yoNzat++mMeN
+ N223JdYeP9+5VM+CE8q+mKJcdpQH59T+4WIeO6LtNsTaMfNPlup5cELZF1OUy47y4JzaTy5fs5jHjm+P
+ vo9YO2a+bvncUj0TTij7Yopy2VEenNP7q4t5/HjT9jZi7bj5nqV6JpxU9sUU5bKjPDin91PL5QeJzePH
+ m7Y9Yu24+Z3L5WtF9Vw4qeyLKcplR3lwWH5s+drFPH68aXsdsXbc/IrlvyzVc+HEsi+mKJcd5cHhMz+8
+ XL4wm8ePaPs4sXbc/PLFW2BK2RdTlMuO8uDwgmg7bkRbTawdN2KNj8q+mKJcdpQHhyDajhvR9qXE2nEj
+ 1vik7IspymVHeXAoiLbjRrT9f2LtuBFrvEr2xRTlsqM8OHyAaDtuzh5tYu24EWu8WvbFFOWyozw4fIRo
+ O27OGm1i7bgRa2zJvpiiXHaUB4dPEG3HzdmiTawdN2KNbdkXU5TLjvLg8Aqi7bg5S7SJteNGrPEm2RdT
+ lMuO8uDwSqLtuHn2aBNrx41Y482yL6Yolx3lwWGDaDtunjXaxNpxI9Z4l+yLKcplR3lw2CTajptnizax
+ dtyINd4t+2KKctlRHhzeQLQdN88SbWLtuBFr3ET2xRTlsqM8OLyRaDtupkebWDtuxBo3k30xRbnsKA8O
+ 7yDajpup0SbWjhuxxk1lX0xRLjvKg8M7ibbjZlq0ibXjRqxxc9kXU5TLjvLgcAOi7biZEm1i7bgRa9xF
+ 9sUU5bKjPDjciGg7brpHm1g7bsQad5N9MUW57CgPDjck2o6brtEm1o4bscZdZV9MUS47yoPDjYm246Zb
+ tIm140ascXfZF1OUy47y4HAHou246RJtYu24EWs8RPbFFOWyozw43IloO26OjjaxdtyINR4m+2KKctlR
+ HhzuSLQdN0dFm1g7bsQaD5V9MUW57CgPDncm2o6bR0ebWDtuxBoPl30xRbnsKA8ODyDajptHRZtYO27E
+ GofIvpiiXHaUB4cHEW3Hzb2jTawdN2KNw2RfTFEuO8qDwwOJtuPmXtEm1o4bscahsi+mKJcd5cHhwUTb
+ cXPraBNrx41Y43DZF1OUy47y4HAA0Xbc3CraxNpxI9ZoIftiinLZUR4cDiLajpv3RptYO27EGm1kX0xR
+ LjvKg8OBRNtx89ZoE2vHjVijleyLKcplR3lwOJhoO252o02sHTdijXayL6Yolx3lwaEB0XbcvDbaxNpx
+ I9ZoKftiinLZUR4cmhBtx82nok2sHTdijbayL6Yolx3lwaER0XbcfCjaxNpxI9ZoLftiinLZUR4cmhFt
+ x01Gm1g7bsQa7WVfTFEuO8qDQ0Oi7bi5RptYO27EGiNkX0xRLjvKg0NTguG4udzd7Y+Zy93FGiNkX0xR
+ LjvKg0Nj3rSZM403a4ySfTFFuewoDw7NiTZzhhFrjJN9MUW57CgPDgOINvPMI9YYKftiinLZUR4chhBt
+ 5hlHrDFW9sUU5bKjPDgMItrMM41YY7TsiynKZUd5cBhGtJlnGLHGeNkXU5TLjvLgMJBoM5NHrPEUsi+m
+ KJcd5cFhKNFmJo5Y42lkX0xRLjvKg8Ngos1MGrHGU8m+mKJcdpQHh+FEm5kwYo2nk30xRbnsKA8OT0C0
+ mc4j1nhK2RdTlMuO8uDwJESb6ThijaeVfTFFuewoDw5PRLSZTiPWeGrZF1OUy47y4PBkRJvpMGKNp5d9
+ MUW57CgPDk9ItJkjR6xxCtkXU5TLjvLg8KREmzlixBqnkX0xRbnsKA8OT0y0mUeOWONUsi+mKJcd5cHh
+ yYk284gRa5xO9sUU5bKjPDicgGgz9xyxxillX0xRLjvKg8NJiDZzjxFrnFb2xRTlsqM8OJyIaDO3HLHG
+ qWVfTFEuO8qDw8mINnOLEWucXvbFFOWyozw4nJBoM+8ZsQZL9sUU5bKjPDiclGgzbxmxBp/JvpiiXHaU
+ B4cTE21mZ8QavJB9MUW57CgPDicn2sxrRqxByL6Yolx2lAcHRJv56Ig1KGRfTFEuO8qDA18g2kw1Yg0+
+ IPtiinLZUR4c+CLRZl6OWIOPyL6Yolx2lAcHvoRoM5cRa/AJ2RdTlMuO8uDAlxFt5x6xBq+QfTFFuewo
+ Dw6URNs5R6zBK2VfTFEuO8qDAx8k2s41Yg02ZF9MUS47yoMDHyXazjFiDTZlX0xRLjvKgwOfJNqee8Qa
+ vEH2xRTlsqM8OPAqou05R6zBG2VfTFEuO8qDA68m2p5rxBq8Q/bFFOWyozw4sEW0PceINXin7IspymVH
+ eXBgm2ibPWINbiD7Yopy2VEeHHgT0TZzxBrcSPbFFOWyozw48GaibdaINbih7IspymVHeXDgXUTbjBFr
+ cGPZF1OUy47y4MC7ibbeI9bgDrIvpiiXHeXBgZsQbT1HrMGdZF9MUS47yoMDNyPaeo1YgzvKvpiiXHaU
+ BwduSrT1GLEGd5Z9MUW57CgPDtycaDt2xBo8QPbFFOWyozw4cHOXWPhlizlmvmr5V0v1bIAbyb6Yolx2
+ lAcHbkqs9RjRBneWfTFFuewoDw7cjFjrNaIN7ij7Yopy2VEeHLgJsdZzRBvcSfbFFOWyozw48G5irfeI
+ NriD7IspymVHeXDgXcTajBFtcGPZF1OUy47y4MCbibVZI9rghrIvpiiXHeXBgTcRazNHtMGNZF9MUS47
+ yoMD28Ta7BFtcAPZF1OUy47y4MAWsfYcI9rgnbIvpiiXHeXBgVcTa881og3eIftiinLZUR4ceBWx9pwj
+ 2uCNsi+mKJcd5cGBTxJrzz2iDd4g+2KKctlRHhz4KLF2jhFtsCn7Yopy2VEeHPggsXauEW2wIftiinLZ
+ UR4cKIm1c45og1fKvpiiXHaUBwe+jFg794g2eIXsiynKZUd5cOBLiDVzGdEGn5B9MUW57CgPDnyRWDMv
+ R7TBR2RfTFEuO8qDA18g1kw1og0+IPtiinLZUR4cEGvmoyPaoJB9MUW57CgPDicn1sxrRrRByL6Yolx2
+ lAeHExNrZmdEG7yQfTFFuewoDw4nJdbMW0a0wWeyL6Yolx3lweGExJp5z4g2WLIvpiiXHeXB4WTEmrnF
+ iDZOL/tiinLZUR4cTkSsmVuOaOPUsi+mKJcd5cHhJMSauceINk4r+2KKctlRHhxOQKyZe45o45SyL6Yo
+ lx3lweHJiTXziBFtnE72xRTlsqM8ODwxsWYeOaKNU8m+mKJcdpQHhycl1swRI9o4jeyLKcplR3lweEJi
+ zRw5oo1TyL6Yolx2lAeHJyPWTIcRbTy97IspymVHeXB4ImLNdBrRxlPLvpiiXHaUB4cnIdZMxxFtPK3s
+ iynKZUd5cHgCYs10HtHGU8q+mKJcdpQHh+HEmpkwoo2nk30xRbnsKA8Og4k1M2lEG08l+2KKctlRHhyG
+ Emtm4og2nkb2xRTlsqM8OAwk1szkEW08heyLKcplR3lwGEasmWcY0cZ42RdTlMuO8uAwiFgzzzSijdGy
+ L6Yolx3lwWEIsWaecUQbY2VfTFEuO8qDwwBizTzziDZGyr6Yolx2lAeH5sSaOcOINsbJvpiiXHaUB4fG
+ xJo504g2Rsm+mKJcdpQHh6bE2nFzubvbHzOXu18+96tfE9BK9sUU5bKjPDg0JNaOm1++XO7/w8uvuCzM
+ w8ebNkbIvpiiXHaUB4dmxNpxc42167MQbceNaKO97IspymVHeXBoRKwdNxlrV6LtuBFttJZ9MUW57CgP
+ Dk2ItePmQ7F2JdqOG9FGW9kXU5TLjvLg0IBYO24+FWtXou24EW20lH0xRbnsKA8OBxNrx81rY+1KtB03
+ oo12si+mKJcd5cHhQGLtuNmNtSvRdtyINlrJvpiiXHaUB4eDiLXj5q2xdiXajhvRRhvZF1OUy47y4HAA
+ sXbcvDfWrkTbcSPaaCH7Yopy2VEeHB5MrB03t4q1K9F23Ig2Dpd9MUW57CgPDg8k1o6bW8falWg7bkQb
+ h8q+mKJcdpQHhwcRa8fNvWLtSrQdN6KNw2RfTFEuO8qDwwOItePm3rF2JdqOG9HGIbIvpiiXHeXB4c7E
+ 2nHzqFi7Em3HjWjj4bIvpiiXHeXB4Y7E2nHz6Fi7Em3HjWjjobIvpiiXHeXB4U7E2nFzVKxdibbjRrTx
+ MNkXU5TLjvLgcAdi7bg5OtauRNtxI9p4iOyLKcplR3lwuDGxdtx0ibUr0XbciDbuLvtiinLZUR4cbkis
+ HTfdYu1KtB03oo27yr6Yolx2lAeHGxFrx03XWLsSbceNaONusi+mKJcd5cHhBsTacdM91q5E23Ej2riL
+ 7IspymVHeXB4J7F23EyJtSvRdtyINm4u+2KKctlRHhzeQawdN9Ni7Uq0HTeijZvKvpiiXHaUB4c3EmvH
+ zdRYuxJtx41o42ayL6Yolx3lweENxNpxMz3WrkTbcSPauInsiynKZUd5cNgk1o6bZ4m1K9F23Ig23i37
+ Yopy2VEeHDaItePm2WLtSrQdN6KNd8m+mKJcdpQHh1cSa8fNs8balWg7bkQbb5Z9MUW57CgPDq8g1o6b
+ Z4+1K9F23Ig23iT7Yopy2VEeHD5BrB03Z4m1K9F23Ig2tmVfTFEuO8qDw0eItePmbLF2JdqOG9HGluyL
+ KcplR3lw+ACxdtycNdauRNtxI9p4teyLKcplR3lwKIi14+bssXYl2o4b0carZF9MUS47yoNDEGvHjVj7
+ UqLtuBFtfFL2xRTlsqM8OLwg1o4bsVYTbceNaOOjsi+mKJcd5cHhM2LtuBFrHyfajhvRxgdlX0xRLjvK
+ g8Py35avXczjR6y9jmg7bi53/89L9Vw4seyLKcplR3lwTu9zy29fzOPn8kZTrL2et8DHzbcuP7VUz4WT
+ yr6Yolx2lAfn9P7KYh4/3qy9jTdtx83fWKpnwkllX0xRLjvKg3Nq/3v56sU8drxZex9v2o6Zy49N/ORS
+ PRNOKPtiinLZUR6cU/v+xTx2vFm7DW/ajpl/vFTPgxPKvpiiXHaUB+fUvm0xjxuxdlui7fHzHUv1LDih
+ 7IspymVHeXBO7VsW85jxbdD78O3Rx87lDx9Uz4ETyr6Yolx2lAfn1L5uMfcfb9buy5u2x82vWqpnwAll
+ X0xRLjvKg3Nqv3Qx9x1v1h7Dm7bHzFcu1f05oeyLKcplR3lwTu3XLuZ+483aY3nTdv/5DUt1e04o+2KK
+ ctlRHpxT+92Luc94s3YMb9ruO793qe7OCWVfTFEuO8qDc2rfvZjbjzdrx/Km7X7zl5fq5pxQ9sUU5bKj
+ PDin9oOLue14s9aDN233mX+xVPfmhLIvpiiXHeXBObXPL79pMbcZb9Z68abttvONy88s1a05oeyLKcpl
+ R3lwTu97F/P+EWs9ibbbzeVfRqluzEllX0xRLjvKg8Pyxxbz9hFrvYm2988fX6rbcmLZF1OUy47y4LD8
+ xPLNi9kfP7M2g59pe/tcvjb876W6KyeWfTFFuewoDw6f+e/LNy3m9ePN2izetO3Pb1x+bKnuycllX0xR
+ LjvKg8MLou31I9ZmEm2vH7HGR2VfTFEuO8qDQxBtnx6xNpto+/SINT4p+2KKctlRHhwKou3DI9aeg2j7
+ 8Ig1XiX7Yopy2VEeHD5AtH35iLXnItq+fMQar5Z9MUW57CgPDh8h2n5uxNpzEm0/N2KNLdkXU5TLjvLg
+ 8AmiTaw9O9Em1niD7IspymVHeXB4hTNHm1g7hzNHm1jjTbIvpiiXHeXB4ZXOGG1i7VzOGG1ijTfLvpii
+ XHaUB4cNZ4o2sXZOZ4o2sca7ZF9MUS47yoPDpjNEm1g7tzNEm1jj3bIvpiiXHeXB4Q2eOdrEGhfPHG1i
+ jZvIvpiiXHaUB4c3esZoE2u89IzRJta4meyLKcplR3lweIdnijaxRuWZok2scVPZF1OUy47y4PBOzxBt
+ Yo2PeYZoE2vcXPbFFOWyozw43MDkaBNrvMbkaBNr3EX2xRTlsqM8ONzIxGgTa+yYGG1ijbvJvpiiXHaU
+ B4cbmhRtYo23mBRtYo27yr6Yolx2lAeHG5sQbWKN95gQbWKNu8u+mKJcdpQHhzvoHG1ijVvoHG1ijYfI
+ vpiiXHaUB4c76RhtYo1b6hhtYo2Hyb6Yolx2lAeHO+oUbWKNe+gUbWKNh8q+mKJcdpQHhzvrEG1ijXvq
+ EG1ijYfLvpiiXHaUB4cHODLaxBqPcGS0iTUOkX0xRbnsKA8OD3JEtIk1HumIaBNrHCb7Yopy2VEeHB7o
+ kdEm1jjCI6NNrHGo7IspymVHeXB4sEdEm1jjSI+INrHG4bIvpiiXHeXB4QD3jDaxRgf3jDaxRgvZF1OU
+ y47y4HCQe0SbWKOTe0SbWKON7IspymVHeXA40C2jTazR0S2jTazRSvbFFOWyozw4HOwSbd+8vGe+ehFr
+ dHX53Lx8jr5nfuty+bVSfXw4RPbFFOWyozw4NPCTy59c3jKX38h+dKk+LnTxX5ffsbxl/tjyE0v1ceEw
+ 2RdTlMuO8uDQyPctr/0W6dct37P8n6X6WNDNTy1/ffna5TXzm5fvX6qPBYfLvpiiXHaUB4dmPr/80PKX
+ lt+z/Prlq5ZfuVzeUHzH8o+Xzy3Vfw/dXd4o/6Plzy6Xz+mvXy6f479h+b3L5XP/XyyXXwvVfw8tZF9M
+ US47yoMDAOzKvpiiXHaUBwcA2JV9MUW57CgPDgCwK/tiinLZUR4cAGBX9sUU5bKjPDgAwK7siynKZUd5
+ cACAXdkXU5TLjvLgAAC7si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+mKJcdpQHBwDYlX0xRbnsKA8OALAr
+ +2KKctlRHhwAYFf2xRTlsqM8OADAruyLKcplR3lwAIBd2RdTlMuO8uAAALuyL6Yolx3lwQEAdmVfTFEu
+ O8qDAwDsyr6Yolx2lAcHANiVfTFFuewoDw4AsCv7Yopy2VEeHABgV/bFFOWyozw4AMCu7IspymVHeXAA
+ gF3ZF1OUy47y4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMDAOzKvpiiXHaUBwcA2JV9MUW57CgPDgCwK/ti
+ inLZUR4cAGBX9sUU5bKjPDgAwK7siynKZUd5cACAXdkXU5TLjvLgAAC7si+mKJcd5cEBAHZlX0xRLjvK
+ gwMA7Mq+mKJcdpQHBwDYlX0xRbnsKA8OALAr+2KKctlRHhwAYFf2xRTlsqM8OADAruyLKcplR3lwAIBd
+ 2RdTlMuO8uAAALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDsyr6Yolx2lAcHANiVfTFFuewoDw4AsCv7Yopy
+ 2VEeHABgV/bFFOWyozw4AMCu7IspymVHeXAAgF3ZF1OUy47y4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMD
+ AOzKvpiiXHaUBwcA2JV9MUW57CgPDgCwK/tiinLZUR4cAGBX9sUU5bKjPDgAwK7siynKZUd5cACAXdkX
+ U5TLjvLgAAC7si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+mKJcdpQHBwDYlX0xRbnsKA8OALAr+2KKctlR
+ HhwAYFf2xRTlsqM8OADAruyLKcplR3lwAIBd2RdTlMuO8uAAALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDs
+ yr6Yolx2lAcHANiVfTFFuewoDw4AsCv7Yopy2VEeHABgV/bFFOWyozw4AMCu7IspymVHeXAAgF3ZF1OU
+ y47y4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMDAOzKvpiiXHaUBwcA2JV9MUW57CgPDgCwK/tiinLZUR4c
+ AGBX9sUU5bKjPDgAwK7siynKZUd5cACAXdkXU5TLjvLgAAC7si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+
+ mKJcdpQHBwDYlX0xRbnsKA8OALAr+2KKctlRHhwAYFf2xRTlsqM8OADAruyLKcplR3lwAIBd2RdTlMuO
+ 8uAAALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDsyr6Yolx2lAcHANiVfTFFuewoDw4AsCv7Yopy2VEeHABg
+ V/bFFOWyozw4AMCu7IspymVHeXAAgF3ZF1OUy47y4AAAu7IvpiiXHeXBAQB2ZV9MUS47yoMDAOzKvpii
+ XHaUBwcA2JV9MUW57CgPDgCwK/tiinLZUR4cAGBX9sUU5bKjPDgAwK7siynKZUd5cACAXdkXU5TLjvLg
+ AAC7si+mKJcd5cEBAHZlX0xRLjvKgwMA7Mq+mKJcdpQHBwDYlX0xRbnsKA8OALAr+2KKctlRHhwAYFf2
+ xRTlsqM8OADAruyLKcplR3lwAIBd2RdTlMuO8uAAALuyL6Yolx3lwQEAdmVfTFEuO8qDAwDsyr6Yolx2
+ lAcHANiVfTFFuexozU+/PDgAwKafrhpjgnLZ0Zr/+eLgAAC7frxqjAnKZUdrfuTFwQEAdv27qjEmKJcd
+ rfm+FwcHANj1vVVjTFAuO1rzXS8ODgCw689XjTFBuexozW96cXAAgF3fWDXGBOWyo8/m3y0vDw8A8Br/
+ dikbY4Jy2dFn86eXl8cHAHiNP7WUjTFBuezos/nFy39aXj4AAICP+Q/LL1rKxpigXHb0Yv7A8vIhAAB8
+ zO9fvjBVY0xQLjuK+VvLywcBAFD5G8sXp2qMCcplRzG/cPmny8sHAgDw0j9fvvCt0OtUjTFBueyomK9Y
+ Lg/i5YMBALj4Z8ulFb5kqsaYoFx29IG5VPPfXF4+IADg3C7fBv2SN2vXqRpjgnLZ0Sfm8gcR/uPy8mEB
+ AOdy+dOgX/wDBtVUjTFBuezoFXMp6cvf0+Yv1wWAc7n8pbiXv2ft8jPuH52qMSYolx1tzjctf2H53uUS
+ cD++/N/l5cMFAGa5/F5++T398nv75ff4P7984/LqqRpjgnIJAEAf5RIAgD7KJQAAfZRLAAD6KJcAAPRR
+ LgEA6KNcAgDQR7kEAKCPcgkAQB/lEgCAPsolAAB9lEsAAPoolwAA9FEuAQDoo1wCANBHuQQAoI9yCQBA
+ H+USAIA+yiUAAH2USwAA+iiXAAD0US4BAOijXAIA0Ee5BACgj3IJAEAf5RIAgD7KJQAAfZRLAAD6KJcA
+ APRRLgEA6KNcAgDQR7kEAKCPcgkAQB/lEgCAPsolAAB9lEsAAPoolwAA9FEuAQDoo1wCANBHuQQAoI9y
+ CQBAH+USAIA+yiUAAH2USwAA+iiXAAD0US4BAOijXAIA0Ee5BACgj3IJAEAf5RIAgC5+9uf9P2fMnIaN
+ w1EmAAAAAElFTkSuQmCC
+
+
+
+ True
+
+
+ True
+
+
+ 133, 17
+
+
\ No newline at end of file
diff --git a/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs
new file mode 100644
index 00000000..20bbe7e8
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs
@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace LibationWinForms.ProcessQueue
+{
+ public enum QueuePosition
+ {
+ Fisrt,
+ OneUp,
+ OneDown,
+ Last
+ }
+
+ /*
+ * This data structure is like lifting a metal chain one link at a time.
+ * Each time you grab and lift a new link (MoveNext call):
+ *
+ * 1) you're holding a new link in your hand (Current)
+ * 2) the remaining chain to be lifted shortens by 1 link (Queued)
+ * 3) the pile of chain at your feet grows by 1 link (Completed)
+ *
+ * The index is the link position from the first link you lifted to the
+ * last one in the chain.
+ */
+ public class TrackedQueue where T : class
+ {
+ public event EventHandler CompletedCountChanged;
+ public event EventHandler QueuededCountChanged;
+ public T Current { get; private set; }
+
+ public IReadOnlyList Queued => _queued;
+ public IReadOnlyList Completed => _completed;
+
+ private readonly List _queued = new();
+ private readonly List _completed = new();
+ private readonly object lockObject = new();
+
+ public T this[int index]
+ {
+ get
+ {
+ lock (lockObject)
+ {
+ if (index < _completed.Count)
+ return _completed[index];
+ index -= _completed.Count;
+
+ if (index == 0 && Current != null) return Current;
+
+ if (Current != null) index--;
+
+ if (index < _queued.Count) return _queued.ElementAt(index);
+
+ throw new IndexOutOfRangeException();
+ }
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ lock (lockObject)
+ {
+ return _queued.Count + _completed.Count + (Current == null ? 0 : 1);
+ }
+ }
+ }
+
+ public int IndexOf(T item)
+ {
+ lock (lockObject)
+ {
+ if (_completed.Contains(item))
+ return _completed.IndexOf(item);
+
+ if (Current == item) return _completed.Count;
+
+ if (_queued.Contains(item))
+ return _queued.IndexOf(item) + (Current is null ? 0 : 1);
+ return -1;
+ }
+ }
+
+ public bool RemoveQueued(T item)
+ {
+ lock (lockObject)
+ {
+ bool removed = _queued.Remove(item);
+ if (removed)
+ QueuededCountChanged?.Invoke(this, _queued.Count);
+ return removed;
+ }
+ }
+
+ public void ClearCurrent()
+ {
+ lock(lockObject)
+ Current = null;
+ }
+
+ public bool RemoveCompleted(T item)
+ {
+ lock (lockObject)
+ {
+ bool removed = _completed.Remove(item);
+ if (removed)
+ CompletedCountChanged?.Invoke(this, _completed.Count);
+ return removed;
+ }
+ }
+
+ public void ClearQueue()
+ {
+ lock (lockObject)
+ {
+ _queued.Clear();
+ QueuededCountChanged?.Invoke(this, 0);
+ }
+ }
+
+ public void ClearCompleted()
+ {
+ lock (lockObject)
+ {
+ _completed.Clear();
+ CompletedCountChanged?.Invoke(this, 0);
+ }
+ }
+
+ public bool Any(Func predicate)
+ {
+ lock (lockObject)
+ {
+ return (Current != null && predicate(Current)) || _completed.Any(predicate) || _queued.Any(predicate);
+ }
+ }
+
+ public void MoveQueuePosition(T item, QueuePosition requestedPosition)
+ {
+ lock (lockObject)
+ {
+ if (_queued.Count == 0 || !_queued.Contains(item)) return;
+
+ if ((requestedPosition == QueuePosition.Fisrt || requestedPosition == QueuePosition.OneUp) && _queued[0] == item)
+ return;
+ if ((requestedPosition == QueuePosition.Last || requestedPosition == QueuePosition.OneDown) && _queued[^1] == item)
+ return;
+
+ int queueIndex = _queued.IndexOf(item);
+
+ if (requestedPosition == QueuePosition.OneUp)
+ {
+ _queued.RemoveAt(queueIndex);
+ _queued.Insert(queueIndex - 1, item);
+ }
+ else if (requestedPosition == QueuePosition.OneDown)
+ {
+ _queued.RemoveAt(queueIndex);
+ _queued.Insert(queueIndex + 1, item);
+ }
+ else if (requestedPosition == QueuePosition.Fisrt)
+ {
+ _queued.RemoveAt(queueIndex);
+ _queued.Insert(0, item);
+ }
+ else
+ {
+ _queued.RemoveAt(queueIndex);
+ _queued.Insert(_queued.Count, item);
+ }
+ }
+ }
+
+ public bool MoveNext()
+ {
+ lock (lockObject)
+ {
+ if (Current != null)
+ {
+ _completed.Add(Current);
+ CompletedCountChanged?.Invoke(this, _completed.Count);
+ }
+ if (_queued.Count == 0)
+ {
+ Current = null;
+ return false;
+ }
+ Current = _queued[0];
+ _queued.RemoveAt(0);
+
+ QueuededCountChanged?.Invoke(this, _queued.Count);
+ return true;
+ }
+ }
+
+ public bool TryPeek(out T item)
+ {
+ lock (lockObject)
+ {
+ if (_queued.Count == 0)
+ {
+ item = null;
+ return false;
+ }
+ item = _queued[0];
+ return true;
+ }
+ }
+
+ public T Peek()
+ {
+ lock (lockObject)
+ {
+ if (_queued.Count == 0) throw new InvalidOperationException("Queue empty");
+ return _queued.Count > 0 ? _queued[0] : default;
+ }
+ }
+
+ public void Enqueue(T item)
+ {
+ lock (lockObject)
+ {
+ _queued.Add(item);
+ QueuededCountChanged?.Invoke(this, _queued.Count);
+ }
+ }
+ }
+}
diff --git a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.Designer.cs b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.Designer.cs
new file mode 100644
index 00000000..77f793fc
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.Designer.cs
@@ -0,0 +1,59 @@
+namespace LibationWinForms.ProcessQueue
+{
+ partial class VirtualFlowControl
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.panel1 = new System.Windows.Forms.Panel();
+ this.SuspendLayout();
+ //
+ // panel1
+ //
+ this.panel1.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.panel1.BackColor = System.Drawing.SystemColors.ControlDark;
+ this.panel1.Location = new System.Drawing.Point(0, 0);
+ this.panel1.Name = "panel1";
+ this.panel1.Size = new System.Drawing.Size(377, 505);
+ this.panel1.TabIndex = 0;
+ //
+ // VirtualFlowControl
+ //
+ this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.Controls.Add(this.panel1);
+ this.Name = "VirtualFlowControl";
+ this.Size = new System.Drawing.Size(377, 505);
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Panel panel1;
+ }
+}
diff --git a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs
new file mode 100644
index 00000000..d031d191
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace LibationWinForms.ProcessQueue
+{
+
+ internal delegate void RequestDataDelegate(int queueIndex, int numVisible, IReadOnlyList panelsToFill);
+ internal delegate void ControlButtonClickedDelegate(int queueIndex, string buttonName, ProcessBookControl panelClicked);
+ internal partial class VirtualFlowControl : UserControl
+ {
+ ///
+ /// Triggered when the needs to update the displayed s
+ ///
+ public event RequestDataDelegate RequestData;
+ ///
+ /// Triggered when one of the 's buttons has been clicked
+ ///
+ public event ControlButtonClickedDelegate ButtonClicked;
+
+ #region Dynamic Properties
+
+ ///
+ /// The number of virtual s in the
+ ///
+ public int VirtualControlCount
+ {
+ get => _virtualControlCount;
+ set
+ {
+ if (_virtualControlCount == 0)
+ vScrollBar1.Value = 0;
+
+ _virtualControlCount = value;
+ AdjustScrollBar();
+ DoVirtualScroll();
+ }
+ }
+
+ private int _virtualControlCount;
+
+ int ScrollValue => Math.Max(vScrollBar1.Value, 0);
+ ///
+ /// Amount the control moves with a small scroll change
+ ///
+ private int SmallScrollChange => VirtualControlHeight * SMALL_SCROLL_CHANGE_MULTIPLE;
+ ///
+ /// Amount the control moves with a large scroll change. Equal to the number of whole s in the panel, less 1.
+ ///
+ private int LargeScrollChange => Math.Max(DisplayHeight / VirtualControlHeight - 1, SMALL_SCROLL_CHANGE_MULTIPLE) * VirtualControlHeight;
+ ///
+ /// Virtual height of all virtual controls within this
+ ///
+ private int VirtualHeight => (VirtualControlCount + NUM_BLANK_SPACES_AT_BOTTOM) * VirtualControlHeight - DisplayHeight + 2 * TopMargin;
+ ///
+ /// Index of the first virtual
+ ///
+ private int FirstVisibleVirtualIndex => ScrollValue / VirtualControlHeight;
+ ///
+ /// The display height of this
+ ///
+ private int DisplayHeight => DisplayRectangle.Height;
+
+ #endregion
+
+ #region Instance variables
+
+ ///
+ /// The total height, inclusing margins, of the repeated
+ ///
+ private readonly int VirtualControlHeight;
+ ///
+ /// Margin between the top and the top of the Panel, and the bottom and the bottom of the panel
+ ///
+ private readonly int TopMargin;
+
+ private readonly VScrollBar vScrollBar1;
+ private readonly List BookControls = new();
+
+ #endregion
+
+ #region Global behavior settings
+
+ ///
+ /// Total number of actual controls added to the panel. 23 is sufficient up to a 4k monitor height.
+ ///
+ private const int NUM_ACTUAL_CONTROLS = 23;
+ ///
+ /// Multiple of that is moved for each small scroll change
+ ///
+ private const int SMALL_SCROLL_CHANGE_MULTIPLE = 1;
+ ///
+ /// Amount of space at the bottom of the , in multiples of
+ ///
+ private const int NUM_BLANK_SPACES_AT_BOTTOM = 2;
+
+ #endregion
+
+ public VirtualFlowControl()
+ {
+ InitializeComponent();
+
+ vScrollBar1 = new VScrollBar
+ {
+ Minimum = 0,
+ Value = 0,
+ Dock = DockStyle.Right
+ };
+ Controls.Add(vScrollBar1);
+
+ vScrollBar1.Scroll += (_, s) => SetScrollPosition(s.NewValue);
+ panel1.Width -= vScrollBar1.Width + panel1.Margin.Right;
+ panel1.Resize += (_, _) =>
+ {
+ AdjustScrollBar();
+ DoVirtualScroll();
+ };
+
+
+ var control = InitControl(0);
+ VirtualControlHeight = control.Height + control.Margin.Top + control.Margin.Bottom;
+ TopMargin = control.Margin.Top;
+
+ BookControls.Add(control);
+ panel1.Controls.Add(control);
+
+ if (DesignMode)
+ return;
+
+ for (int i = 1; i < NUM_ACTUAL_CONTROLS; i++)
+ {
+ control = InitControl(VirtualControlHeight * i);
+ BookControls.Add(control);
+ panel1.Controls.Add(control);
+ }
+
+ vScrollBar1.SmallChange = SmallScrollChange;
+ panel1.Height += NUM_BLANK_SPACES_AT_BOTTOM * VirtualControlHeight;
+ }
+
+ private ProcessBookControl InitControl(int locationY)
+ {
+ var control = new ProcessBookControl();
+ control.Location = new Point(control.Margin.Left, locationY + control.Margin.Top);
+ control.Width = panel1.ClientRectangle.Width - control.Margin.Left - control.Margin.Right;
+ control.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
+
+ control.cancelBtn.Click += ControlButton_Click;
+ control.moveFirstBtn.Click += ControlButton_Click;
+ control.moveUpBtn.Click += ControlButton_Click;
+ control.moveDownBtn.Click += ControlButton_Click;
+ control.moveLastBtn.Click += ControlButton_Click;
+ return control;
+ }
+
+ ///
+ /// Handles all button clicks from all , detects which one sent the click, and fires to notify the model of the click
+ ///
+ private void ControlButton_Click(object sender, EventArgs e)
+ {
+ Control button = sender as Control;
+ Control form = button.Parent;
+ while (form is not ProcessBookControl)
+ form = form.Parent;
+
+ int clickedIndex = BookControls.IndexOf((ProcessBookControl)form);
+
+ ButtonClicked?.Invoke(FirstVisibleVirtualIndex + clickedIndex, button.Name, BookControls[clickedIndex]);
+ }
+
+ ///
+ /// Adjusts the max width and enabled status based on the and the
+ ///
+ private void AdjustScrollBar()
+ {
+ int maxFullVisible = DisplayHeight / VirtualControlHeight;
+
+ if (VirtualControlCount <= maxFullVisible)
+ {
+ vScrollBar1.Enabled = false;
+ vScrollBar1.Value = 0;
+
+ for (int i = VirtualControlCount; i < NUM_ACTUAL_CONTROLS; i++)
+ BookControls[i].Visible = false;
+ }
+ else
+ {
+ vScrollBar1.Enabled = true;
+ vScrollBar1.LargeChange = LargeScrollChange;
+
+ //https://stackoverflow.com/a/2882878/3335599
+ int newMaximum = VirtualHeight + vScrollBar1.LargeChange - 1;
+ if (newMaximum < vScrollBar1.Maximum)
+ vScrollBar1.Value = Math.Max(vScrollBar1.Value - (vScrollBar1.Maximum - newMaximum), 0);
+ vScrollBar1.Maximum = newMaximum;
+ }
+ }
+
+ ///
+ /// Calculated the virtual controls that are in view at the currrent scroll position and windows size,
+ /// positions to simulate scroll activity, then fires to notify the model to update all visible controls
+ ///
+ private void DoVirtualScroll()
+ {
+ int firstVisible = FirstVisibleVirtualIndex;
+
+ int position = ScrollValue % VirtualControlHeight;
+ panel1.Location = new Point(0, -position);
+
+ int numVisible = DisplayHeight / VirtualControlHeight;
+
+ if (DisplayHeight % VirtualControlHeight != 0)
+ numVisible++;
+
+ numVisible = Math.Min(numVisible, VirtualControlCount);
+ numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
+
+ RequestData?.Invoke(firstVisible, numVisible, BookControls);
+
+ for (int i = 0; i < BookControls.Count; i++)
+ BookControls[i].Visible = i < numVisible;
+ }
+
+ ///
+ /// Set scroll value to an integral multiple of
+ ///
+ private void SetScrollPosition(int value)
+ {
+ int newPos = (int)Math.Round((double)value / SmallScrollChange) * SmallScrollChange;
+ if (vScrollBar1.Value != newPos)
+ {
+ //https://stackoverflow.com/a/2882878/3335599
+ vScrollBar1.Value = Math.Min(newPos, vScrollBar1.Maximum - vScrollBar1.LargeChange + 1);
+ DoVirtualScroll();
+ }
+ }
+
+
+ private const int WM_MOUSEWHEEL = 522;
+ private const int WHEEL_DELTA = 120;
+ protected override void WndProc(ref Message m)
+ {
+ //Capture mouse wheel movement and interpret it as a scroll event
+ if (m.Msg == WM_MOUSEWHEEL)
+ {
+ //https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousewheel
+ int wheelDelta = -(short)(((ulong)m.WParam) >> 16 & ushort.MaxValue);
+
+ int numSmallPositionMoves = Math.Abs(wheelDelta) / WHEEL_DELTA;
+
+ int scrollDelta = Math.Sign(wheelDelta) * numSmallPositionMoves * SmallScrollChange;
+
+ int newScrollPosition;
+
+ if (scrollDelta > 0)
+ newScrollPosition = Math.Min(vScrollBar1.Value + scrollDelta, vScrollBar1.Maximum);
+ else
+ newScrollPosition = Math.Max(vScrollBar1.Value + scrollDelta, vScrollBar1.Minimum);
+
+ SetScrollPosition(newScrollPosition);
+ }
+
+ base.WndProc(ref m);
+ }
+ }
+}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.resx b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.resx
similarity index 93%
rename from Source/LibationWinForms/ProcessQueue/ProcessBookQueue.resx
rename to Source/LibationWinForms/ProcessQueue/VirtualFlowControl.resx
index 5cb320f3..f298a7be 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookQueue.resx
+++ b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.resx
@@ -57,7 +57,4 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
- 17, 17
-
\ No newline at end of file
diff --git a/Source/LibationWinForms/grid/ProductsGrid.cs b/Source/LibationWinForms/grid/ProductsGrid.cs
index 93caca4d..b088262e 100644
--- a/Source/LibationWinForms/grid/ProductsGrid.cs
+++ b/Source/LibationWinForms/grid/ProductsGrid.cs
@@ -36,8 +36,10 @@ namespace LibationWinForms
// VS has improved since then with .net6+ but I haven't checked again
#endregion
+
public partial class ProductsGrid : UserControl
{
+ public event EventHandler LiberateClicked;
/// Number of visible rows has changed
public event EventHandler VisibleCountChanged;
@@ -76,7 +78,7 @@ namespace LibationWinForms
return;
if (e.ColumnIndex == liberateGVColumn.Index)
- await Liberate_Click(getGridEntry(e.RowIndex));
+ Liberate_Click(getGridEntry(e.RowIndex));
else if (e.ColumnIndex == tagAndDetailsGVColumn.Index)
Details_Click(getGridEntry(e.RowIndex));
else if (e.ColumnIndex == descriptionGVColumn.Index)
@@ -128,7 +130,7 @@ namespace LibationWinForms
displayWindow.Show(this);
}
- private static async Task Liberate_Click(GridEntry liveGridEntry)
+ private void Liberate_Click(GridEntry liveGridEntry)
{
var libraryBook = liveGridEntry.LibraryBook;
@@ -144,8 +146,7 @@ namespace LibationWinForms
return;
}
- // else: liberate
- await liveGridEntry.DownloadBook();
+ LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
}
private static void Details_Click(GridEntry liveGridEntry)