Move ProcessQueueViewModel logic into LibationUiBase

Fix UI bug in classic when queue is in popped-out mode.
This commit is contained in:
MBucari 2025-07-15 22:01:54 -06:00
parent 1cf889eed7
commit 4dab16837e
12 changed files with 723 additions and 828 deletions

View File

@ -1,114 +1,17 @@
using ApplicationServices; using Avalonia.Collections;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading; using Avalonia.Threading;
using DataLayer; using DataLayer;
using LibationFileManager; using LibationFileManager;
using LibationUiBase;
using LibationUiBase.Forms;
using LibationUiBase.ProcessQueue; using LibationUiBase.ProcessQueue;
using ReactiveUI;
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
#nullable enable #nullable enable
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels;
public class ProcessQueueViewModel : ProcessQueueViewModelBase
{ {
public override void WriteLine(string text)
public class ProcessQueueViewModel : ViewModelBase, ILogForm, IProcessQueue
{
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
public TrackedQueue<ProcessBookViewModel> Queue { get; }
public ProcessBookViewModel? SelectedItem { get; set; }
public Task? QueueRunner { get; private set; }
public bool Running => !QueueRunner?.IsCompleted ?? false;
private readonly LogMe Logger;
public ProcessQueueViewModel()
{
Logger = LogMe.RegisterForm(this);
Queue = new(Items);
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
if (Design.IsDesignMode)
_ = Configuration.Instance.LibationFiles;
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}
private int _completedCount;
private int _errorCount;
private int _queuedCount;
private string? _runningTime;
private bool _progressBarVisible;
private decimal _speedLimit;
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
public string? RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
public bool AnyCompleted => CompletedCount > 0;
public bool AnyQueued => QueuedCount > 0;
public bool AnyErrors => ErrorCount > 0;
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
public decimal SpeedLimit
{
get
{
return _speedLimit;
}
set
{
var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
var config = Configuration.Instance;
config.DownloadSpeedLimit = newValue;
_speedLimit
= config.DownloadSpeedLimit <= newValue ? value
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
: 0;
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
SpeedLimitIncrement = _speedLimit > 100 ? 10
: _speedLimit > 10 ? 1
: _speedLimit > 1 ? 0.1m
: 0.01m;
Dispatcher.UIThread.Invoke(() =>
{
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
this.RaisePropertyChanged();
});
}
}
public decimal SpeedLimitIncrement { get; private set; }
private void Queue_CompletedCountChanged(object? sender, int e)
{
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
ErrorCount = errCount;
CompletedCount = completeCount;
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
}
private void Queue_QueuededCountChanged(object? sender, int cueCount)
{
QueuedCount = cueCount;
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
}
public void WriteLine(string text)
{ {
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
LogEntries.Add(new() LogEntries.Add(new()
@ -118,165 +21,20 @@ namespace LibationAvalonia.ViewModels
})); }));
} }
public ProcessQueueViewModel() : base(CreateEmptyList())
#region Add Books to Queue
private bool isBookInQueue(LibraryBook libraryBook)
{ {
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase>
if (entry == null) ?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
} }
public bool RemoveCompleted(LibraryBook libraryBook) public AvaloniaList<ProcessBookViewModelBase> Items { get; }
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModel entry protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook)
&& entry.Status is ProcessBookStatus.Completed => new ProcessBookViewModel(libraryBook, Logger);
&& Queue.RemoveCompleted(entry);
public void AddDownloadPdf(IEnumerable<LibraryBook> entries) private static AvaloniaList<ProcessBookViewModelBase> CreateEmptyList()
{ {
List<ProcessBookViewModel> procs = new(); if (Design.IsDesignMode)
foreach (var entry in entries) _ = Configuration.Instance.LibationFiles;
{ return new AvaloniaList<ProcessBookViewModelBase>();
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.AddDownloadDecryptBook();
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddConvertMp3(IEnumerable<LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.AddConvertToMp3();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddToQueue(IEnumerable<ProcessBookViewModel> pbook)
{
Dispatcher.UIThread.Invoke(() =>
{
Queue.Enqueue(pbook);
if (!Running)
QueueRunner = QueueLoop();
});
}
#endregion
DateTime StartingTime;
private async Task QueueLoop()
{
try
{
Serilog.Log.Logger.Information("Begin processing queue");
RunningTime = string.Empty;
ProgressBarVisible = true;
StartingTime = DateTime.Now;
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
bool shownServiceOutageMessage = false;
while (Queue.MoveNext())
{
if (Queue.Current is not ProcessBookViewModel nextBook)
{
Serilog.Log.Logger.Information("Current queue item is empty.");
continue;
}
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
var result = await nextBook.ProcessOneAsync();
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
if (result == ProcessBookResult.ValidationFail)
Queue.ClearCurrent();
else if (result == ProcessBookResult.FailedAbort)
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
{
await MessageBox.Show(@$"
You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
",
"Possible Interruption of Service",
MessageBoxButtons.OK,
MessageBoxIcon.Asterisk);
shownServiceOutageMessage = true;
}
}
Serilog.Log.Logger.Information("Completed processing queue");
Queue_CompletedCountChanged(this, 0);
ProgressBarVisible = false;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
}
}
private void CounterTimer_Tick(object? state)
{
string timeToStr(TimeSpan time)
{
string minsSecs = $"{time:mm\\:ss}";
if (time.TotalHours >= 1)
return $"{time.TotalHours:F0}:{minsSecs}";
return minsSecs;
}
RunningTime = timeToStr(DateTime.Now - StartingTime);
}
}
public class LogEntry
{
public DateTime LogDate { get; init; }
public string LogDateString => LogDate.ToShortTimeString();
public string? LogMessage { get; init; }
} }
} }

