diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs index 7eed8118..498dfd47 100644 --- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs @@ -1,282 +1,40 @@ -using ApplicationServices; -using Avalonia.Collections; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; using DataLayer; using LibationFileManager; -using LibationUiBase; -using LibationUiBase.Forms; using LibationUiBase.ProcessQueue; -using ReactiveUI; using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; #nullable enable -namespace LibationAvalonia.ViewModels +namespace LibationAvalonia.ViewModels; + +public class ProcessQueueViewModel : ProcessQueueViewModelBase { - - public class ProcessQueueViewModel : ViewModelBase, ILogForm, IProcessQueue + public override void WriteLine(string text) { - public ObservableCollection LogEntries { get; } = new(); - public AvaloniaList Items { get; } = new(); - public TrackedQueue 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 + Dispatcher.UIThread.Invoke(() => + LogEntries.Add(new() { - 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(() => - LogEntries.Add(new() - { - LogDate = DateTime.Now, - LogMessage = text.Trim() - })); - } - - - #region Add Books to Queue - - 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; - } - - public bool RemoveCompleted(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 entries) - { - List procs = new(); - foreach (var entry in entries) - { - 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 entries) - { - List 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 entries) - { - List 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 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); - } + LogDate = DateTime.Now, + LogMessage = text.Trim() + })); } - public class LogEntry + public ProcessQueueViewModel() : base(CreateEmptyList()) { - public DateTime LogDate { get; init; } - public string LogDateString => LogDate.ToShortTimeString(); - public string? LogMessage { get; init; } + Items = Queue.UnderlyingList as AvaloniaList + ?? throw new ArgumentNullException(nameof(Queue.UnderlyingList)); + } + + public AvaloniaList Items { get; } + protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook) + => new ProcessBookViewModel(libraryBook, Logger); + + private static AvaloniaList CreateEmptyList() + { + if (Design.IsDesignMode) + _ = Configuration.Instance.LibationFiles; + return new AvaloniaList(); } } diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs index 8eda2373..97ae56f9 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs @@ -17,7 +17,7 @@ namespace LibationAvalonia.Views { public partial class ProcessQueueControl : UserControl { - private TrackedQueue? Queue => _viewModel?.Queue; + private TrackedQueue? Queue => _viewModel?.Queue; private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel; public ProcessQueueControl() diff --git a/Source/LibationUiBase/IProcessQueue.cs b/Source/LibationUiBase/IProcessQueue.cs deleted file mode 100644 index 84b21e0f..00000000 --- a/Source/LibationUiBase/IProcessQueue.cs +++ /dev/null @@ -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 entries); - void AddConvertMp3(IEnumerable entries); - void AddDownloadDecrypt(IEnumerable entries); -} - -public static class ProcessQueueExtensions -{ - - public static bool QueueDownloadPdf(this IProcessQueue queue, IList 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 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 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; - } -} diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs new file mode 100644 index 00000000..40ff8da8 --- /dev/null +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs @@ -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 LogEntries { get; } = new(); + public TrackedQueue 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? 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 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 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 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 entries) + { + List 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 entries) + { + List 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 entries) + { + List 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 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; } +} diff --git a/Source/LibationUiBase/TrackedQueue[T].cs b/Source/LibationUiBase/TrackedQueue[T].cs index 51f36f0b..e36dac4b 100644 --- a/Source/LibationUiBase/TrackedQueue[T].cs +++ b/Source/LibationUiBase/TrackedQueue[T].cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; +#nullable enable namespace LibationUiBase { public enum QueuePosition @@ -34,10 +35,10 @@ namespace LibationUiBase */ public class TrackedQueue where T : class { - public event EventHandler CompletedCountChanged; - public event EventHandler QueuededCountChanged; + public event EventHandler? CompletedCountChanged; + public event EventHandler? QueuededCountChanged; - public T Current { get; private set; } + public T? Current { get; private set; } public IReadOnlyList Queued => _queued; public IReadOnlyList Completed => _completed; @@ -46,9 +47,10 @@ namespace LibationUiBase private readonly List _completed = new(); private readonly object lockObject = new(); - private readonly ICollection _underlyingList; + private readonly ICollection? _underlyingList; + public ICollection? UnderlyingList => _underlyingList; - public TrackedQueue(ICollection underlyingList = null) + public TrackedQueue(ICollection? underlyingList = null) { _underlyingList = underlyingList; } @@ -169,7 +171,7 @@ namespace LibationUiBase } } - public T FirstOrDefault(Func predicate) + public T? FirstOrDefault(Func predicate) { lock (lockObject) { diff --git a/Source/LibationWinForms/Form1.Liberate.cs b/Source/LibationWinForms/Form1.Liberate.cs index 2bda3e52..9b905be4 100644 --- a/Source/LibationWinForms/Form1.Liberate.cs +++ b/Source/LibationWinForms/Form1.Liberate.cs @@ -17,7 +17,7 @@ namespace LibationWinForms try { var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray()); - if (processBookQueue1.QueueDownloadDecrypt(unliberated)) + if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated)) SetQueueCollapseState(false); } catch (Exception ex) @@ -28,7 +28,7 @@ namespace LibationWinForms 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); } @@ -42,7 +42,7 @@ namespace LibationWinForms "Convert all M4b => Mp3?", MessageBoxButtons.YesNo, 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); } } diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs index 41d28080..f6260e5e 100644 --- a/Source/LibationWinForms/Form1.ProcessQueue.cs +++ b/Source/LibationWinForms/Form1.ProcessQueue.cs @@ -16,7 +16,7 @@ namespace LibationWinForms int WidthChange = 0; private void Configure_ProcessQueue() { - processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut; + processBookQueue1.PopoutButton.Click += ProcessBookQueue1_PopOut; WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth; int width = this.Width; @@ -29,7 +29,7 @@ namespace LibationWinForms { try { - if (processBookQueue1.QueueDownloadDecrypt(libraryBooks)) + if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks)) SetQueueCollapseState(false); 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); - 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); } catch (Exception ex) @@ -67,7 +67,7 @@ namespace LibationWinForms { try { - if (processBookQueue1.QueueConvertToMp3(libraryBooks)) + if (processBookQueue1.ViewModel.QueueConvertToMp3(libraryBooks)) SetQueueCollapseState(false); } catch (Exception ex) @@ -87,10 +87,14 @@ namespace LibationWinForms } else if (!collapsed && splitContainer1.Panel2Collapsed) { + if (!processBookQueue1.PopoutButton.Visible) + //Queue is in popout mode. Do nothing. + return; + Width += WidthChange; splitContainer1.Panel2.Controls.Add(processBookQueue1); splitContainer1.Panel2Collapsed = false; - processBookQueue1.popoutBtn.Visible = true; + processBookQueue1.PopoutButton.Visible = true; } Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed)); @@ -110,7 +114,7 @@ namespace LibationWinForms dockForm.FormClosing += DockForm_FormClosing; splitContainer1.Panel2.Controls.Remove(processBookQueue1); splitContainer1.Panel2Collapsed = true; - processBookQueue1.popoutBtn.Visible = false; + processBookQueue1.PopoutButton.Visible = false; dockForm.PassControl(processBookQueue1); dockForm.Show(); this.Width -= dockForm.WidthChange; @@ -127,7 +131,7 @@ namespace LibationWinForms this.Width += dockForm.WidthChange; splitContainer1.Panel2.Controls.Add(dockForm.RegainControl()); splitContainer1.Panel2Collapsed = false; - processBookQueue1.popoutBtn.Visible = true; + processBookQueue1.PopoutButton.Visible = true; dockForm.SaveSizeAndLocation(Configuration.Instance); this.Focus(); toggleQueueHideBtn.Visible = true; diff --git a/Source/LibationWinForms/Form1.VisibleBooks.cs b/Source/LibationWinForms/Form1.VisibleBooks.cs index a72bb40b..fd248c3b 100644 --- a/Source/LibationWinForms/Form1.VisibleBooks.cs +++ b/Source/LibationWinForms/Form1.VisibleBooks.cs @@ -59,7 +59,7 @@ namespace LibationWinForms { try { - if (processBookQueue1.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray())) + if (processBookQueue1.ViewModel.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray())) SetQueueCollapseState(false); } catch (Exception ex) diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs index 80d4f405..f302b4b8 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs @@ -11,6 +11,4 @@ public class ProcessBookViewModel : ProcessBookViewModelBase protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize) => WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80); - - public string BookText => $"{Title}\r\nBy {Author}\r\nNarrated by {Narrator}"; } diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs index d9a97565..477c8a39 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs @@ -55,7 +55,6 @@ this.panel2 = new System.Windows.Forms.Panel(); this.logCopyBtn = 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.tabControl1.SuspendLayout(); this.tabPage1.SuspendLayout(); @@ -329,11 +328,6 @@ 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); - // // ProcessQueueControl // this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); @@ -377,7 +371,6 @@ 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; diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index 2251ee37..a6b3a48d 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -1,495 +1,319 @@ -using System; +using LibationFileManager; +using LibationUiBase; +using LibationUiBase.ProcessQueue; +using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Linq; -using System.Threading.Tasks; 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 { - internal partial class ProcessQueueControl : UserControl, ILogForm, IProcessQueue + public ProcessQueueViewModel ViewModel { get; } = new(); + public ToolStripButton PopoutButton { get; } = new() { - 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; - } - } + DisplayStyle = ToolStripItemDisplayStyle.Text, + Name = nameof(PopoutButton), + Text = "Pop Out", + TextAlign = ContentAlignment.MiddleCenter, + Alignment = ToolStripItemAlignment.Right, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right, + }; - private int CompletedCount - { - set - { - completedNumberLbl.Text = value.ToString(); - completedNumberLbl.Visible = value > 0; - } - } + public ProcessQueueControl() + { + InitializeComponent(); - public Task QueueRunner { get; private set; } - public bool Running => !QueueRunner?.IsCompleted ?? false; - public ToolStripButton popoutBtn = new(); + var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; + numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps; + statusStrip1.Items.Add(PopoutButton); - public ProcessQueueControl() - { - InitializeComponent(); + virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData; + virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked; - var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; - numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps; + ViewModel.LogWritten += (_, text) => WriteLine(text); + ViewModel.PropertyChanged += ProcessQueue_PropertyChanged; + ViewModel.BookPropertyChanged += ProcessBook_PropertyChanged; + Load += ProcessQueueControl_Load; + } - 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); + private void ProcessQueueControl_Load(object? sender, EventArgs e) + { + if (DesignMode) return; + ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null)); + } - Logger = LogMe.RegisterForm(this); + public void WriteLine(string text) + { + if (!IsDisposed) + logDGV.Rows.Add(DateTime.Now, text.Trim()); + } - virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData; - virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked; + private async void cancelAllBtn_Click(object? sender, EventArgs e) + { + ViewModel.Queue.ClearQueue(); + if (ViewModel.Queue.Current is not null) + await ViewModel.Queue.Current.CancelAsync(); + virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; + UpdateAllControls(); + } - Queue.QueuededCountChanged += Queue_QueuededCountChanged; - Queue.CompletedCountChanged += Queue_CompletedCountChanged; - - Load += ProcessQueueControl_Load; - } - - private void ProcessQueueControl_Load(object sender, EventArgs e) - { - if (DesignMode) return; + private void btnClearFinished_Click(object? sender, EventArgs e) + { + ViewModel.Queue.ClearCompleted(); + virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; + UpdateAllControls(); + if (!ViewModel.Running) runningTimeLbl.Text = string.Empty; - QueuedCount = 0; - ErrorCount = 0; - CompletedCount = 0; - } + } - private bool isBookInQueue(DataLayer.LibraryBook libraryBook) + 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; + } + + #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)) { - 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; + queueNumberLbl.Text = ViewModel.QueuedCount.ToString(); + queueNumberLbl.Visible = ViewModel.QueuedCount > 0; + virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; } - - 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 entries) + if (e.PropertyName is null or nameof(ViewModel.ErrorCount)) { - List 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); + errorNumberLbl.Text = ViewModel.ErrorCount.ToString(); + errorNumberLbl.Visible = ViewModel.ErrorCount > 0; } - - public void AddDownloadDecrypt(IEnumerable entries) + if (e.PropertyName is null or nameof(ViewModel.CompletedCount)) { - List 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); + completedNumberLbl.Text = ViewModel.CompletedCount.ToString(); + completedNumberLbl.Visible = ViewModel.CompletedCount > 0; } - - public void AddConvertMp3(IEnumerable entries) + if (e.PropertyName is null or nameof(ViewModel.Progress)) { - List 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); + toolStripProgressBar1.Maximum = ViewModel.Queue.Count; + toolStripProgressBar1.Value = ViewModel.Queue.Completed.Count; } - private void AddToQueue(IEnumerable pbook) + if (e.PropertyName is null or nameof(ViewModel.ProgressBarVisible)) { - if (!IsHandleCreated) - CreateControl(); - - BeginInvoke(() => - { - Queue.Enqueue(pbook); - if (!Running) - QueueRunner = QueueLoop(); - }); + toolStripProgressBar1.Visible = ViewModel.ProgressBarVisible; } - - DateTime StartingTime; - private async Task QueueLoop() + if (e.PropertyName is null or nameof(ViewModel.RunningTime)) { - 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) - { - if (IsDisposed) return; - - var timeStamp = DateTime.Now; - Invoke(() => 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.FailedRetry 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 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(); - } - - 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 - StartingTime); - } - - 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 - /// The nme of the property that needs updating. If null, all properties are updated. - private void UpdateControl(int queueIndex, string propertyName = null) - { - try - { - int i = queueIndex - FirstVisible; - - if (i > NumVisible || i < 0) return; - - var proc = Queue[queueIndex]; - - Invoke(() => - { - Panels[i].SuspendLayout(); - if (propertyName is null or nameof(proc.Cover)) - Panels[i].SetCover(proc.Cover as Image); - if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator)) - Panels[i].SetBookInfo(proc.BookText); - - if (proc.Result != ProcessBookResult.None) - { - Panels[i].SetResult(proc.Result); - return; - } - - if (propertyName is null or nameof(proc.Status)) - Panels[i].SetStatus(proc.Status); - if (propertyName is null or nameof(proc.Progress)) - Panels[i].SetProgrss(proc.Progress); - if (propertyName is null or nameof(proc.TimeRemaining)) - Panels[i].SetRemainingTime(proc.TimeRemaining); - Panels[i].ResumeLayout(); - }); - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "Error updating the queued item's display."); - } - } - - 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) - { - try - { - ProcessBookViewModel item = Queue[queueIndex]; - if (buttonName == nameof(panelClicked.cancelBtn)) - { - if (item is not null) - await item.CancelAsync(); - 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(); - } - } - catch(Exception ex) - { - Serilog.Log.Logger.Error(ex, "Error handling button click from queued item"); - } - } - - /// - /// 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((ProcessBookViewModel)sender); - UpdateControl(index, e.PropertyName); - } - - #endregion - - private void numericUpDown1_ValueChanged(object sender, EventArgs e) - { - var newValue = (long)(numericUpDown1.Value * 1024 * 1024); - - var config = Configuration.Instance; - config.DownloadSpeedLimit = newValue; - if (config.DownloadSpeedLimit > newValue) - numericUpDown1.Value = - numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024 - : 0; - - numericUpDown1.Increment = - numericUpDown1.Value > 100 ? 10 - : numericUpDown1.Value > 10 ? 1 - : numericUpDown1.Value > 1 ? 0.1m - : 0.01m; - - numericUpDown1.DecimalPlaces = - numericUpDown1.Value >= 10 ? 0 - : numericUpDown1.Value >= 1 ? 1 - : 2; + runningTimeLbl.Text = ViewModel.RunningTime; } } - public class NumericUpDownSuffix : NumericUpDown + + /// + /// 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 + /// The nme of the property that needs updating. If null, all properties are updated. + private void UpdateControl(int queueIndex, string? propertyName = null) { - [Description("Suffix displayed after numeric value."), Category("Data")] - [Browsable(true)] - [EditorBrowsable(EditorBrowsableState.Always)] - [DisallowNull] - public string Suffix + try { - get => _suffix; - set + int i = queueIndex - FirstVisible; + + if (Panels is null || i > NumVisible || i < 0) return; + + var proc = ViewModel.Queue[queueIndex]; + + Invoke(() => { - base.Text = string.IsNullOrEmpty(_suffix) ? base.Text : base.Text.Replace(_suffix, value); - _suffix = value; - ChangingText = true; + Panels[i].SuspendLayout(); + if (propertyName is null or nameof(proc.Cover)) + Panels[i].SetCover(proc.Cover as Image); + if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator)) + Panels[i].SetBookInfo($"{proc.Title}\r\nBy {proc.Author}\r\nNarrated by {proc.Narrator}"); + + if (proc.Result != ProcessBookResult.None) + { + Panels[i].SetResult(proc.Result); + return; + } + + if (propertyName is null or nameof(proc.Status)) + Panels[i].SetStatus(proc.Status); + if (propertyName is null or nameof(proc.Progress)) + Panels[i].SetProgrss(proc.Progress); + if (propertyName is null or nameof(proc.TimeRemaining)) + Panels[i].SetRemainingTime(proc.TimeRemaining); + Panels[i].ResumeLayout(); + }); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "Error updating the queued item's display."); + } + } + + private void UpdateAllControls() + { + int numToShow = Math.Min(NumVisible, ViewModel.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) + { + try + { + var item = ViewModel.Queue[queueIndex]; + if (buttonName == nameof(panelClicked.cancelBtn)) + { + if (item is not null) + { + await item.CancelAsync(); + if (ViewModel.Queue.RemoveQueued(item)) + virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; + } + } + else if (buttonName == nameof(panelClicked.moveFirstBtn)) + { + ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Fisrt); + UpdateAllControls(); + } + else if (buttonName == nameof(panelClicked.moveUpBtn)) + { + ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneUp); + UpdateControl(queueIndex); + if (queueIndex > 0) + UpdateControl(queueIndex - 1); + } + else if (buttonName == nameof(panelClicked.moveDownBtn)) + { + ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneDown); + UpdateControl(queueIndex); + if (queueIndex + 1 < ViewModel.Queue.Count) + UpdateControl(queueIndex + 1); + } + else if (buttonName == nameof(panelClicked.moveLastBtn)) + { + ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Last); + UpdateAllControls(); } } - private string _suffix = string.Empty; - public override string Text + catch(Exception ex) { - get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty); - set - { - if (Value == Minimum) - base.Text = "∞"; - else - base.Text = value + Suffix; - } + Serilog.Log.Logger.Error(ex, "Error handling button click from queued item"); + } + } + + /// + /// View needs updating + /// + private void VirtualFlowControl1_RequestData(int firstIndex, int numVisible, IReadOnlyList panelsToFill) + { + FirstVisible = firstIndex; + NumVisible = numVisible; + Panels = panelsToFill; + UpdateAllControls(); + } + + #endregion + + private void numericUpDown1_ValueChanged(object? sender, EventArgs e) + { + var newValue = (long)(numericUpDown1.Value * 1024 * 1024); + + var config = Configuration.Instance; + config.DownloadSpeedLimit = newValue; + if (config.DownloadSpeedLimit > newValue) + numericUpDown1.Value = + numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024 + : 0; + + numericUpDown1.Increment = + numericUpDown1.Value > 100 ? 10 + : numericUpDown1.Value > 10 ? 1 + : numericUpDown1.Value > 1 ? 0.1m + : 0.01m; + + numericUpDown1.DecimalPlaces = + numericUpDown1.Value >= 10 ? 0 + : numericUpDown1.Value >= 1 ? 1 + : 2; + } +} + +public class NumericUpDownSuffix : NumericUpDown +{ + [Description("Suffix displayed after numeric value."), Category("Data")] + [Browsable(true)] + [EditorBrowsable(EditorBrowsableState.Always)] + [DisallowNull] + public string Suffix + { + get => _suffix; + set + { + base.Text = string.IsNullOrEmpty(_suffix) ? base.Text : base.Text.Replace(_suffix, value); + _suffix = value; + ChangingText = true; + } + } + private string _suffix = string.Empty; + + [AllowNull] + public override string Text + { + get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty); + set + { + if (Value == Minimum) + base.Text = "∞"; + else + base.Text = value + Suffix; } } } diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs new file mode 100644 index 00000000..d94eb845 --- /dev/null +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs @@ -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? LogWritten; + /// + /// Fires when a ProcessBookViewModelBase in the queue has a property changed + /// + public event EventHandler? BookPropertyChanged; + private ObservableCollection Items { get; } + + public ProcessQueueViewModel() : base(CreateEmptyList()) + { + Items = Queue.UnderlyingList as ObservableCollection + ?? 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() ?? []) + item.PropertyChanged += Item_PropertyChanged; + } + + void unubscribe(IList? items) + { + foreach (var item in e.NewItems?.OfType() ?? []) + 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 CreateEmptyList() + => new ProcessBookCollection(); + + private class ProcessBookCollection : ObservableCollection + { + 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(); + } + } +}