View File

@ -17,7 +17,7 @@ namespace LibationAvalonia.Views
{ {
public partial class ProcessQueueControl : UserControl public partial class ProcessQueueControl : UserControl
{ {
private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue; private TrackedQueue<ProcessBookViewModelBase>? Queue => _viewModel?.Queue;
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel; private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl() public ProcessQueueControl()

View File

@ -1,82 +0,0 @@
using DataLayer;
using System.Collections.Generic;
using System.Linq;
namespace LibationUiBase;
public interface IProcessQueue
{
bool RemoveCompleted(LibraryBook libraryBook);
void AddDownloadPdf(IEnumerable<LibraryBook> entries);
void AddConvertMp3(IEnumerable<LibraryBook> entries);
void AddDownloadDecrypt(IEnumerable<LibraryBook> entries);
}
public static class ProcessQueueExtensions
{
public static bool QueueDownloadPdf(this IProcessQueue queue, IList<LibraryBook> libraryBooks)
{
var needsPdf = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated).ToArray();
if (needsPdf.Length > 0)
{
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
queue.AddDownloadPdf(needsPdf);
return true;
}
return false;
}
public static bool QueueConvertToMp3(this IProcessQueue queue, IList<LibraryBook> libraryBooks)
{
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
if (preLiberated.Length > 0)
{
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
queue.AddConvertMp3(preLiberated);
return true;
}
return false;
}
public static bool QueueDownloadDecrypt(this IProcessQueue queue, IList<LibraryBook> libraryBooks)
{
if (libraryBooks.Count == 1)
{
var item = libraryBooks[0];
if (item.AbsentFromLastScan)
return false;
else if(item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
{
queue.RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
queue.AddDownloadDecrypt([item]);
return true;
}
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
queue.RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
queue.AddDownloadPdf([item]);
return true;
}
}
else
{
var toLiberate
= libraryBooks
.Where(x => !x.AbsentFromLastScan && x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
.ToArray();
if (toLiberate.Length > 0)
{
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
queue.AddDownloadDecrypt(toLiberate);
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,324 @@
using DataLayer;
using LibationFileManager;
using LibationUiBase.Forms;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ApplicationServices;
using System.Threading.Tasks;
#nullable enable
namespace LibationUiBase.ProcessQueue;
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
{
public abstract void WriteLine(string text);
protected abstract ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook);
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public TrackedQueue<ProcessBookViewModelBase> Queue { get; }
public ProcessBookViewModelBase? SelectedItem { get; set; }
public Task? QueueRunner { get; private set; }
public bool Running => !QueueRunner?.IsCompleted ?? false;
protected readonly LogMe Logger;
public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList)
{
Logger = LogMe.RegisterForm(this);
Queue = new(underlyingList);
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}
private int _completedCount;
private int _errorCount;
private int _queuedCount;
private string? _runningTime;
private bool _progressBarVisible;
private decimal _speedLimit;
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } }
public int ErrorCount { get => _errorCount; private set { RaiseAndSetIfChanged(ref _errorCount, value); RaisePropertyChanged(nameof(AnyErrors)); } }
public string? RunningTime { get => _runningTime; set => RaiseAndSetIfChanged(ref _runningTime, value); }
public bool ProgressBarVisible { get => _progressBarVisible; set => RaiseAndSetIfChanged(ref _progressBarVisible, value); }
public bool AnyCompleted => CompletedCount > 0;
public bool AnyQueued => QueuedCount > 0;
public bool AnyErrors => ErrorCount > 0;
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
public decimal SpeedLimit
{
get
{
return _speedLimit;
}
set
{
var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
var config = Configuration.Instance;
config.DownloadSpeedLimit = newValue;
_speedLimit
= config.DownloadSpeedLimit <= newValue ? value
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
: 0;
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
SpeedLimitIncrement = _speedLimit > 100 ? 10
: _speedLimit > 10 ? 1
: _speedLimit > 1 ? 0.1m
: 0.01m;
RaisePropertyChanged(nameof(SpeedLimitIncrement));
RaisePropertyChanged(nameof(SpeedLimit));
}
}
public decimal SpeedLimitIncrement { get; private set; }
private void Queue_CompletedCountChanged(object? sender, int e)
{
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
ErrorCount = errCount;
CompletedCount = completeCount;
RaisePropertyChanged(nameof(Progress));
}
private void Queue_QueuededCountChanged(object? sender, int cueCount)
{
QueuedCount = cueCount;
RaisePropertyChanged(nameof(Progress));
}
#region Add Books to Queue
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
{
var needsPdf = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated).ToArray();
if (needsPdf.Length > 0)
{
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
AddDownloadPdf(needsPdf);
return true;
}
return false;
}
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks)
{
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
if (preLiberated.Length > 0)
{
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
AddConvertMp3(preLiberated);
return true;
}
return false;
}
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks)
{
if (libraryBooks.Count == 1)
{
var item = libraryBooks[0];
if (item.AbsentFromLastScan)
return false;
else if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
{
RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
AddDownloadDecrypt([item]);
return true;
}
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
AddDownloadPdf([item]);
return true;
}
}
else
{
var toLiberate
= libraryBooks
.Where(x => !x.AbsentFromLastScan && x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
.ToArray();
if (toLiberate.Length > 0)
{
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
AddDownloadDecrypt(toLiberate);
return true;
}
}
return false;
}
private bool isBookInQueue(LibraryBook libraryBook)
{
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
if (entry == null)
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
}
private bool RemoveCompleted(LibraryBook libraryBook)
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry
&& entry.Status is ProcessBookStatus.Completed
&& Queue.RemoveCompleted(entry);
private void AddDownloadPdf(IEnumerable<LibraryBook> entries)
{
List<ProcessBookViewModelBase> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
var pbook = CreateNewBook(entry);
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
private void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
{
List<ProcessBookViewModelBase> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
var pbook = CreateNewBook(entry);
pbook.AddDownloadDecryptBook();
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
private void AddConvertMp3(IEnumerable<LibraryBook> entries)
{
List<ProcessBookViewModelBase> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
var pbook = CreateNewBook(entry);
pbook.AddConvertToMp3();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook)
{
Invoke(() =>
{
Queue.Enqueue(pbook);
if (!Running)
QueueRunner = QueueLoop();
});
}
#endregion
private DateTime StartingTime;
private async Task QueueLoop()
{
try
{
Serilog.Log.Logger.Information("Begin processing queue");
RunningTime = string.Empty;
ProgressBarVisible = true;
StartingTime = DateTime.Now;
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
bool shownServiceOutageMessage = false;
while (Queue.MoveNext())
{
if (Queue.Current is not ProcessBookViewModelBase nextBook)
{
Serilog.Log.Logger.Information("Current queue item is empty.");
continue;
}
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
var result = await nextBook.ProcessOneAsync();
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
if (result == ProcessBookResult.ValidationFail)
Queue.ClearCurrent();
else if (result == ProcessBookResult.FailedAbort)
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
{
await MessageBoxBase.Show(@$"
You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
",
"Possible Interruption of Service",
MessageBoxButtons.OK,
MessageBoxIcon.Asterisk);
shownServiceOutageMessage = true;
}
}
Serilog.Log.Logger.Information("Completed processing queue");
Queue_CompletedCountChanged(this, 0);
ProgressBarVisible = false;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
}
}
private void CounterTimer_Tick(object? state)
{
string timeToStr(TimeSpan time)
{
string minsSecs = $"{time:mm\\:ss}";
if (time.TotalHours >= 1)
return $"{time.TotalHours:F0}:{minsSecs}";
return minsSecs;
}
RunningTime = timeToStr(DateTime.Now - StartingTime);
}
}
public class LogEntry
{
public DateTime LogDate { get; init; }
public string LogDateString => LogDate.ToShortTimeString();
public string? LogMessage { get; init; }
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
#nullable enable
namespace LibationUiBase namespace LibationUiBase
{ {
public enum QueuePosition public enum QueuePosition
@ -34,10 +35,10 @@ namespace LibationUiBase
*/ */
public class TrackedQueue<T> where T : class public class TrackedQueue<T> where T : class
{ {
public event EventHandler<int> CompletedCountChanged; public event EventHandler<int>? CompletedCountChanged;
public event EventHandler<int> QueuededCountChanged; public event EventHandler<int>? QueuededCountChanged;
public T Current { get; private set; } public T? Current { get; private set; }
public IReadOnlyList<T> Queued => _queued; public IReadOnlyList<T> Queued => _queued;
public IReadOnlyList<T> Completed => _completed; public IReadOnlyList<T> Completed => _completed;
@ -46,9 +47,10 @@ namespace LibationUiBase
private readonly List<T> _completed = new(); private readonly List<T> _completed = new();
private readonly object lockObject = new(); private readonly object lockObject = new();
private readonly ICollection<T> _underlyingList; private readonly ICollection<T>? _underlyingList;
public ICollection<T>? UnderlyingList => _underlyingList;
public TrackedQueue(ICollection<T> underlyingList = null) public TrackedQueue(ICollection<T>? underlyingList = null)
{ {
_underlyingList = underlyingList; _underlyingList = underlyingList;
} }
@ -169,7 +171,7 @@ namespace LibationUiBase
} }
} }
public T FirstOrDefault(Func<T, bool> predicate) public T? FirstOrDefault(Func<T, bool> predicate)
{ {
lock (lockObject) lock (lockObject)
{ {

View File

@ -17,7 +17,7 @@ namespace LibationWinForms
try try
{ {
var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray()); var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
if (processBookQueue1.QueueDownloadDecrypt(unliberated)) if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
SetQueueCollapseState(false); SetQueueCollapseState(false);
} }
catch (Exception ex) catch (Exception ex)
@ -28,7 +28,7 @@ namespace LibationWinForms
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e) private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
{ {
if (processBookQueue1.QueueDownloadPdf(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()))) if (processBookQueue1.ViewModel.QueueDownloadPdf(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
SetQueueCollapseState(false); SetQueueCollapseState(false);
} }
@ -42,7 +42,7 @@ namespace LibationWinForms
"Convert all M4b => Mp3?", "Convert all M4b => Mp3?",
MessageBoxButtons.YesNo, MessageBoxButtons.YesNo,
MessageBoxIcon.Warning); MessageBoxIcon.Warning);
if (result == DialogResult.Yes && processBookQueue1.QueueConvertToMp3(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()))) if (result == DialogResult.Yes && processBookQueue1.ViewModel.QueueConvertToMp3(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
SetQueueCollapseState(false); SetQueueCollapseState(false);
} }
} }

View File

@ -16,7 +16,7 @@ namespace LibationWinForms
int WidthChange = 0; int WidthChange = 0;
private void Configure_ProcessQueue() private void Configure_ProcessQueue()
{ {
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut; processBookQueue1.PopoutButton.Click += ProcessBookQueue1_PopOut;
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth; WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
int width = this.Width; int width = this.Width;
@ -29,7 +29,7 @@ namespace LibationWinForms
{ {
try try
{ {
if (processBookQueue1.QueueDownloadDecrypt(libraryBooks)) if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks))
SetQueueCollapseState(false); SetQueueCollapseState(false);
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists()) else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists())
{ {
@ -54,7 +54,7 @@ namespace LibationWinForms
{ {
Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook); Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook);
if (processBookQueue1.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray())) if (processBookQueue1.ViewModel.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray()))
SetQueueCollapseState(false); SetQueueCollapseState(false);
} }
catch (Exception ex) catch (Exception ex)
@ -67,7 +67,7 @@ namespace LibationWinForms
{ {
try try
{ {
if (processBookQueue1.QueueConvertToMp3(libraryBooks)) if (processBookQueue1.ViewModel.QueueConvertToMp3(libraryBooks))
SetQueueCollapseState(false); SetQueueCollapseState(false);
} }
catch (Exception ex) catch (Exception ex)
@ -87,10 +87,14 @@ namespace LibationWinForms
} }
else if (!collapsed && splitContainer1.Panel2Collapsed) else if (!collapsed && splitContainer1.Panel2Collapsed)
{ {
if (!processBookQueue1.PopoutButton.Visible)
//Queue is in popout mode. Do nothing.
return;
Width += WidthChange; Width += WidthChange;
splitContainer1.Panel2.Controls.Add(processBookQueue1); splitContainer1.Panel2.Controls.Add(processBookQueue1);
splitContainer1.Panel2Collapsed = false; splitContainer1.Panel2Collapsed = false;
processBookQueue1.popoutBtn.Visible = true; processBookQueue1.PopoutButton.Visible = true;
} }
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed)); Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
@ -110,7 +114,7 @@ namespace LibationWinForms
dockForm.FormClosing += DockForm_FormClosing; dockForm.FormClosing += DockForm_FormClosing;
splitContainer1.Panel2.Controls.Remove(processBookQueue1); splitContainer1.Panel2.Controls.Remove(processBookQueue1);
splitContainer1.Panel2Collapsed = true; splitContainer1.Panel2Collapsed = true;
processBookQueue1.popoutBtn.Visible = false; processBookQueue1.PopoutButton.Visible = false;
dockForm.PassControl(processBookQueue1); dockForm.PassControl(processBookQueue1);
dockForm.Show(); dockForm.Show();
this.Width -= dockForm.WidthChange; this.Width -= dockForm.WidthChange;
@ -127,7 +131,7 @@ namespace LibationWinForms
this.Width += dockForm.WidthChange; this.Width += dockForm.WidthChange;
splitContainer1.Panel2.Controls.Add(dockForm.RegainControl()); splitContainer1.Panel2.Controls.Add(dockForm.RegainControl());
splitContainer1.Panel2Collapsed = false; splitContainer1.Panel2Collapsed = false;
processBookQueue1.popoutBtn.Visible = true; processBookQueue1.PopoutButton.Visible = true;
dockForm.SaveSizeAndLocation(Configuration.Instance); dockForm.SaveSizeAndLocation(Configuration.Instance);
this.Focus(); this.Focus();
toggleQueueHideBtn.Visible = true; toggleQueueHideBtn.Visible = true;

View File

@ -59,7 +59,7 @@ namespace LibationWinForms
{ {
try try
{ {
if (processBookQueue1.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray())) if (processBookQueue1.ViewModel.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray()))
SetQueueCollapseState(false); SetQueueCollapseState(false);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -11,6 +11,4 @@ public class ProcessBookViewModel : ProcessBookViewModelBase
protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize) protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
=> WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80); => WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80);
public string BookText => $"{Title}\r\nBy {Author}\r\nNarrated by {Narrator}";
} }

View File

@ -55,7 +55,6 @@
this.panel2 = new System.Windows.Forms.Panel(); this.panel2 = new System.Windows.Forms.Panel();
this.logCopyBtn = new System.Windows.Forms.Button(); this.logCopyBtn = new System.Windows.Forms.Button();
this.clearLogBtn = new System.Windows.Forms.Button(); this.clearLogBtn = new System.Windows.Forms.Button();
this.counterTimer = new System.Windows.Forms.Timer(this.components);
this.statusStrip1.SuspendLayout(); this.statusStrip1.SuspendLayout();
this.tabControl1.SuspendLayout(); this.tabControl1.SuspendLayout();
this.tabPage1.SuspendLayout(); this.tabPage1.SuspendLayout();
@ -329,11 +328,6 @@
this.clearLogBtn.UseVisualStyleBackColor = true; this.clearLogBtn.UseVisualStyleBackColor = true;
this.clearLogBtn.Click += new System.EventHandler(this.clearLogBtn_Click); this.clearLogBtn.Click += new System.EventHandler(this.clearLogBtn_Click);
// //
// counterTimer
//
this.counterTimer.Interval = 950;
this.counterTimer.Tick += new System.EventHandler(this.CounterTimer_Tick);
//
// ProcessQueueControl // ProcessQueueControl
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
@ -377,7 +371,6 @@
private System.Windows.Forms.Panel panel3; private System.Windows.Forms.Panel panel3;
private System.Windows.Forms.Panel panel4; private System.Windows.Forms.Panel panel4;
private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl; private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl;
private System.Windows.Forms.Timer counterTimer;
private System.Windows.Forms.DataGridView logDGV; private System.Windows.Forms.DataGridView logDGV;
private System.Windows.Forms.DataGridViewTextBoxColumn timestampColumn; private System.Windows.Forms.DataGridViewTextBoxColumn timestampColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn logEntryColumn; private System.Windows.Forms.DataGridViewTextBoxColumn logEntryColumn;

View File

@ -1,51 +1,30 @@
using System; using LibationFileManager;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using ApplicationServices;
using LibationFileManager;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
namespace LibationWinForms.ProcessQueue #nullable enable
{ namespace LibationWinForms.ProcessQueue;
internal partial class ProcessQueueControl : UserControl, ILogForm, IProcessQueue
{
private TrackedQueue<ProcessBookViewModel> 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 internal partial class ProcessQueueControl : UserControl
{ {
set public ProcessQueueViewModel ViewModel { get; } = new();
public ToolStripButton PopoutButton { get; } = new()
{ {
completedNumberLbl.Text = value.ToString(); DisplayStyle = ToolStripItemDisplayStyle.Text,
completedNumberLbl.Visible = value > 0; Name = nameof(PopoutButton),
} Text = "Pop Out",
} TextAlign = ContentAlignment.MiddleCenter,
Alignment = ToolStripItemAlignment.Right,
public Task QueueRunner { get; private set; } Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
public bool Running => !QueueRunner?.IsCompleted ?? false; };
public ToolStripButton popoutBtn = new();
public ProcessQueueControl() public ProcessQueueControl()
{ {
@ -53,257 +32,108 @@ namespace LibationWinForms.ProcessQueue
var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps; numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
statusStrip1.Items.Add(PopoutButton);
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);
Logger = LogMe.RegisterForm(this);
virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData; virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData;
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked; virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
Queue.QueuededCountChanged += Queue_QueuededCountChanged; ViewModel.LogWritten += (_, text) => WriteLine(text);
Queue.CompletedCountChanged += Queue_CompletedCountChanged; ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
ViewModel.BookPropertyChanged += ProcessBook_PropertyChanged;
Load += ProcessQueueControl_Load; Load += ProcessQueueControl_Load;
} }
private void ProcessQueueControl_Load(object sender, EventArgs e) private void ProcessQueueControl_Load(object? sender, EventArgs e)
{ {
if (DesignMode) return; if (DesignMode) return;
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
runningTimeLbl.Text = string.Empty;
QueuedCount = 0;
ErrorCount = 0;
CompletedCount = 0;
}
private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
{
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
if (entry == null)
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
}
public bool RemoveCompleted(DataLayer.LibraryBook libraryBook)
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModel entry
&& entry.Status is ProcessBookStatus.Completed
&& Queue.RemoveCompleted(entry);
public void AddDownloadPdf(IEnumerable<DataLayer.LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.PropertyChanged += Pbook_DataAvailable;
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddDownloadDecrypt(IEnumerable<DataLayer.LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.PropertyChanged += Pbook_DataAvailable;
pbook.AddDownloadDecryptBook();
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddConvertMp3(IEnumerable<DataLayer.LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.PropertyChanged += Pbook_DataAvailable;
pbook.AddConvertToMp3();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
private void AddToQueue(IEnumerable<ProcessBookViewModel> pbook)
{
if (!IsHandleCreated)
CreateControl();
BeginInvoke(() =>
{
Queue.Enqueue(pbook);
if (!Running)
QueueRunner = QueueLoop();
});
}
DateTime StartingTime;
private async Task QueueLoop()
{
try
{
Serilog.Log.Logger.Information("Begin processing queue");
StartingTime = DateTime.Now;
counterTimer.Start();
bool shownServiceOutageMessage = false;
while (Queue.MoveNext())
{
var nextBook = Queue.Current;
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
var result = await nextBook.ProcessOneAsync();
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
if (result == ProcessBookResult.ValidationFail)
Queue.ClearCurrent();
else if (result == ProcessBookResult.FailedAbort)
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
nextBook.LibraryBook.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
{
MessageBox.Show(@$"
You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
",
"Possible Interruption of Service",
MessageBoxButtons.OK,
MessageBoxIcon.Asterisk);
shownServiceOutageMessage = true;
}
}
Serilog.Log.Logger.Information("Completed processing queue");
Queue_CompletedCountChanged(this, 0);
counterTimer.Stop();
virtualFlowControl2.VirtualControlCount = Queue.Count;
UpdateAllControls();
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
}
} }
public void WriteLine(string text) public void WriteLine(string text)
{ {
if (IsDisposed) return; if (!IsDisposed)
logDGV.Rows.Add(DateTime.Now, text.Trim());
var timeStamp = DateTime.Now;
Invoke(() => logDGV.Rows.Add(timeStamp, text.Trim()));
} }
#region Control event handlers private async void cancelAllBtn_Click(object? sender, EventArgs e)
private void Queue_CompletedCountChanged(object sender, int e)
{ {
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail); ViewModel.Queue.ClearQueue();
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success); if (ViewModel.Queue.Current is not null)
await ViewModel.Queue.Current.CancelAsync();
ErrorCount = errCount; virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
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 async void cancelAllBtn_Click(object sender, EventArgs e)
{
Queue.ClearQueue();
if (Queue.Current is not null)
await Queue.Current.CancelAsync();
virtualFlowControl2.VirtualControlCount = Queue.Count;
UpdateAllControls(); UpdateAllControls();
} }
private void btnClearFinished_Click(object sender, EventArgs e) private void btnClearFinished_Click(object? sender, EventArgs e)
{ {
Queue.ClearCompleted(); ViewModel.Queue.ClearCompleted();
virtualFlowControl2.VirtualControlCount = Queue.Count; virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
UpdateAllControls(); UpdateAllControls();
if (!Running) if (!ViewModel.Running)
runningTimeLbl.Text = string.Empty; runningTimeLbl.Text = string.Empty;
} }
private void CounterTimer_Tick(object sender, EventArgs e) private void clearLogBtn_Click(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 - StartingTime);
}
private void clearLogBtn_Click(object sender, EventArgs e)
{ {
logDGV.Rows.Clear(); logDGV.Rows.Clear();
} }
private void LogCopyBtn_Click(object sender, EventArgs e) private void LogCopyBtn_Click(object? sender, EventArgs e)
{ {
string logText = string.Join("\r\n", logDGV.Rows.Cast<DataGridViewRow>().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}")); string logText = string.Join("\r\n", logDGV.Rows.Cast<DataGridViewRow>().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}"));
Clipboard.SetDataObject(logText, false, 5, 150); Clipboard.SetDataObject(logText, false, 5, 150);
} }
private void LogDGV_Resize(object sender, EventArgs e) private void LogDGV_Resize(object? sender, EventArgs e)
{ {
logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width; logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width;
} }
#endregion
#region View-Model update event handling #region View-Model update event handling
private void ProcessBook_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is not ProcessBookViewModel pbvm)
return;
int index = ViewModel.Queue.IndexOf(pbvm);
UpdateControl(index, e.PropertyName);
}
private void ProcessQueue_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is null or nameof(ViewModel.QueuedCount))
{
queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
}
if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
{
errorNumberLbl.Text = ViewModel.ErrorCount.ToString();
errorNumberLbl.Visible = ViewModel.ErrorCount > 0;
}
if (e.PropertyName is null or nameof(ViewModel.CompletedCount))
{
completedNumberLbl.Text = ViewModel.CompletedCount.ToString();
completedNumberLbl.Visible = ViewModel.CompletedCount > 0;
}
if (e.PropertyName is null or nameof(ViewModel.Progress))
{
toolStripProgressBar1.Maximum = ViewModel.Queue.Count;
toolStripProgressBar1.Value = ViewModel.Queue.Completed.Count;
}
if (e.PropertyName is null or nameof(ViewModel.ProgressBarVisible))
{
toolStripProgressBar1.Visible = ViewModel.ProgressBarVisible;
}
if (e.PropertyName is null or nameof(ViewModel.RunningTime))
{
runningTimeLbl.Text = ViewModel.RunningTime;
}
}
/// <summary> /// <summary>
/// Index of the first <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/> /// Index of the first <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
/// </summary> /// </summary>
@ -315,22 +145,22 @@ This error appears to be caused by a temporary interruption of service that some
/// <summary> /// <summary>
/// Controls displaying the <see cref="ProcessBookViewModel"/> state, starting with <see cref="FirstVisible"/> /// Controls displaying the <see cref="ProcessBookViewModel"/> state, starting with <see cref="FirstVisible"/>
/// </summary> /// </summary>
private IReadOnlyList<ProcessBookControl> Panels; private IReadOnlyList<ProcessBookControl>? Panels;
/// <summary> /// <summary>
/// Updates the display of a single <see cref="ProcessBookControl"/> at <paramref name="queueIndex"/> within <see cref="Queue"/> /// Updates the display of a single <see cref="ProcessBookControl"/> at <paramref name="queueIndex"/> within <see cref="Queue"/>
/// </summary> /// </summary>
/// <param name="queueIndex">index of the <see cref="ProcessBookViewModel"/> within the <see cref="Queue"/></param> /// <param name="queueIndex">index of the <see cref="ProcessBookViewModel"/> within the <see cref="Queue"/></param>
/// <param name="propertyName">The nme of the property that needs updating. If null, all properties are updated.</param> /// <param name="propertyName">The nme of the property that needs updating. If null, all properties are updated.</param>
private void UpdateControl(int queueIndex, string propertyName = null) private void UpdateControl(int queueIndex, string? propertyName = null)
{ {
try try
{ {
int i = queueIndex - FirstVisible; int i = queueIndex - FirstVisible;
if (i > NumVisible || i < 0) return; if (Panels is null || i > NumVisible || i < 0) return;
var proc = Queue[queueIndex]; var proc = ViewModel.Queue[queueIndex];
Invoke(() => Invoke(() =>
{ {
@ -338,7 +168,7 @@ This error appears to be caused by a temporary interruption of service that some
if (propertyName is null or nameof(proc.Cover)) if (propertyName is null or nameof(proc.Cover))
Panels[i].SetCover(proc.Cover as Image); Panels[i].SetCover(proc.Cover as Image);
if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator)) if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator))
Panels[i].SetBookInfo(proc.BookText); Panels[i].SetBookInfo($"{proc.Title}\r\nBy {proc.Author}\r\nNarrated by {proc.Narrator}");
if (proc.Result != ProcessBookResult.None) if (proc.Result != ProcessBookResult.None)
{ {
@ -363,13 +193,12 @@ This error appears to be caused by a temporary interruption of service that some
private void UpdateAllControls() private void UpdateAllControls()
{ {
int numToShow = Math.Min(NumVisible, Queue.Count - FirstVisible); int numToShow = Math.Min(NumVisible, ViewModel.Queue.Count - FirstVisible);
for (int i = 0; i < numToShow; i++) for (int i = 0; i < numToShow; i++)
UpdateControl(FirstVisible + i); UpdateControl(FirstVisible + i);
} }
/// <summary> /// <summary>
/// View notified the model that a botton was clicked /// View notified the model that a botton was clicked
/// </summary> /// </summary>
@ -379,36 +208,38 @@ This error appears to be caused by a temporary interruption of service that some
{ {
try try
{ {
ProcessBookViewModel item = Queue[queueIndex]; var item = ViewModel.Queue[queueIndex];
if (buttonName == nameof(panelClicked.cancelBtn)) if (buttonName == nameof(panelClicked.cancelBtn))
{ {
if (item is not null) if (item is not null)
{
await item.CancelAsync(); await item.CancelAsync();
Queue.RemoveQueued(item); if (ViewModel.Queue.RemoveQueued(item))
virtualFlowControl2.VirtualControlCount = Queue.Count; virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
}
} }
else if (buttonName == nameof(panelClicked.moveFirstBtn)) else if (buttonName == nameof(panelClicked.moveFirstBtn))
{ {
Queue.MoveQueuePosition(item, QueuePosition.Fisrt); ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
UpdateAllControls(); UpdateAllControls();
} }
else if (buttonName == nameof(panelClicked.moveUpBtn)) else if (buttonName == nameof(panelClicked.moveUpBtn))
{ {
Queue.MoveQueuePosition(item, QueuePosition.OneUp); ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneUp);
UpdateControl(queueIndex); UpdateControl(queueIndex);
if (queueIndex > 0) if (queueIndex > 0)
UpdateControl(queueIndex - 1); UpdateControl(queueIndex - 1);
} }
else if (buttonName == nameof(panelClicked.moveDownBtn)) else if (buttonName == nameof(panelClicked.moveDownBtn))
{ {
Queue.MoveQueuePosition(item, QueuePosition.OneDown); ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneDown);
UpdateControl(queueIndex); UpdateControl(queueIndex);
if (queueIndex + 1 < Queue.Count) if (queueIndex + 1 < ViewModel.Queue.Count)
UpdateControl(queueIndex + 1); UpdateControl(queueIndex + 1);
} }
else if (buttonName == nameof(panelClicked.moveLastBtn)) else if (buttonName == nameof(panelClicked.moveLastBtn))
{ {
Queue.MoveQueuePosition(item, QueuePosition.Last); ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Last);
UpdateAllControls(); UpdateAllControls();
} }
} }
@ -429,18 +260,9 @@ This error appears to be caused by a temporary interruption of service that some
UpdateAllControls(); UpdateAllControls();
} }
/// <summary>
/// Model updates the view
/// </summary>
private void Pbook_DataAvailable(object sender, PropertyChangedEventArgs e)
{
int index = Queue.IndexOf((ProcessBookViewModel)sender);
UpdateControl(index, e.PropertyName);
}
#endregion #endregion
private void numericUpDown1_ValueChanged(object sender, EventArgs e) private void numericUpDown1_ValueChanged(object? sender, EventArgs e)
{ {
var newValue = (long)(numericUpDown1.Value * 1024 * 1024); var newValue = (long)(numericUpDown1.Value * 1024 * 1024);
@ -463,6 +285,7 @@ This error appears to be caused by a temporary interruption of service that some
: 2; : 2;
} }
} }
public class NumericUpDownSuffix : NumericUpDown public class NumericUpDownSuffix : NumericUpDown
{ {
[Description("Suffix displayed after numeric value."), Category("Data")] [Description("Suffix displayed after numeric value."), Category("Data")]
@ -480,6 +303,8 @@ This error appears to be caused by a temporary interruption of service that some
} }
} }
private string _suffix = string.Empty; private string _suffix = string.Empty;
[AllowNull]
public override string Text public override string Text
{ {
get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty); get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty);
@ -492,4 +317,3 @@ This error appears to be caused by a temporary interruption of service that some
} }
} }
} }
}

View File

@ -0,0 +1,74 @@
using DataLayer;
using LibationUiBase.ProcessQueue;
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
#nullable enable
namespace LibationWinForms.ProcessQueue;
internal class ProcessQueueViewModel : ProcessQueueViewModelBase
{
public event EventHandler<string>? LogWritten;
/// <summary>
/// Fires when a ProcessBookViewModelBase in the queue has a property changed
/// </summary>
public event EventHandler<PropertyChangedEventArgs>? BookPropertyChanged;
private ObservableCollection<ProcessBookViewModelBase> Items { get; }
public ProcessQueueViewModel() : base(CreateEmptyList())
{
Items = Queue.UnderlyingList as ObservableCollection<ProcessBookViewModelBase>
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
Items.CollectionChanged += Items_CollectionChanged;
}
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
subscribe(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
unubscribe(e.OldItems);
break;
}
void subscribe(IList? items)
{
foreach (var item in e.NewItems?.OfType<ProcessBookViewModel>() ?? [])
item.PropertyChanged += Item_PropertyChanged;
}
void unubscribe(IList? items)
{
foreach (var item in e.NewItems?.OfType<ProcessBookViewModel>() ?? [])
item.PropertyChanged -= Item_PropertyChanged;
}
}
private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e)
=> BookPropertyChanged?.Invoke(sender, e);
public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim()));
protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook)
=> new ProcessBookViewModel(libraryBook, Logger);
private static ObservableCollection<ProcessBookViewModelBase> CreateEmptyList()
=> new ProcessBookCollection();
private class ProcessBookCollection : ObservableCollection<ProcessBookViewModelBase>
{
protected override void ClearItems()
{
//ObservableCollection doesn't raise Remove for each item on Clear, so we need to do it ourselves.
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, this));
base.ClearItems();
}
}
}